├── .gitignore
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── dh
│ │ └── updemo
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── dh
│ │ │ └── updemo
│ │ │ ├── App.kt
│ │ │ ├── FileItem.kt
│ │ │ ├── FilesItem.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── MultipleSingleFileUploadsActivity.kt
│ │ │ ├── SingleFileUploadActivity.kt
│ │ │ ├── UploadAdapter.kt
│ │ │ └── UploadMultipleFilesSimultaneouslyActivity.kt
│ └── res
│ │ ├── drawable
│ │ ├── ic_launcher_background.xml
│ │ └── ic_launcher_foreground.xml
│ │ ├── layout
│ │ ├── activity_main.xml
│ │ ├── item_upload.xml
│ │ ├── multiple_single_files_layout.xml
│ │ ├── single_file_upload_layout.xml
│ │ └── upload_multiple_files_simultaneously_layout.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── values-land
│ │ └── dimens.xml
│ │ ├── values-night
│ │ └── themes.xml
│ │ ├── values-v23
│ │ └── themes.xml
│ │ ├── values-w1240dp
│ │ └── dimens.xml
│ │ ├── values-w600dp
│ │ └── dimens.xml
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ │ └── xml
│ │ ├── backup_rules.xml
│ │ ├── data_extraction_rules.xml
│ │ └── network_security_config.xml
│ └── test
│ └── java
│ └── com
│ └── dh
│ └── updemo
│ └── ExampleUnitTest.kt
├── build.gradle
├── config.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── jitpack.yml
├── pictureresources
├── one.gif
├── three.gif
└── two.gif
├── quickupload
├── .gitignore
├── build.gradle
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── dh
│ │ └── quickupload
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ └── java
│ │ └── com
│ │ └── dh
│ │ └── quickupload
│ │ ├── GeneralUploadRequest.kt
│ │ ├── GeneralUploadTask.kt
│ │ ├── UploadConfiguration.kt
│ │ ├── UploadRequest.kt
│ │ ├── UploadService.kt
│ │ ├── UploadTask.kt
│ │ ├── data
│ │ ├── HttpUploadTaskParameters.kt
│ │ ├── NameValue.kt
│ │ ├── RetryPolicyConfig.kt
│ │ ├── UploadElapsedTime.kt
│ │ ├── UploadFile.kt
│ │ ├── UploadInfo.kt
│ │ ├── UploadRate.kt
│ │ ├── UploadStatus.kt
│ │ └── UploadTaskParameters.kt
│ │ ├── exceptions
│ │ └── Exceptions.kt
│ │ ├── extensions
│ │ └── Extensions.kt
│ │ ├── network
│ │ ├── BaseNetwork.kt
│ │ ├── BodyWriter.kt
│ │ ├── NetworkRequest.kt
│ │ ├── ServerResponse.kt
│ │ └── okhttp
│ │ │ ├── OkHttpBodyWriter.kt
│ │ │ ├── OkHttpExtensions.kt
│ │ │ ├── OkHttpNetwork.kt
│ │ │ └── OkHttpNetworkRequest.kt
│ │ ├── observer
│ │ ├── network
│ │ │ └── NetworkMonitor.kt
│ │ └── task
│ │ │ ├── TaskCompletionNotifier.kt
│ │ │ ├── UploadObserverBase.kt
│ │ │ └── UploadTaskObserver.kt
│ │ ├── quick
│ │ ├── QuickUploadRequest.kt
│ │ ├── QuickUploadTask.kt
│ │ └── UploadFileExtensions.kt
│ │ └── tools
│ │ ├── datapreservation
│ │ ├── Persistable.kt
│ │ └── PersistableData.kt
│ │ ├── logger
│ │ ├── DefaultExt.kt
│ │ └── Logger.kt
│ │ └── translationfile
│ │ ├── ContentResolverSchemeHandler.kt
│ │ ├── FileSchemeHandler.kt
│ │ └── SchemeHandler.kt
│ └── test
│ └── java
│ └── com
│ └── dh
│ └── quickupload
│ └── ExampleUnitTest.kt
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | # Gradle files
2 | .gradle/
3 | build/
4 |
5 | # Local configuration file (sdk path, etc)
6 | local.properties
7 |
8 | # Log/OS Files
9 | *.log
10 |
11 | # Android Studio generated files and folders
12 | captures/
13 | .externalNativeBuild/
14 | .cxx/
15 | *.apk
16 | output.json
17 |
18 | # IntelliJ
19 | *.iml
20 | .idea/
21 | misc.xml
22 | deploymentTargetDropDown.xml
23 | render.experimental.xml
24 |
25 | # Keystore files
26 | *.jks
27 | *.keystore
28 |
29 | # Google Services (e.g. APIs or Firebase)
30 | google-services.json
31 |
32 | # Android Profiling
33 | *.hprof
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://imgse.com/i/pk3f4HO)
2 |
3 | ### 一个让开发者快速完成上传功能的框架 支持java、kotlin,使用说明:[QuickUpDoc](https://xj-up.github.io/quickupdoc/)
4 |
5 | # 截图
6 |
7 | | 单文件上传模式 | 单文件上传模式多个文件上传 | 多个文件同时上传模式 |
8 | |----------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------|
9 | |  |  |  |
10 |
11 | # 如何集成
12 |
13 | - 添加仓库
14 |
15 | ```groovy
16 | // build.gradle(Project:)
17 | allprojects {
18 | repositories {
19 | maven { url 'https://jitpack.io' }
20 | }
21 | }
22 | ```
23 |
24 | - 添加依赖
25 |
26 | ```groovy
27 | // build.gradle(Module:)
28 | dependencies {
29 | implementation 'com.github.XJ-Up:quickupload:1.2.1'
30 | }
31 | ```
32 | ## 具体使用可参考[demo](https://github.com/XJ-Up/quickupload/tree/main/app/src/main/java/com/dh/updemo)
33 | #### 如果你没有服务器上传接口,你可以下载服务器[demo](https://github.com/XJ-Up/TestServer)搭建自己的测试服务器,来体验quickupload
34 |
35 |
36 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'org.jetbrains.kotlin.android'
4 | }
5 |
6 | android {
7 | namespace 'com.dh.updemo'
8 | compileSdk compilesdk_version
9 |
10 | defaultConfig {
11 | applicationId "com.dh.updemo"
12 | minSdk minsdk_version
13 | targetSdk targetsdk_version
14 | versionCode 4
15 | versionName "1.0.0"
16 |
17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
18 | }
19 |
20 | buildTypes {
21 | release {
22 | minifyEnabled false
23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
24 | }
25 | }
26 | compileOptions {
27 | sourceCompatibility JavaVersion.VERSION_1_8
28 | targetCompatibility JavaVersion.VERSION_1_8
29 | }
30 | kotlinOptions {
31 | jvmTarget = '1.8'
32 | }
33 | buildFeatures {
34 | viewBinding true
35 | }
36 | }
37 |
38 | dependencies {
39 | testImplementation 'junit:junit:4.+'
40 | androidTestImplementation 'androidx.test.ext:junit:1.1.3'
41 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
42 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
43 | implementation 'com.google.android.material:material:1.4.0'
44 | implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
45 | implementation project(':quickupload')
46 | // implementation 'com.github.XJ-Up:quickupload:1.2.1'
47 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/dh/updemo/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.dh.updemo
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.dh.updemo", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
18 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
29 |
31 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dh/updemo/App.kt:
--------------------------------------------------------------------------------
1 | package com.dh.updemo
2 |
3 | import android.app.Application
4 | import android.app.NotificationChannel
5 | import android.app.NotificationManager
6 | import android.content.Context
7 | import android.os.Build
8 | import com.dh.quickupload.BuildConfig
9 | import com.dh.quickupload.UploadConfiguration
10 |
11 | class App : Application() {
12 | companion object {
13 | const val notificationChannelID = "TestChannel"
14 | }
15 |
16 | private fun createNotificationChannel() {
17 | if (Build.VERSION.SDK_INT >= 26) {
18 | val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
19 | val channel = NotificationChannel(
20 | notificationChannelID,
21 | "TestApp Channel",
22 | NotificationManager.IMPORTANCE_LOW
23 | )
24 | manager.createNotificationChannel(channel)
25 | }
26 | }
27 | override fun onCreate() {
28 | super.onCreate()
29 | createNotificationChannel()
30 | UploadConfiguration.initialize(
31 | context = this,
32 | defaultNotificationChannel = notificationChannelID,
33 | debug = BuildConfig.DEBUG
34 | )
35 | }
36 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/dh/updemo/FileItem.kt:
--------------------------------------------------------------------------------
1 | package com.dh.updemo
2 |
3 | import com.dh.quickupload.observer.task.UploadObserverBase
4 |
5 | /**
6 | * 单个地址文件上传示例
7 | * 使用:
8 | * 继承 UploadObserverBase()
9 | *
10 | */
11 | data class FileItem(
12 | val fileName: String,
13 | val filePath: String,
14 | override val uploadId: String,
15 | ) : UploadObserverBase(uploadId)
--------------------------------------------------------------------------------
/app/src/main/java/com/dh/updemo/FilesItem.kt:
--------------------------------------------------------------------------------
1 | package com.dh.updemo
2 |
3 | import com.dh.quickupload.observer.task.UploadObserverBase
4 |
5 | /**
6 | * 多个地址文件上传示例
7 | * 使用:
8 | * 继承 UploadObserverBase()
9 | *
10 | */
11 | data class FilesItem(
12 | val fileName: String,
13 | val filePath: MutableList,
14 | override val uploadId: String,
15 | ) : UploadObserverBase(uploadId)
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dh/updemo/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.dh.updemo
2 |
3 | import android.Manifest
4 | import android.content.Intent
5 | import android.content.pm.PackageManager
6 | import android.os.Build
7 | import androidx.appcompat.app.AppCompatActivity
8 | import android.os.Bundle
9 | import android.widget.Button
10 | import androidx.activity.result.contract.ActivityResultContracts
11 | import androidx.core.content.ContextCompat
12 |
13 |
14 | class MainActivity : AppCompatActivity() {
15 | private val notificationPermissionRequest =
16 | registerForActivityResult(ActivityResultContracts.RequestPermission()) {
17 | }
18 |
19 | override fun onCreate(savedInstanceState: Bundle?) {
20 | super.onCreate(savedInstanceState)
21 | setContentView(R.layout.activity_main)
22 | checkPostNotificationsPermission()
23 | findViewById(R.id.singleFile).setOnClickListener {
24 | startActivity(Intent(this, SingleFileUploadActivity::class.java))
25 | }
26 | findViewById(R.id.singleMultiple).setOnClickListener {
27 | startActivity(Intent(this, MultipleSingleFileUploadsActivity::class.java))
28 | }
29 | findViewById(R.id.multipleSimultaneous).setOnClickListener {
30 | startActivity(Intent(this, UploadMultipleFilesSimultaneouslyActivity::class.java))
31 | }
32 |
33 | }
34 |
35 | private fun checkPostNotificationsPermission() {
36 | if (Build.VERSION.SDK_INT >= 33 && ContextCompat.checkSelfPermission(
37 | this,
38 | Manifest.permission.POST_NOTIFICATIONS
39 | ) != PackageManager.PERMISSION_GRANTED
40 | ) {
41 | notificationPermissionRequest.launch(Manifest.permission.POST_NOTIFICATIONS)
42 | }
43 | }
44 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/dh/updemo/MultipleSingleFileUploadsActivity.kt:
--------------------------------------------------------------------------------
1 | package com.dh.updemo
2 |
3 | import android.app.Activity
4 | import android.content.Intent
5 | import android.net.Uri
6 | import android.os.Bundle
7 | import android.provider.OpenableColumns
8 | import android.widget.Button
9 | import androidx.appcompat.app.AppCompatActivity
10 | import androidx.recyclerview.widget.LinearLayoutManager
11 | import androidx.recyclerview.widget.RecyclerView
12 | import com.dh.quickupload.UploadService
13 | import com.dh.quickupload.data.UploadStatus
14 | import com.dh.quickupload.quick.QuickUploadRequest
15 | import java.io.File
16 | import java.lang.ref.WeakReference
17 |
18 | class MultipleSingleFileUploadsActivity : AppCompatActivity() {
19 | private lateinit var recyclerView: RecyclerView
20 | private lateinit var uploadAdapter: UploadAdapter
21 | private val fileList = mutableListOf()
22 |
23 | companion object {
24 | private const val READ_REQUEST_CODE = 7
25 | }
26 |
27 | private var filePath: MutableList = mutableListOf()
28 | override fun onCreate(savedInstanceState: Bundle?) {
29 | super.onCreate(savedInstanceState)
30 | setContentView(R.layout.multiple_single_files_layout)
31 |
32 | recyclerView = findViewById(R.id.recycler_view)
33 | recyclerView.layoutManager = LinearLayoutManager(this)
34 | recyclerView.itemAnimator=null
35 |
36 | uploadAdapter = UploadAdapter(fileList) {
37 | val uploadStatus = it.status
38 |
39 | if (uploadStatus == UploadStatus.InProgress) {
40 | it.stopUpload()
41 | } else if (uploadStatus == UploadStatus.DEFAULT || uploadStatus == UploadStatus.Error) {
42 | val filePath =it.filePath
43 | val uploadRequest =
44 | QuickUploadRequest(this, serverUrl = "http://192.168.30.137:8080/upload")
45 | .setMethod("POST")
46 | .addFileToUpload(
47 | filePath = filePath,
48 | parameterName = "files"
49 | )
50 | it.quickUploadRequest = uploadRequest
51 | it.startUpload()
52 | }
53 | }
54 | recyclerView.adapter = uploadAdapter
55 | findViewById(R.id.uploadStart).setOnClickListener {
56 | fileList.forEachIndexed { index, s ->
57 | if (UploadService.taskList.contains(s.uploadId)){
58 | return@forEachIndexed
59 | }
60 | val uploadRequest =
61 | QuickUploadRequest(this, serverUrl = "http://192.168.30.137:8080/upload")
62 | .setMethod("POST")
63 | .addFileToUpload(
64 | filePath = s.filePath,
65 | parameterName = "files"
66 | )
67 | s.quickUploadRequest =uploadRequest
68 | s.startUpload()
69 | }
70 |
71 | }
72 | findViewById(R.id.endOfUpload).setOnClickListener {
73 | UploadService.stopAllUploads()
74 | }
75 | findViewById(R.id.selectFile).setOnClickListener {
76 | openFilePicker()
77 | }
78 | }
79 |
80 | fun openFilePicker() {
81 | val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
82 | addCategory(Intent.CATEGORY_OPENABLE)
83 | type = "*/*"
84 | putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
85 | flags =
86 | (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
87 | }
88 | startActivityForResult(intent, READ_REQUEST_CODE)
89 | }
90 |
91 | override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
92 | super.onActivityResult(requestCode, resultCode, resultData)
93 | if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
94 | resultData?.let { intent ->
95 | val clipData = intent.clipData
96 | if (clipData != null) {
97 | // 多个文件被选中
98 | val file = arrayListOf()
99 | for (i in 0 until clipData.itemCount) {
100 | val uri = clipData.getItemAt(i).uri
101 | file.add(uri.toString())
102 | }
103 | onPickedFiles(file)
104 | } else {
105 | // 单个文件被选中
106 | val uri = intent.data
107 | uri?.let {
108 | // 处理 URI
109 | onPickedFiles(arrayListOf(it.toString()))
110 | }
111 | }
112 | }
113 | }
114 | }
115 |
116 | private fun onPickedFiles(path: MutableList) {
117 | filePath.clear()
118 | filePath.addAll(path)
119 | fileList.clear()
120 | filePath.forEach {
121 | val fileItem = FileItem(name(Uri.parse(it)), it, it)
122 | fileItem.refresh { _, _, _, _ ->
123 | val indexOf = fileList.indexOf(fileItem)
124 | uploadAdapter.notifyItemChanged(indexOf)
125 | }
126 | UploadService.addObserver(fileItem)
127 | fileList.add(fileItem)
128 | }
129 | uploadAdapter.notifyDataSetChanged()
130 | }
131 |
132 | fun name(uri: Uri): String {
133 | return contentResolver.query(uri, null, null, null, null)?.use {
134 | if (it.moveToFirst()) {
135 | val displayNameColumn = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
136 | if (displayNameColumn >= 0) it.getString(displayNameColumn) else null
137 | } else {
138 | null
139 | }
140 | } ?: uri.toString().split(File.separator).last()
141 | }
142 |
143 | override fun onDestroy() {
144 | super.onDestroy()
145 | UploadService.removeAllObserver()
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dh/updemo/SingleFileUploadActivity.kt:
--------------------------------------------------------------------------------
1 | package com.dh.updemo
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Intent
5 | import android.net.Uri
6 | import android.os.Bundle
7 | import android.provider.OpenableColumns
8 | import android.widget.Button
9 | import android.widget.ProgressBar
10 | import android.widget.TextView
11 | import android.widget.Toast
12 | import androidx.appcompat.app.AppCompatActivity
13 | import com.dh.quickupload.UploadService
14 | import com.dh.quickupload.data.UploadStatus
15 | import com.dh.quickupload.quick.QuickUploadRequest
16 | import java.io.File
17 |
18 | /**
19 | * 单文件上传
20 | */
21 | class SingleFileUploadActivity : AppCompatActivity() {
22 | private lateinit var progressBar: ProgressBar
23 | private lateinit var uploadStart: Button
24 | private lateinit var endOfUpload: Button
25 | private lateinit var selectFile: Button
26 | private lateinit var uploadProgress: TextView
27 | private lateinit var uploadAddress: TextView
28 |
29 | companion object {
30 | private const val READ_REQUEST_CODE = 4
31 | }
32 |
33 | private var fileItem: FileItem? = null
34 | override fun onCreate(savedInstanceState: Bundle?) {
35 | super.onCreate(savedInstanceState)
36 | setContentView(R.layout.single_file_upload_layout)
37 | progressBar = findViewById(R.id.progressBar)
38 | uploadStart = findViewById(R.id.uploadStart)
39 | endOfUpload = findViewById(R.id.endOfUpload)
40 | uploadProgress = findViewById(R.id.uploadProgress)
41 | uploadAddress = findViewById(R.id.uploadAddress)
42 | selectFile = findViewById(R.id.selectFile)
43 | uploadStart.setOnClickListener {
44 | if (fileItem == null) {
45 | Toast.makeText(this, "请选择文件", Toast.LENGTH_SHORT).show()
46 | } else {
47 | fileItem?.let {
48 | it.quickUploadRequest= QuickUploadRequest(this, serverUrl = "http://192.168.30.137:8080/upload")
49 | .setMethod("POST")
50 | .addFileToUpload(
51 | filePath = it.filePath,
52 | parameterName = "files"
53 | )
54 | .setResumedFileStart(0)
55 | it.startUpload()
56 | }
57 | }
58 |
59 | }
60 | endOfUpload.setOnClickListener {
61 | fileItem?.stopUpload()
62 | }
63 | selectFile.setOnClickListener {
64 | openFilePicker()
65 | }
66 | }
67 |
68 | fun openFilePicker() {
69 | val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
70 | addCategory(Intent.CATEGORY_OPENABLE)
71 | type = "*/*"
72 | flags =
73 | (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
74 | }
75 | startActivityForResult(intent, READ_REQUEST_CODE)
76 | }
77 |
78 | override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
79 | if (requestCode == READ_REQUEST_CODE && resultCode == RESULT_OK) {
80 | if (resultData != null) {
81 | val uri = resultData.data
82 | onPickedFiles(uri.toString())
83 | }
84 | } else {
85 | super.onActivityResult(requestCode, resultCode, resultData)
86 | }
87 | }
88 |
89 | @SuppressLint("SetTextI18n")
90 | private fun onPickedFiles(path: String) {
91 | fileItem = FileItem(name(Uri.parse(path)), path, path)
92 | fileItem?.refresh { uploadStatus, uploadInfo, throwable, serverResponse ->
93 | when (uploadStatus) {
94 | UploadStatus.DEFAULT -> {
95 |
96 | }
97 |
98 | UploadStatus.Wait -> {
99 |
100 | }
101 |
102 | UploadStatus.InProgress -> {
103 | progressBar.progress = uploadInfo.progressPercent
104 | uploadProgress.text = "已上传:${uploadInfo.progressPercent}%"
105 | }
106 |
107 | UploadStatus.Success -> {
108 | uploadProgress.text = "连接成功"
109 | }
110 |
111 | UploadStatus.Error -> {
112 | uploadProgress.text = throwable.toString()
113 | }
114 |
115 | UploadStatus.Completed -> {
116 | if (uploadInfo.progressPercent == 100) {
117 | uploadProgress.text = "上传完成"
118 | }
119 | }
120 |
121 | else -> {}
122 | }
123 | }
124 | UploadService.addObserver(fileItem!!)
125 | uploadAddress.text = "本地文件地址:$path"
126 |
127 | }
128 |
129 | fun name(uri: Uri): String {
130 | return contentResolver.query(uri, null, null, null, null)?.use {
131 | if (it.moveToFirst()) {
132 | val displayNameColumn = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
133 | if (displayNameColumn >= 0) it.getString(displayNameColumn) else null
134 | } else {
135 | null
136 | }
137 | } ?: uri.toString().split(File.separator).last()
138 | }
139 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/dh/updemo/UploadAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.dh.updemo
2 |
3 | import android.view.LayoutInflater
4 | import android.view.View
5 | import android.view.ViewGroup
6 | import android.widget.Button
7 | import android.widget.ProgressBar
8 | import android.widget.TextView
9 | import androidx.recyclerview.widget.RecyclerView
10 | import com.dh.quickupload.data.UploadStatus
11 |
12 | class UploadAdapter(
13 | private val fileList: List,
14 | private val onUploadButtonClick: (FileItem) -> Unit
15 | ) :
16 | RecyclerView.Adapter() {
17 |
18 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UploadViewHolder {
19 | val view = LayoutInflater.from(parent.context).inflate(R.layout.item_upload, parent, false)
20 | return UploadViewHolder(view)
21 | }
22 |
23 | override fun onBindViewHolder(holder: UploadViewHolder, position: Int) {
24 | holder.bind(fileList[position], position)
25 | }
26 | override fun getItemCount(): Int = fileList.size
27 | inner class UploadViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
28 | private val fileName: TextView = itemView.findViewById(R.id.file_name)
29 | private val progressBar: ProgressBar = itemView.findViewById(R.id.progress_bar)
30 | private val uploadButton: Button = itemView.findViewById(R.id.upload_button)
31 | fun bind(fileItem: FileItem, position: Int) {
32 | fileName.text = fileItem.fileName
33 | progressBar.progress = fileItem.uploadInfo.progressPercent
34 | when (fileItem.status) {
35 | UploadStatus.DEFAULT -> {
36 | uploadButton.text = "上传"
37 | }
38 |
39 | UploadStatus.Wait -> {
40 | uploadButton.text = "等待"
41 | }
42 |
43 | UploadStatus.InProgress -> {
44 | uploadButton.text = "取消"
45 | }
46 |
47 | UploadStatus.Error -> {
48 | uploadButton.text = "重新上传"
49 | }
50 |
51 | UploadStatus.Completed -> {
52 | if (fileItem.uploadInfo.progressPercent==100){
53 | uploadButton.text = "上传完成"
54 |
55 | }
56 | }
57 |
58 | else -> {
59 | }
60 | }
61 | uploadButton.setOnClickListener {
62 | onUploadButtonClick(fileItem)
63 | }
64 | }
65 | }
66 |
67 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/dh/updemo/UploadMultipleFilesSimultaneouslyActivity.kt:
--------------------------------------------------------------------------------
1 | package com.dh.updemo
2 |
3 | import android.annotation.SuppressLint
4 | import android.app.Activity
5 | import android.content.Intent
6 | import android.os.Bundle
7 | import android.widget.Button
8 | import android.widget.ProgressBar
9 | import android.widget.TextView
10 | import android.widget.Toast
11 | import androidx.appcompat.app.AppCompatActivity
12 | import com.dh.quickupload.UploadService
13 | import com.dh.quickupload.data.UploadStatus
14 | import com.dh.quickupload.quick.QuickUploadRequest
15 |
16 | /**
17 | * 同时上传多个文件活动
18 | * 简单来说就是多个文件在同一个上传请求中上传,它们具有相同的 UploadID
19 | * 无论多少个文件,上传请求只有一个
20 | * 注意:不支持断点续传
21 | * 此功能需要服务器支持单请求多文件同时上传
22 | */
23 | class UploadMultipleFilesSimultaneouslyActivity : AppCompatActivity() {
24 | private lateinit var progressBar: ProgressBar
25 | private lateinit var uploadStart: Button
26 | private lateinit var endOfUpload: Button
27 | private lateinit var selectFile: Button
28 | private lateinit var uploadProgress: TextView
29 | private lateinit var uploadAddress: TextView
30 |
31 | companion object {
32 | private const val READ_REQUEST_CODE = 5
33 | }
34 |
35 | private var filesItem: FilesItem? = null
36 | override fun onCreate(savedInstanceState: Bundle?) {
37 | super.onCreate(savedInstanceState)
38 | setContentView(R.layout.upload_multiple_files_simultaneously_layout)
39 | progressBar = findViewById(R.id.progressBar)
40 | uploadStart = findViewById(R.id.uploadStart)
41 | endOfUpload = findViewById(R.id.endOfUpload)
42 | uploadProgress = findViewById(R.id.uploadProgress)
43 | uploadAddress = findViewById(R.id.uploadAddress)
44 | selectFile = findViewById(R.id.selectFile)
45 | uploadStart.setOnClickListener {
46 | if (filesItem == null) {
47 | Toast.makeText(this, "请选择文件", Toast.LENGTH_SHORT).show()
48 | } else {
49 | filesItem?.let {
50 | it.quickUploadRequest= QuickUploadRequest(this, serverUrl = "http://192.168.30.137:8080/upload")
51 | .setMethod("POST")
52 | .apply {
53 | it.filePath.forEachIndexed { _, s ->
54 | addFileToUpload(
55 | filePath = s,
56 | parameterName = "files"
57 | )
58 | }
59 | }
60 | it.startUpload()
61 | }
62 |
63 | }
64 | }
65 | endOfUpload.setOnClickListener {
66 | filesItem?.stopUpload()
67 | }
68 | selectFile.setOnClickListener {
69 | openFilePicker()
70 | }
71 | }
72 |
73 | fun openFilePicker() {
74 | val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
75 | addCategory(Intent.CATEGORY_OPENABLE)
76 | type = "*/*"
77 | putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
78 | flags =
79 | (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
80 | }
81 | startActivityForResult(intent, READ_REQUEST_CODE)
82 | }
83 |
84 | @Deprecated("Deprecated in Java")
85 | override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
86 | super.onActivityResult(requestCode, resultCode, resultData)
87 | if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
88 | resultData?.let { intent ->
89 | val clipData = intent.clipData
90 | if (clipData != null) {
91 | // 多个文件被选中
92 | val file = arrayListOf()
93 |
94 | for (i in 0 until clipData.itemCount) {
95 | val uri = clipData.getItemAt(i).uri
96 | file.add(uri.toString())
97 | }
98 | onPickedFiles(file)
99 | } else {
100 | // 单个文件被选中
101 | val uri = intent.data
102 | uri?.let {
103 | // 处理 URI
104 | onPickedFiles(arrayListOf(it.toString()))
105 | }
106 | }
107 | }
108 | }
109 | }
110 |
111 | @SuppressLint("SetTextI18n")
112 | private fun onPickedFiles(path: MutableList) {
113 | filesItem = FilesItem("多文件", path, "多文件")
114 | filesItem?.refresh { uploadStatus, uploadInfo, throwable, serverResponse ->
115 | when (uploadStatus) {
116 | UploadStatus.DEFAULT -> {
117 |
118 | }
119 |
120 | UploadStatus.Wait -> {
121 |
122 | }
123 |
124 | UploadStatus.InProgress -> {
125 | progressBar.progress = uploadInfo.progressPercent
126 | uploadProgress.text = "已上传:${uploadInfo.progressPercent.toString()}%"
127 | }
128 |
129 | UploadStatus.Success -> {
130 | uploadProgress.text = "连接成功"
131 | }
132 |
133 | UploadStatus.Error -> {
134 | uploadProgress.text = throwable.toString()
135 | }
136 |
137 | UploadStatus.Completed -> {
138 | if (uploadInfo.progressPercent == 100) {
139 | uploadProgress.text = "上传完成"
140 | }
141 | }
142 |
143 | else -> {}
144 | }
145 | }
146 | UploadService.addObserver(filesItem!!)
147 | uploadAddress.text = "本地文件地址:${path.joinToString()}"
148 | }
149 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
15 |
21 |
27 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_upload.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
15 |
16 |
22 |
23 |
33 |
34 |
35 |
42 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/multiple_single_files_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
15 |
20 |
21 |
27 |
28 |
35 |
36 |
43 |
44 |
45 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/single_file_upload_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
14 |
20 |
21 |
27 |
28 |
34 |
35 |
36 |
45 |
46 |
47 |
58 |
59 |
60 |
68 |
69 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/upload_multiple_files_simultaneously_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
15 |
20 |
21 |
27 |
28 |
34 |
35 |
41 |
42 |
43 |
52 |
53 |
54 |
65 |
66 |
67 |
75 |
76 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XJ-Up/quickupload/79d546f5ef6af3db984979c72d25366e4c22f02b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XJ-Up/quickupload/79d546f5ef6af3db984979c72d25366e4c22f02b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XJ-Up/quickupload/79d546f5ef6af3db984979c72d25366e4c22f02b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XJ-Up/quickupload/79d546f5ef6af3db984979c72d25366e4c22f02b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XJ-Up/quickupload/79d546f5ef6af3db984979c72d25366e4c22f02b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XJ-Up/quickupload/79d546f5ef6af3db984979c72d25366e4c22f02b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XJ-Up/quickupload/79d546f5ef6af3db984979c72d25366e4c22f02b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XJ-Up/quickupload/79d546f5ef6af3db984979c72d25366e4c22f02b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XJ-Up/quickupload/79d546f5ef6af3db984979c72d25366e4c22f02b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XJ-Up/quickupload/79d546f5ef6af3db984979c72d25366e4c22f02b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values-land/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 | 48dp
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/values-v23/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/values-w1240dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 | 200dp
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values-w600dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 | 48dp
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 | 16dp
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | UpDemo
3 | MainActivity
4 |
5 | First Fragment
6 | Second Fragment
7 | Next
8 | Previous
9 |
10 |
11 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam in scelerisque sem. Mauris
12 | volutpat, dolor id interdum ullamcorper, risus dolor egestas lectus, sit amet mattis purus
13 | dui nec risus. Maecenas non sodales nisi, vel dictum dolor. Class aptent taciti sociosqu ad
14 | litora torquent per conubia nostra, per inceptos himenaeos. Suspendisse blandit eleifend
15 | diam, vel rutrum tellus vulputate quis. Aliquam eget libero aliquet, imperdiet nisl a,
16 | ornare ex. Sed rhoncus est ut libero porta lobortis. Fusce in dictum tellus.\n\n
17 | Suspendisse interdum ornare ante. Aliquam nec cursus lorem. Morbi id magna felis. Vivamus
18 | egestas, est a condimentum egestas, turpis nisl iaculis ipsum, in dictum tellus dolor sed
19 | neque. Morbi tellus erat, dapibus ut sem a, iaculis tincidunt dui. Interdum et malesuada
20 | fames ac ante ipsum primis in faucibus. Curabitur et eros porttitor, ultricies urna vitae,
21 | molestie nibh. Phasellus at commodo eros, non aliquet metus. Sed maximus nisl nec dolor
22 | bibendum, vel congue leo egestas.\n\n
23 | Sed interdum tortor nibh, in sagittis risus mollis quis. Curabitur mi odio, condimentum sit
24 | amet auctor at, mollis non turpis. Nullam pretium libero vestibulum, finibus orci vel,
25 | molestie quam. Fusce blandit tincidunt nulla, quis sollicitudin libero facilisis et. Integer
26 | interdum nunc ligula, et fermentum metus hendrerit id. Vestibulum lectus felis, dictum at
27 | lacinia sit amet, tristique id quam. Cras eu consequat dui. Suspendisse sodales nunc ligula,
28 | in lobortis sem porta sed. Integer id ultrices magna, in luctus elit. Sed a pellentesque
29 | est.\n\n
30 | Aenean nunc velit, lacinia sed dolor sed, ultrices viverra nulla. Etiam a venenatis nibh.
31 | Morbi laoreet, tortor sed facilisis varius, nibh orci rhoncus nulla, id elementum leo dui
32 | non lorem. Nam mollis ipsum quis auctor varius. Quisque elementum eu libero sed commodo. In
33 | eros nisl, imperdiet vel imperdiet et, scelerisque a mauris. Pellentesque varius ex nunc,
34 | quis imperdiet eros placerat ac. Duis finibus orci et est auctor tincidunt. Sed non viverra
35 | ipsum. Nunc quis augue egestas, cursus lorem at, molestie sem. Morbi a consectetur ipsum, a
36 | placerat diam. Etiam vulputate dignissim convallis. Integer faucibus mauris sit amet finibus
37 | convallis.\n\n
38 | Phasellus in aliquet mi. Pellentesque habitant morbi tristique senectus et netus et
39 | malesuada fames ac turpis egestas. In volutpat arcu ut felis sagittis, in finibus massa
40 | gravida. Pellentesque id tellus orci. Integer dictum, lorem sed efficitur ullamcorper,
41 | libero justo consectetur ipsum, in mollis nisl ex sed nisl. Donec maximus ullamcorper
42 | sodales. Praesent bibendum rhoncus tellus nec feugiat. In a ornare nulla. Donec rhoncus
43 | libero vel nunc consequat, quis tincidunt nisl eleifend. Cras bibendum enim a justo luctus
44 | vestibulum. Fusce dictum libero quis erat maximus, vitae volutpat diam dignissim.
45 |
46 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/network_security_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/test/java/com/dh/updemo/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.dh.updemo
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | apply from: "config.gradle"
3 | dependencies {
4 | classpath 'com.android.tools.build:gradle:8.0.0'
5 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
6 | }
7 | }
8 |
9 | plugins {
10 | id 'com.android.library' version '8.0.0' apply false
11 | id 'org.jetbrains.kotlin.android' version '1.6.21' apply false
12 | }
--------------------------------------------------------------------------------
/config.gradle:
--------------------------------------------------------------------------------
1 | ext {
2 | kotlin_version = "1.6.20"
3 | minsdk_version = 21
4 | targetsdk_version = 34
5 | compilesdk_version = 34
6 | }
7 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XJ-Up/quickupload/79d546f5ef6af3db984979c72d25366e4c22f02b/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri May 10 15:10:46 CST 2024
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/jitpack.yml:
--------------------------------------------------------------------------------
1 | jdk:
2 | - openjdk17
--------------------------------------------------------------------------------
/pictureresources/one.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XJ-Up/quickupload/79d546f5ef6af3db984979c72d25366e4c22f02b/pictureresources/one.gif
--------------------------------------------------------------------------------
/pictureresources/three.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XJ-Up/quickupload/79d546f5ef6af3db984979c72d25366e4c22f02b/pictureresources/three.gif
--------------------------------------------------------------------------------
/pictureresources/two.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XJ-Up/quickupload/79d546f5ef6af3db984979c72d25366e4c22f02b/pictureresources/two.gif
--------------------------------------------------------------------------------
/quickupload/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/quickupload/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'org.jetbrains.kotlin.android'
4 | id 'kotlin-parcelize'
5 | id 'maven-publish'
6 | }
7 |
8 | android {
9 | namespace 'com.dh.quickupload'
10 | compileSdk compilesdk_version
11 |
12 | defaultConfig {
13 | minSdk minsdk_version
14 | targetSdk targetsdk_version
15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
16 | consumerProguardFiles "consumer-rules.pro"
17 |
18 | }
19 |
20 | buildTypes {
21 | release {
22 | minifyEnabled false
23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
24 | }
25 | }
26 | compileOptions {
27 | sourceCompatibility JavaVersion.VERSION_1_8
28 | targetCompatibility JavaVersion.VERSION_1_8
29 | }
30 | kotlinOptions {
31 | jvmTarget = '1.8'
32 | }
33 | buildFeatures{
34 | buildConfig=true
35 | }
36 | }
37 | afterEvaluate{
38 | publishing {
39 | publications {
40 | release(MavenPublication) {
41 | from components.release
42 | groupId = 'com.dh'
43 | artifactId = 'quickupload'
44 | version = '1.2.1'
45 | }
46 | }
47 | }
48 | }
49 | dependencies {
50 | testImplementation 'junit:junit:4.+'
51 | androidTestImplementation 'androidx.test.ext:junit:1.1.2'
52 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
53 | implementation 'androidx.appcompat:appcompat:1.4.0'
54 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
55 | // 协程
56 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2"
57 | api 'com.squareup.okhttp3:okhttp:4.10.0'
58 | }
--------------------------------------------------------------------------------
/quickupload/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/XJ-Up/quickupload/79d546f5ef6af3db984979c72d25366e4c22f02b/quickupload/consumer-rules.pro
--------------------------------------------------------------------------------
/quickupload/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/quickupload/src/androidTest/java/com/dh/quickupload/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.dh.quickupload.test", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/quickupload/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/GeneralUploadRequest.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload
2 |
3 | import android.content.Context
4 | import android.util.Base64
5 | import com.dh.quickupload.data.HttpUploadTaskParameters
6 | import com.dh.quickupload.data.NameValue
7 | import com.dh.quickupload.extensions.addHeader
8 | import com.dh.quickupload.extensions.isValidHttpUrl
9 |
10 | /**
11 | * 表示一般的HTTP上载请求。
12 | * 子类创建您自己的自定义HTTP上传请求。
13 | * @ param context应用程序上下文
14 | * @ param serverUrl处理请求的服务器端脚本的URL
15 | *
16 | */
17 | abstract class GeneralUploadRequest>(context: Context, serverUrl: String) :
18 | UploadRequest(context, serverUrl) {
19 |
20 | private val httpParams = HttpUploadTaskParameters()
21 | init {
22 | require(serverUrl.isValidHttpUrl()) { "Specify either http:// or https:// as protocol" }
23 | }
24 |
25 | override fun getAdditionalParameters() = httpParams.toPersistableData()
26 |
27 | /**
28 | * 向此上传请求添加标头。
29 | *
30 | * @ param headerName标头名称
31 | * @ param headervvalue标头值
32 | * @ return self实例
33 | */
34 | fun addHeader(headerName: String, headerValue: String): B {
35 | httpParams.requestHeaders.addHeader(headerName, headerValue)
36 | return self()
37 | }
38 |
39 | /**
40 | * 设置HTTP基本身份验证标头。
41 | * @ param用户名HTTP基本身份验证用户名
42 | * @ param密码HTTP基本身份验证密码
43 | * @ return self实例
44 | */
45 | fun setBasicAuth(username: String, password: String): B {
46 | val auth = Base64.encodeToString("$username:$password".toByteArray(), Base64.NO_WRAP)
47 | return addHeader("Authorization", "Basic $auth")
48 | }
49 |
50 | /**
51 | * 使用令牌设置HTTP承载身份验证。
52 | * @ param bearerToken持有者授权令牌
53 | * @ return self实例
54 | */
55 | fun setBearerAuth(bearerToken: String): B {
56 | return addHeader("Authorization", "Bearer $bearerToken")
57 | }
58 |
59 | /**
60 | * 为该上传请求添加一个参数。
61 | *
62 | * @ param paramName参数名
63 | * @ Paramparamvalue参数值
64 | * @ return self实例
65 | */
66 | open fun addParameter(paramName: String, paramValue: String): B {
67 | httpParams.requestParameters.add(NameValue(paramName, paramValue))
68 | return self()
69 | }
70 |
71 | /**
72 | * 将具有多个值的参数添加到此上传请求中。
73 | *
74 | * @ param paramName参数名
75 | * @ param数组值
76 | * @ return self实例
77 | */
78 | open fun addArrayParameter(paramName: String, vararg array: String): B {
79 | for (value in array) {
80 | httpParams.requestParameters.add(NameValue(paramName, value))
81 | }
82 | return self()
83 | }
84 |
85 | /**
86 | * 将具有多个值的参数添加到此上传请求中。
87 | *
88 | * @ param paramName参数名
89 | * @ param列表值
90 | * @ return self实例
91 | */
92 | open fun addArrayParameter(paramName: String, list: List): B {
93 | for (value in list) {
94 | httpParams.requestParameters.add(NameValue(paramName, value))
95 | }
96 | return self()
97 | }
98 |
99 | /**
100 | * 设置要使用的HTTP方法。默认情况下,它设置为POST。
101 | *
102 | * @ param方法使用新的HTTP方法
103 | * @ return self实例
104 | */
105 | fun setMethod(method: String): B {
106 | httpParams.method = method.uppercase()
107 | return self()
108 | }
109 |
110 | /**
111 | * 设置此上传请求是否使用固定长度流模式。
112 | * 默认设置为true。
113 | * 如果它使用固定长度流模式,则由返回的值
114 | * [GeneralUploadTask.getBodyLength] 将自动用于正确设置
115 | * 底层 [java.net.HttpURLConnection],否则将使用chunk流模式。
116 | * @ param fixedLength true使用固定长度流模式 (这是默认设置) 或
117 | * false使用分块流模式。
118 | * @ return self实例
119 | */
120 | fun setUsesFixedLengthStreamingMode(fixedLength: Boolean): B {
121 | httpParams.usesFixedLengthStreamingMode = fixedLength
122 | return self()
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/GeneralUploadTask.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload
2 |
3 | import android.annotation.SuppressLint
4 | import com.dh.quickupload.data.HttpUploadTaskParameters
5 | import com.dh.quickupload.tools.logger.Logger
6 | import com.dh.quickupload.network.BodyWriter
7 | import com.dh.quickupload.network.NetworkRequest
8 | import com.dh.quickupload.network.BaseNetwork
9 |
10 | /**
11 | * 通用上传任务。
12 | * 子类来创建您的自定义上传任务。
13 | */
14 | abstract class GeneralUploadTask : UploadTask(), NetworkRequest.RequestBodyDelegate,
15 | BodyWriter.OnStreamWriteListener {
16 |
17 | protected val httpParams by lazy {
18 | HttpUploadTaskParameters.createFromPersistableData(params.additionalParameters)
19 | }
20 |
21 | /**
22 | * 在子类中实现,以在进度通知中提供预期的上传。
23 | * @ return http请求正文的预期大小。
24 | * @ 抛出UnsupportedEncodingException
25 | */
26 | abstract val bodyLength: Long
27 |
28 | /**
29 | * 上传逻辑的实现。
30 | * 如果您想利用Android上传服务提供的自动化功能,
31 | * 不要在子类中覆盖或更改此方法的实现。如果你这么做了,
32 | * 您可以完全控制上传的方式,例如,您可以使用自定义
33 | * http堆栈,但你必须手动设置请求到服务器的一切你
34 | * 在你的 [GeneralUploadRequest] 子类中设置,并从服务器获取响应。
35 | *
36 | * @ 如果发生错误,则抛出异常
37 | */
38 | @SuppressLint("NewApi")
39 | @Throws(Exception::class)
40 | override fun upload(httpStack: BaseNetwork) {
41 | Logger.debug(javaClass.simpleName, params.id) { "正在启动上传任务" }
42 |
43 | setAllFilesHaveBeenSuccessfullyUploaded(false)
44 | totalBytes = bodyLength
45 |
46 | val response = httpStack.createRequest(params.id, httpParams.method, params.serverUrl)
47 | .setHeaders(httpParams.requestHeaders.map { it.validateAsHeader() })
48 | .setTotalBodyBytes(totalBytes, httpParams.usesFixedLengthStreamingMode)
49 | .getResponse(this, this)
50 |
51 | Logger.debug(javaClass.simpleName, params.id) {
52 | "服务器响应: code ${response.code},body ${response.bodyString}"
53 | }
54 |
55 | // 仅在用户未取消操作时完成广播。
56 | if (job.isActive) {
57 | if (response.isSuccessful) {
58 | setAllFilesHaveBeenSuccessfullyUploaded()
59 | }
60 | onResponseReceived(response)
61 | }
62 | }
63 |
64 | override fun shouldContinueWriting() = job.isActive
65 |
66 | final override fun onBytesWritten(bytesWritten: Int) {
67 | onProgress(bytesWritten.toLong())
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/UploadConfiguration.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload
2 |
3 | import android.app.Application
4 | import android.os.Build
5 | import com.dh.quickupload.data.RetryPolicyConfig
6 | import com.dh.quickupload.tools.logger.Logger
7 | import com.dh.quickupload.network.BaseNetwork
8 | import com.dh.quickupload.network.okhttp.OkHttpNetwork
9 | import com.dh.quickupload.observer.network.NetworkMonitor
10 | import com.dh.quickupload.tools.translationfile.ContentResolverSchemeHandler
11 | import com.dh.quickupload.tools.translationfile.FileSchemeHandler
12 | import com.dh.quickupload.tools.translationfile.SchemeHandler
13 | import kotlinx.coroutines.asCoroutineDispatcher
14 | import java.lang.reflect.InvocationTargetException
15 | import java.util.LinkedHashMap
16 | import java.util.concurrent.Executors
17 | import java.util.concurrent.Semaphore
18 | import java.util.concurrent.ThreadPoolExecutor
19 | import java.util.concurrent.TimeUnit
20 |
21 | object UploadConfiguration {
22 |
23 | private const val fileScheme = "/"
24 | private const val contentScheme = "content://"
25 |
26 | /**
27 | * 默认Http堆栈构造函数使用的默认用户代理。
28 | */
29 | const val defaultUserAgent = "AndroidUploadService/1.0.0"
30 |
31 | private val schemeHandlers by lazy {
32 | LinkedHashMap>().apply {
33 | this[fileScheme] = FileSchemeHandler::class.java
34 | this[contentScheme] = ContentResolverSchemeHandler::class.java
35 | }
36 | }
37 |
38 | /**
39 | * 使用命名空间和默认通知通道初始化上传服务。
40 | * 这必须在你的应用程序子类的onCreate方法之前做任何事情。
41 | * @ param context您的应用程序的上下文
42 | * @ param defaultNotificationChannel要使用的默认通知通道
43 | * @ param debug将其设置为您的BuildConfig.DEBUG
44 | */
45 | @JvmStatic
46 | fun initialize(context: Application, defaultNotificationChannel: String, debug: Boolean) {
47 | this.namespace = context.packageName
48 | this.defaultNotificationChannel = defaultNotificationChannel
49 | Logger.setDevelopmentMode(debug)
50 | }
51 |
52 | /**
53 | * 上传服务将要运行的命名空间。这必须在应用程序中设置
54 | * 子类的onCreate方法之前的任何东西。
55 | */
56 | @JvmStatic
57 | var namespace: String? = null
58 | private set
59 | get() = if (field == null)
60 | throw IllegalArgumentException("您必须在应用程序子类中将命名空间设置为您的应用程序包名称 (context.packageName)")
61 | else
62 | field
63 |
64 | /**
65 | * 要使用的默认通知通道。这必须在应用程序中设置
66 | * 子类的onCreate方法之前的任何东西。
67 | */
68 | @JvmStatic
69 | var defaultNotificationChannel: String? = null
70 | private set
71 | get() = if (field == null)
72 | throw IllegalArgumentException("您必须在应用程序子类中设置defaultNotificationChannel")
73 | else
74 | field
75 |
76 | /**
77 | * 创建自定义调度器
78 | */
79 | val dispatcher =
80 | Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()).apply {
81 | // 设置线程池的 keep-alive 时间
82 | (this as ThreadPoolExecutor).setKeepAliveTime(5, TimeUnit.SECONDS)
83 | allowCoreThreadTimeOut(true) // 允许核心线程超时
84 | }.asCoroutineDispatcher()
85 |
86 |
87 | /**
88 | * Semaphore 以限制最大并发任务数
89 | */
90 | @JvmStatic
91 | var maxConcurrentTasks = 2 // 设置最大并发任务数
92 | set(value) {
93 | require(value < 1) { "任务数必须大于1" }
94 | field = value
95 | }
96 | val semaphore = Semaphore(maxConcurrentTasks)
97 |
98 | /**
99 | * 创建网络状态监听
100 | * 当网络断开时立即停止全部上传
101 | */
102 | @JvmStatic
103 | var networkListening: (Application) -> NetworkMonitor = {
104 | NetworkMonitor(it)
105 | }
106 | /**
107 | * 在关闭服务之前要等待多少空闲时间。
108 | * 服务在运行时处于空闲状态,但没有任务正在运行。
109 | */
110 | @JvmStatic
111 | var idleTimeoutSeconds = 10
112 | set(value) {
113 | require(value >= 1) { "空闲超时最小允许值为1。不能是 $value" }
114 | field = value
115 | }
116 | /**
117 | * 上传任务用于数据传输的缓冲区大小 (以字节为单位)。
118 | */
119 | @JvmStatic
120 | var bufferSizeBytes = 4096
121 | set(value) {
122 | require(value >= 256) { "您不能将缓冲区大小设置为低于256字节" }
123 | field = value
124 | }
125 | /**
126 | * 配置上传网络
127 | */
128 | @JvmStatic
129 | var network: BaseNetwork = OkHttpNetwork()
130 |
131 | /**
132 | * 以毫秒为单位的进度通知之间的间隔。
133 | * 如果上传任务报告的频率超过此值,则上传服务将自动应用限制。
134 | * 默认为每秒3次更新
135 | */
136 | @JvmStatic
137 | var uploadProgressNotificationIntervalMillis: Long = 1000 / 3
138 |
139 | /**
140 | * 设置上传服务重试策略。有关详细信息,请参阅 [RetryPolicyConfig] 文档
141 | * 每个参数的解释。
142 | */
143 | @JvmStatic
144 | var retryPolicy = RetryPolicyConfig(
145 | initialWaitTimeSeconds = 1,
146 | maxWaitTimeSeconds = 100,
147 | multiplier = 2,
148 | defaultMaxRetries = 3
149 | )
150 |
151 | /**
152 | * 如果设置为true,服务将在做上传时在前台模式,
153 | * 降低被低内存系统杀死的概率。
154 | * 仅当您的上传具有通知配置时才使用此设置。
155 | * 这是不可能在没有通知的情况下在前台运行,根据Android政策
156 | * 约束,所以如果你设置为true,但你上传任务没有
157 | * 通知配置,该服务将简单地在后台模式下运行。
158 | *
159 | * 注意: 从Android Oreo (API 26) 开始,此设置被忽略,因为它始终必须为true,
160 | * 因为服务必须在前台运行并向用户公开通知。
161 | */
162 | @JvmStatic
163 | var isForegroundService = true
164 | get() = Build.VERSION.SDK_INT >= 26 || field
165 |
166 |
167 |
168 | /**
169 | * 注册一个自定义方案处理程序。
170 | * 您不能覆盖现有的文件和内容: // 方案。
171 | * @ param scheme要支持的方案 (例如content://,yourCustomScheme://)
172 | * @ param处理程序方案处理程序实现
173 | */
174 | @JvmStatic
175 | fun addSchemeHandler(scheme: String, handler: Class) {
176 | require(!(scheme == fileScheme || scheme == contentScheme)) { "无法覆盖: $scheme!" }
177 | schemeHandlers[scheme] = handler
178 | }
179 |
180 | @Throws(
181 | NoSuchMethodException::class,
182 | IllegalAccessException::class,
183 | InvocationTargetException::class,
184 | InstantiationException::class
185 | )
186 | @JvmStatic
187 | fun getSchemeHandler(path: String): SchemeHandler {
188 | val trimmedPath = path.trim()
189 |
190 | for ((scheme, handler) in schemeHandlers) {
191 | if (trimmedPath.startsWith(scheme, ignoreCase = true)) {
192 | return handler.newInstance().apply {
193 | init(trimmedPath)
194 | }
195 | }
196 | }
197 |
198 | throw UnsupportedOperationException("$ path不支持的方案。当前支持的方案为 ${schemeHandlers.keys}")
199 | }
200 |
201 | }
202 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/UploadRequest.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload
2 |
3 | import android.content.Context
4 | import com.dh.quickupload.data.UploadFile
5 | import com.dh.quickupload.data.UploadTaskParameters
6 | import com.dh.quickupload.extensions.startNewUpload
7 | import com.dh.quickupload.tools.datapreservation.Persistable
8 | import com.dh.quickupload.tools.datapreservation.PersistableData
9 | import java.util.UUID
10 | import kotlin.collections.ArrayList
11 |
12 | /**
13 | * 要扩展以创建上传请求的基类。如果您正在实现基于HTTP的上传,
14 | * 扩展 [GeneralUploadRequest] 代替。
15 | */
16 | abstract class UploadRequest>
17 | /**
18 | * 创建一个新的上传请求。
19 | *
20 | * @ param context应用程序上下文
21 | * @ param serverUrl处理请求的服务器端脚本的URL
22 | * @ 如果一个或多个参数无效,则抛出IllegalArgumentException
23 | */
24 | @Throws(IllegalArgumentException::class)
25 | constructor(protected val context: Context, private var serverUrl: String) : Persistable {
26 |
27 | private var uploadId = UUID.randomUUID().toString()
28 | private var started: Boolean = false
29 | private var maxRetries = UploadConfiguration.retryPolicy.defaultMaxRetries
30 | private var autoDeleteSuccessfullyUploadedFiles = false
31 | protected val files = ArrayList()
32 | private var resumedFileStart :Long= 0
33 |
34 | /**
35 | * 在子类中实现以指定将处理上传任务的类。
36 | * 类必须是 [UploadTask] 的子类。
37 | * @ return类
38 | */
39 | protected abstract val taskClass: Class
40 |
41 | init {
42 | require(serverUrl.isNotBlank()) { "服务器URL不能为空" }
43 | }
44 |
45 | private val uploadTaskParameters: UploadTaskParameters
46 | get() = UploadTaskParameters(
47 | taskClass = taskClass.name,
48 | id = uploadId,
49 | serverUrl = serverUrl,
50 | maxRetries = maxRetries,
51 | autoDeleteSuccessfullyUploadedFiles = autoDeleteSuccessfullyUploadedFiles,
52 | files = files,
53 | resumedFileStart=resumedFileStart,
54 | additionalParameters = getAdditionalParameters()
55 |
56 | )
57 |
58 | /**
59 | * 启动后台文件上传服务。
60 | * @ 返回uploadId字符串。如果您在构造函数中传递了自己的uploadId,则此
61 | * 方法将返回相同的uploadId,否则它将自动返回
62 | * 生成的uploadId
63 | */
64 | open fun startUpload(): String {
65 | check(!started) {
66 | "您已经在此上传请求实例上调用了一次startUpload(),并且您不能多次调用它。请检查您的代码。"
67 | }
68 | check(!UploadService.taskList.contains(uploadTaskParameters.id)) {
69 | "您已尝试使用相同的uploadID执行startUpload()已在运行任务。您正在尝试对多个上载使用相同的ID。"
70 | }
71 | started = true
72 | return context.startNewUpload(
73 | params = uploadTaskParameters
74 | )
75 | }
76 |
77 | protected abstract fun getAdditionalParameters(): PersistableData
78 |
79 | @Suppress("UNCHECKED_CAST")
80 | protected fun self(): B {
81 | return this as B
82 | }
83 |
84 |
85 | /**
86 | * 设置上传成功后自动删除文件。
87 | * @ param autoDeleteFiles为true以自动删除包含在
88 | * 请求时,上传成功完成。
89 | * 默认情况下,此设置设置为false,并且不会删除任何内容。
90 | * @ return self实例
91 | */
92 | fun setAutoDeleteFilesAfterSuccessfulUpload(autoDeleteFiles: Boolean): B {
93 | this.autoDeleteSuccessfullyUploadedFiles = autoDeleteFiles
94 | return self()
95 | }
96 |
97 | /**
98 | * 设置发生错误时库将尝试的最大重试次数,
99 | * 在返回错误之前。
100 | *
101 | * @ param maxRetries发生错误时的最大重试次数
102 | * @ return self实例
103 | */
104 | fun setMaxRetries(maxRetries: Int): B {
105 | this.maxRetries = maxRetries
106 | return self()
107 | }
108 |
109 | /**
110 | * 设置上传ID。
111 | *
112 | * @ param uploadID要分配给此上传请求的唯一ID。
113 | * 如果为null或空,将自动生成随机UUID。
114 | * 它在接收更新时用于广播接收器。
115 | */
116 | fun setUploadID(uploadID: String): B {
117 | this.uploadId = uploadID
118 | return self()
119 | }
120 | /**
121 | * 设置断点续传开始的地方
122 | */
123 | fun setResumedFileStart(index: Long): B {
124 | this.resumedFileStart = index
125 | return self()
126 | }
127 | /**
128 | * 获取表示此上传请求的 [PersistableData] 对象。
129 | * @ return [PersistableData] 表示此上传的对象
130 | */
131 | override fun toPersistableData() = uploadTaskParameters.toPersistableData()
132 | }
133 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/UploadService.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload
2 |
3 | import android.app.Notification
4 | import android.app.Service
5 | import android.content.Context
6 | import android.content.Intent
7 | import android.os.Build
8 | import android.os.IBinder
9 | import android.os.PowerManager
10 | import androidx.core.app.NotificationCompat
11 | import com.dh.quickupload.extensions.acquirePartialWakeLock
12 | import com.dh.quickupload.extensions.getUploadTask
13 | import com.dh.quickupload.extensions.getUploadTaskCreationParameters
14 | import com.dh.quickupload.extensions.safeRelease
15 | import com.dh.quickupload.tools.logger.Logger
16 | import com.dh.quickupload.observer.task.TaskCompletionNotifier
17 | import com.dh.quickupload.observer.task.UploadObserverBase
18 | import com.dh.quickupload.observer.task.UploadTaskObserver
19 | import kotlinx.coroutines.CoroutineScope
20 | import kotlinx.coroutines.MainScope
21 | import java.util.Timer
22 | import java.util.TimerTask
23 | import java.util.concurrent.ConcurrentHashMap
24 |
25 | class UploadService : Service(), CoroutineScope by MainScope() {
26 |
27 | companion object {
28 | internal val TAG = UploadService::class.java.simpleName
29 |
30 | private const val UPLOAD_NOTIFICATION_BASE_ID = 10086
31 |
32 |
33 | private val uploadTasksMap = ConcurrentHashMap()
34 |
35 | @Volatile
36 | private var foregroundUploadId: String? = null
37 |
38 |
39 |
40 | /**
41 | * 使用给定的uploadId停止上传任务。
42 | * @ param uploadId唯一的上传id
43 | */
44 | @Synchronized
45 | @JvmStatic
46 | fun stopUpload(uploadId: String) {
47 | uploadTasksMap[uploadId]?.cancel()
48 | }
49 |
50 | /**
51 | * 获取当前活动的上传任务的列表。
52 | * @ 如果当前没有任务正在运行,则返回uploadIDs列表或空列表
53 | */
54 | @JvmStatic
55 | val taskList: List
56 | @Synchronized get() = if (uploadTasksMap.isEmpty()) {
57 | emptyList()
58 | } else {
59 | uploadTasksMap.keys().toList()
60 | }
61 | /**
62 | * 保存上传观察者对象集合,通过继承 UploadObserverBase 添加到observers即可
63 | */
64 | @JvmStatic
65 | private val observers:MutableList = mutableListOf()
66 | @JvmStatic
67 | fun removeAllObserver() {
68 | observers.clear()
69 | }
70 | @JvmStatic
71 | fun addObserver(observer: UploadObserverBase) {
72 | val existingObserverIndex = observers.indexOfFirst { it.uploadId == observer.uploadId }
73 |
74 | if (existingObserverIndex != -1) {
75 | // 如果找到具有相同id的对象,进行更新
76 | observers[existingObserverIndex] = observer
77 | } else {
78 | // 如果没有找到相同id的对象,添加新对象
79 | observers.add(observer)
80 | }
81 | }
82 |
83 | /**
84 | * 停止所有活动的上传。
85 | */
86 | @Synchronized
87 | @JvmStatic
88 | fun stopAllUploads() {
89 | val iterator = uploadTasksMap.keys.iterator()
90 |
91 | while (iterator.hasNext()) {
92 | uploadTasksMap[iterator.next()]?.cancel()
93 | }
94 | }
95 |
96 | @Synchronized
97 | @JvmStatic
98 | fun noNetworkStopAllUploads() {
99 | val iterator = uploadTasksMap.keys.iterator()
100 |
101 | while (iterator.hasNext()) {
102 | val uploadTask = uploadTasksMap[iterator.next()]
103 | uploadTask?.noNetwork = true
104 | uploadTask?.cancel()
105 | }
106 | }
107 |
108 | /**
109 | * 停止服务。
110 | * @ param context应用程序上下文
111 | * @ param forceStop如果为true,则无论某些任务是否正在运行,都会停止服务,否则
112 | * 停止只有当没有任何活动的任务
113 | * @ return如果服务停止,则返回true,否则返回false
114 | */
115 | @Synchronized
116 | @JvmOverloads
117 | @JvmStatic
118 | fun stop(context: Context, forceStop: Boolean = false) = if (forceStop) {
119 | stopAllUploads()
120 | context.stopService(Intent(context, UploadService::class.java))
121 |
122 | } else {
123 | uploadTasksMap.isEmpty() && context.stopService(
124 | Intent(
125 | context,
126 | UploadService::class.java
127 | )
128 | )
129 | }
130 |
131 | }
132 | private var wakeLock: PowerManager.WakeLock? = null
133 | private var idleTimer: Timer? = null
134 | private val taskObservers by lazy {
135 | arrayOf(
136 | TaskCompletionNotifier(this)
137 | )
138 | }
139 | private val networkListening by lazy {
140 | UploadConfiguration.networkListening(application)
141 | }
142 |
143 | @Synchronized
144 | private fun clearIdleTimer() {
145 | idleTimer?.apply {
146 | Logger.info(TAG, Logger.NA) { "清除空闲计时器" }
147 | cancel()
148 | }
149 | idleTimer = null
150 | }
151 |
152 | @Synchronized
153 | private fun shutdownIfThereArentAnyActiveTasks(): Int {
154 | if (uploadTasksMap.isEmpty()) {
155 | clearIdleTimer()
156 |
157 | Logger.info(TAG, Logger.NA) {
158 | "服务将在 ${UploadConfiguration.idleTimeoutSeconds} 秒内关闭如果没有收到新任务"
159 | }
160 |
161 | idleTimer = Timer(TAG + "IdleTimer").apply {
162 | schedule(object : TimerTask() {
163 | override fun run() {
164 | Logger.info(TAG, Logger.NA) {
165 | "服务即将停止,因为空闲超时为已达到 ${UploadConfiguration.idleTimeoutSeconds}s"
166 | }
167 | stopSelf()
168 | }
169 | }, (UploadConfiguration.idleTimeoutSeconds * 1000).toLong())
170 | }
171 |
172 | return START_NOT_STICKY
173 | }
174 |
175 | return START_STICKY
176 | }
177 |
178 | /**
179 | * 检查任务当前是否为前台通知中显示的任务。
180 | * @ param上传的ID
181 | * @ 如果当前上传任务持有前台通知,则返回true,否则为false
182 | */
183 | @Synchronized
184 | fun holdForegroundNotification(uploadId: String, notification: Notification): Boolean {
185 | if (!UploadConfiguration.isForegroundService) return false
186 |
187 | if (foregroundUploadId == null) {
188 | foregroundUploadId = uploadId
189 | Logger.debug(TAG, uploadId) { "现在保留前台通知" }
190 | }
191 |
192 | if (uploadId == foregroundUploadId) {
193 | startForeground(UPLOAD_NOTIFICATION_BASE_ID, notification)
194 | return true
195 | }
196 |
197 | return false
198 | }
199 |
200 | private fun stopServiceForeground() {
201 | if (Build.VERSION.SDK_INT >= 24) {
202 | stopForeground(STOP_FOREGROUND_REMOVE)
203 | } else {
204 | @Suppress("DEPRECATION")
205 | stopForeground(true)
206 | }
207 | }
208 |
209 | /**
210 | * 由每个任务完成时调用 (成功,错误或由于
211 | * 用户取消)。
212 | * @ param uploadId完成任务的uploadID
213 | */
214 | @Synchronized
215 | fun taskCompleted(uploadId: String) {
216 | val task = uploadTasksMap.remove(uploadId)
217 |
218 | // un-hold foreground upload ID if it's been hold
219 | if (UploadConfiguration.isForegroundService && task != null && task.params.id == foregroundUploadId) {
220 | Logger.debug(TAG, uploadId) { "现在未保留的前台通知" }
221 | foregroundUploadId = null
222 | }
223 |
224 | if (UploadConfiguration.isForegroundService && uploadTasksMap.isEmpty()) {
225 | Logger.debug(
226 | TAG,
227 | Logger.NA
228 | ) { "所有任务已完成,停止前台执行" }
229 | stopServiceForeground()
230 | shutdownIfThereArentAnyActiveTasks()
231 | }
232 | }
233 | private fun getObserverById(id: String): Array {
234 | val observer = observers.find { it.uploadId == id }
235 | return if (observer != null) {
236 | arrayOf(observer)
237 | } else {
238 | emptyArray()
239 | }
240 | }
241 | override fun onCreate() {
242 | super.onCreate()
243 |
244 | wakeLock = acquirePartialWakeLock(wakeLock, TAG)
245 | networkListening.register()
246 | }
247 |
248 | override fun onBind(intent: Intent): IBinder? {
249 | return null
250 | }
251 |
252 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
253 | Logger.debug(TAG, Logger.NA) {
254 | "正在启动UploadService。调试信息: $UploadConfiguration"
255 | }
256 |
257 | val builder =
258 | NotificationCompat.Builder(this, UploadConfiguration.defaultNotificationChannel!!)
259 | .setSmallIcon(android.R.drawable.ic_menu_upload)
260 | .setOngoing(true)
261 | .setGroup(UploadConfiguration.namespace)
262 |
263 | if (Build.VERSION.SDK_INT >= 31) {
264 | builder.foregroundServiceBehavior = Notification.FOREGROUND_SERVICE_IMMEDIATE
265 | }
266 |
267 | val notification = builder.build()
268 |
269 | startForeground(UPLOAD_NOTIFICATION_BASE_ID, notification)
270 |
271 | val taskCreationParameters = intent.getUploadTaskCreationParameters()
272 | ?: return shutdownIfThereArentAnyActiveTasks()
273 |
274 | if (uploadTasksMap.containsKey(taskCreationParameters.params.id)) {
275 | Logger.error(TAG, taskCreationParameters.params.id) {
276 | "防止上传!具有相同ID的上载已在进行中。每次上传都必须有唯一的ID。请检查您的代码!"
277 | }
278 | return shutdownIfThereArentAnyActiveTasks()
279 | }
280 |
281 | val currentTask = getUploadTask(
282 | creationParameters = taskCreationParameters,
283 | scope = this,
284 | observers = getObserverById(taskCreationParameters.params.id)+taskObservers
285 | ) ?: return shutdownIfThereArentAnyActiveTasks()
286 |
287 | clearIdleTimer()
288 | uploadTasksMap[currentTask.params.id] = currentTask
289 | currentTask.start()
290 | return START_STICKY
291 | }
292 |
293 | override fun onDestroy() {
294 | super.onDestroy()
295 | networkListening.unregister()
296 | stopAllUploads()
297 |
298 | if (UploadConfiguration.isForegroundService) {
299 | Logger.debug(
300 | TAG,
301 | Logger.NA
302 | ) { "停止前台执行" }
303 | stopServiceForeground()
304 | }
305 |
306 | wakeLock.safeRelease()
307 |
308 | uploadTasksMap.clear()
309 | observers.clear()
310 | Logger.debug(TAG, Logger.NA) { "UploadService已销毁" }
311 | }
312 | }
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/UploadTask.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload
2 |
3 | import android.content.Context
4 | import com.dh.quickupload.UploadConfiguration.semaphore
5 | import com.dh.quickupload.data.UploadFile
6 | import com.dh.quickupload.data.UploadInfo
7 | import com.dh.quickupload.data.UploadTaskParameters
8 | import com.dh.quickupload.exceptions.NoNetworkException
9 | import com.dh.quickupload.exceptions.UploadError
10 | import com.dh.quickupload.exceptions.UserCancelledUploadException
11 | import com.dh.quickupload.tools.logger.Logger
12 | import com.dh.quickupload.network.BaseNetwork
13 | import com.dh.quickupload.network.ServerResponse
14 | import com.dh.quickupload.observer.task.UploadTaskObserver
15 | import kotlinx.coroutines.*
16 | import java.io.IOException
17 | import java.util.ArrayList
18 | import java.util.Date
19 |
20 | abstract class UploadTask {
21 | companion object {
22 | private val TAG = UploadTask::class.java.simpleName
23 | }
24 |
25 | private var lastProgressNotificationTime: Long = 0
26 |
27 | protected lateinit var context: Context
28 | private lateinit var scope: CoroutineScope
29 | lateinit var params: UploadTaskParameters
30 |
31 | private val observers = ArrayList()
32 |
33 | /**
34 | * 要传输的总字节数。您应该在
35 | * [UploadTask.upload] 子类的方法,在开始上传数据之前
36 | * 转让。
37 | */
38 | var totalBytes: Long = 0
39 |
40 | /**
41 | * 总传输字节。上传时,您应该在子类中更新此值
42 | * 一些数据,并在调用 [UploadTask.onProgress] 之前
43 | */
44 | private var uploadedBytes: Long = 0
45 | lateinit var job: Job
46 |
47 | /**
48 | * 本次上传任务的开始时间戳。
49 | */
50 | private val startTime: Long = Date().time
51 |
52 | /**
53 | * 已进行的上传尝试的计数器;
54 | */
55 | private var attempts: Int = 0
56 |
57 | var noNetwork: Boolean = false
58 | private var errorDelay = UploadConfiguration.retryPolicy.initialWaitTimeSeconds.toLong()
59 |
60 | private val uploadInfo: UploadInfo
61 | get() = UploadInfo(
62 | uploadId = params.id,
63 | startTime = startTime,
64 | uploadedBytes = uploadedBytes,
65 | totalBytes = totalBytes,
66 | numberOfRetries = attempts,
67 | files = params.files
68 | )
69 |
70 | /**
71 | * 上传逻辑的实现。
72 | *
73 | * @ 如果发生错误,则抛出异常
74 | */
75 | @Throws(Exception::class)
76 | protected abstract fun upload(httpStack: BaseNetwork)
77 |
78 | private inline fun theObserver(action: UploadTaskObserver.() -> Unit) {
79 | observers.forEach {
80 | try {
81 | action(it)
82 | } catch (exc: Throwable) {
83 | Logger.error(TAG, params.id, exc) {
84 | "将事件调度到观察者时出错"
85 | }
86 | }
87 | }
88 | }
89 | suspend fun withSemaphore(action: suspend () -> T): T {
90 | semaphore.acquire() // 获取许可,减少可用许可数
91 | return try {
92 | action()
93 | } finally {
94 | semaphore.release() // 释放许可,增加可用许可数
95 | }
96 | }
97 |
98 | /**
99 | * 初始化 [UploadTask]
100 | * 在子类中覆盖此方法以执行自定义任务初始化并获取
101 | * 在 [UploadRequest.initializeIntent] 方法中设置的自定义参数。
102 | * @ Param上下文上传服务实例。您应该使用此引用作为您的上下文。
103 | * @ Param intent intent发送到上下文开始上传
104 | * @ 如果在初始化时发生I/O异常,则抛出IOException
105 | */
106 | @Throws(IOException::class)
107 | fun init(
108 | context: Context,
109 | taskParams: UploadTaskParameters,
110 | scope: CoroutineScope,
111 | vararg taskObservers: UploadTaskObserver
112 | ) {
113 | this.context = context
114 | this.params = taskParams
115 | this.scope = scope
116 | taskObservers.forEach { observers.add(it) }
117 | performInitialization()
118 | }
119 |
120 | open fun performInitialization() {}
121 |
122 | private fun resetAttempts() {
123 | attempts = 0
124 | errorDelay = UploadConfiguration.retryPolicy.initialWaitTimeSeconds.toLong()
125 | }
126 |
127 | fun start() {
128 | job = scope.launch(UploadConfiguration.dispatcher) {
129 | theObserver {
130 | launch(Dispatchers.Main) {
131 | onWait(
132 | uploadInfo
133 | )
134 | }
135 | }
136 | withSemaphore {
137 | try {
138 |
139 | resetAttempts()
140 | while (attempts <= params.maxRetries && isActive) {
141 | try {
142 | resetUploadedBytes()
143 | upload(UploadConfiguration.network)
144 | break
145 | } catch (exc: Throwable) {
146 | if (attempts >= params.maxRetries) {
147 | onError(exc)
148 | } else {
149 | Logger.error(
150 | TAG,
151 | params.id,
152 | exc
153 | ) { "尝试 ${attempts + 1} 时出错。在下一次尝试之前正在等待 ${errorDelay}s。" }
154 |
155 | val sleepDeadline = System.currentTimeMillis() + errorDelay * 1000
156 |
157 | sleepWhile { System.currentTimeMillis() < sleepDeadline }
158 |
159 | errorDelay *= UploadConfiguration.retryPolicy.multiplier.toLong()
160 |
161 | if (errorDelay > UploadConfiguration.retryPolicy.maxWaitTimeSeconds) {
162 | errorDelay =
163 | UploadConfiguration.retryPolicy.maxWaitTimeSeconds.toLong()
164 | }
165 | }
166 | }
167 | attempts++
168 | }
169 | } finally {
170 | if (!job.isActive) {
171 | onUserCancelledUpload()
172 | }
173 | }
174 | }
175 | }
176 |
177 |
178 | }
179 |
180 | private inline fun sleepWhile(millis: Long = 1000, condition: () -> Boolean) {
181 | while (condition()) {
182 | try {
183 | Thread.sleep(millis)
184 | } catch (_: Throwable) {
185 | }
186 | }
187 | }
188 |
189 | protected fun resetUploadedBytes() {
190 | uploadedBytes = 0
191 | }
192 |
193 | /**
194 | * 广播进度更新。
195 | *
196 | * @ param uploadedBytes已经上传到服务器的字节数
197 | * @ param totalBytes请求的总字节数
198 | */
199 | protected fun onProgress(bytesSent: Long) {
200 | uploadedBytes += bytesSent
201 | if (shouldThrottle(uploadedBytes, totalBytes)) return
202 | Logger.debug(
203 | TAG,
204 | params.id
205 | ) { "已上传 ${uploadedBytes * 100 / totalBytes}%, $uploadedBytes of $totalBytes 字节" }
206 | theObserver {
207 | scope. launch(Dispatchers.Main) {
208 | onProgress(uploadInfo)
209 | }
210 | }
211 | }
212 |
213 | /**
214 | * 广播完成状态更新,并通知 [UploadService] 该任务
215 | * 执行成功。
216 | * 当任务完成上传请求并收到响应时调用此
217 | * 从服务器。
218 | *
219 | * @ param响应从服务器得到的响应
220 | */
221 | protected fun onResponseReceived(response: ServerResponse) {
222 | Logger.debug(
223 | TAG,
224 | params.id
225 | ) { "上传 ${if (response.isSuccessful) "完成" else "错误"}" }
226 | if (response.isSuccessful) {
227 | if (params.autoDeleteSuccessfullyUploadedFiles) {
228 | for (file in successfullyUploadedFiles) {
229 | if (file.handler.delete(context)) {
230 | Logger.info(
231 | TAG,
232 | params.id
233 | ) { "成功删除: ${file.path}" }
234 | } else {
235 | Logger.error(
236 | TAG,
237 | params.id
238 | ) { "删除时出错: ${file.path}" }
239 | }
240 | }
241 | }
242 |
243 | theObserver {
244 | scope.launch(Dispatchers.Main) {
245 | onSuccess(
246 | uploadInfo,
247 | response
248 | )
249 | }
250 |
251 | }
252 | } else {
253 | theObserver {
254 | scope.launch(Dispatchers.Main) {
255 | onError(
256 | uploadInfo,
257 | UploadError(response)
258 | )
259 | }
260 |
261 | }
262 | }
263 |
264 | theObserver {
265 | scope.launch(Dispatchers.Main) {
266 | onCompleted(uploadInfo)
267 | }}
268 | }
269 |
270 | /**
271 | * 广播已取消状态。
272 | * 当用户取消请求时,[UploadTask] 自动调用,
273 | * 和 [UploadTask.upload] 的具体实现
274 | * 返回或抛出异常。你不应该在你的显式调用这个方法
275 | * 实施。
276 | */
277 | private fun onUserCancelledUpload() {
278 | Logger.debug(TAG, params.id) { "上传已取消" }
279 | if (noNetwork) {
280 |
281 | onError(NoNetworkException())
282 | } else {
283 | onError(UserCancelledUploadException())
284 | }
285 |
286 | }
287 |
288 | /**
289 | * 广播错误。
290 | * 具体实现时由 [UploadTask] 自动调用
291 | * [UploadTask.upload] 抛出异常,没有任何剩余的重试。
292 | * 您不应该在实现中显式调用此方法。
293 | * @ param异常广播异常。是具体实现抛出的那个[UploadTask.upload] 的
294 | */
295 | private fun onError(exception: Throwable) {
296 | Logger.error(TAG, params.id, exception) { "错误" }
297 | uploadInfo.let {
298 | theObserver {
299 | scope.launch(Dispatchers.Main) {
300 | onError(it, exception) }
301 | }
302 |
303 | theObserver {
304 | scope.launch(Dispatchers.Main) {
305 | onCompleted(it)
306 | }
307 | }
308 | }
309 | }
310 |
311 | /**
312 | * 将所有文件添加到成功上传的文件列表中。
313 | * 这将自动从params.getFiles() 列表中删除它们。
314 | */
315 | protected fun setAllFilesHaveBeenSuccessfullyUploaded(value: Boolean = true) {
316 | params.files.forEach { it.successfullyUploaded = value }
317 | }
318 |
319 | /**
320 | * 获取所有成功上传文件的列表。
321 | * 你不能在你的子类中修改这个列表!您只能阅读其内容。
322 | * 如果你想添加一个元素,
323 | * 使用 [UploadTask.addSuccessfullyUploadedFile]
324 | * @ 返回字符串列表
325 | */
326 | protected val successfullyUploadedFiles: List
327 | get() = params.files.filter { it.successfullyUploaded }
328 |
329 | fun cancel() {
330 | job.cancel()
331 | }
332 |
333 | private fun shouldThrottle(uploadedBytes: Long, totalBytes: Long): Boolean {
334 | val currentTime = System.currentTimeMillis()
335 |
336 | if (uploadedBytes < totalBytes && currentTime < lastProgressNotificationTime + UploadConfiguration.uploadProgressNotificationIntervalMillis) {
337 | return true
338 | }
339 |
340 | lastProgressNotificationTime = currentTime
341 | return false
342 | }
343 | }
344 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/data/HttpUploadTaskParameters.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload.data
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.Parcelize
5 | import com.dh.quickupload.tools.datapreservation.Persistable
6 | import com.dh.quickupload.tools.datapreservation.PersistableData
7 | import java.util.ArrayList
8 |
9 | /**
10 | * 包含HTTP上载的特定参数的类。
11 | */
12 | @Parcelize
13 | data class HttpUploadTaskParameters(
14 | var method: String = "POST",
15 | var usesFixedLengthStreamingMode: Boolean = true,
16 | val requestHeaders: ArrayList = ArrayList(5),
17 | val requestParameters: ArrayList = ArrayList(5)
18 | ) : Parcelable, Persistable {
19 |
20 | companion object : Persistable.Creator {
21 | private object CodingKeys {
22 | const val method = "method"
23 | const val fixedLength = "fixedLength"
24 | const val headers = "headers"
25 | const val parameters = "params"
26 | }
27 |
28 | private fun List.toNameValueArrayList() =
29 | ArrayList(map { NameValue.createFromPersistableData(it) })
30 |
31 | override fun createFromPersistableData(data: PersistableData) = HttpUploadTaskParameters(
32 | method = data.getString(CodingKeys.method),
33 | usesFixedLengthStreamingMode = data.getBoolean(CodingKeys.fixedLength),
34 | requestHeaders = try {
35 | data.getArrayData(CodingKeys.headers).toNameValueArrayList()
36 | } catch (exc: Throwable) {
37 | ArrayList()
38 | },
39 | requestParameters = try {
40 | data.getArrayData(CodingKeys.parameters).toNameValueArrayList()
41 | } catch (exc: Throwable) {
42 | ArrayList()
43 | }
44 | )
45 | }
46 |
47 | override fun toPersistableData() = PersistableData().apply {
48 | putString(CodingKeys.method, method)
49 | putBoolean(CodingKeys.fixedLength, usesFixedLengthStreamingMode)
50 | putArrayData(CodingKeys.headers, requestHeaders.map { it.toPersistableData() })
51 | putArrayData(CodingKeys.parameters, requestParameters.map { it.toPersistableData() })
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/data/NameValue.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload.data
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.Parcelize
5 | import com.dh.quickupload.extensions.isASCII
6 | import com.dh.quickupload.tools.datapreservation.Persistable
7 | import com.dh.quickupload.tools.datapreservation.PersistableData
8 |
9 | @Parcelize
10 | data class NameValue(val name: String, val value: String) : Parcelable, Persistable {
11 | fun validateAsHeader(): NameValue {
12 | require(name.isASCII() && value.isASCII()) {
13 | "标头 ${name}及其值 ${value}必须仅为ASCII!"
14 | }
15 |
16 | return this
17 | }
18 |
19 | override fun toPersistableData() = PersistableData().apply {
20 | putString(CodingKeys.name, name)
21 | putString(CodingKeys.value, value)
22 | }
23 |
24 | companion object : Persistable.Creator {
25 | private object CodingKeys {
26 | const val name = "name"
27 | const val value = "value"
28 | }
29 |
30 | override fun createFromPersistableData(data: PersistableData) = NameValue(
31 | name = data.getString(CodingKeys.name),
32 | value = data.getString(CodingKeys.value)
33 | )
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/data/RetryPolicyConfig.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload.data
2 |
3 | data class RetryPolicyConfig(
4 | /**
5 | * 当上传失败时,设置下一次尝试之前等待的时间 (以秒为单位)
6 | */
7 | val initialWaitTimeSeconds: Int,
8 |
9 | /**
10 | * 设置两次上传尝试之间的最长等待时间 (以秒为单位)。
11 | */
12 | val maxWaitTimeSeconds: Int,
13 |
14 | /**
15 | * 设置退避定时器乘数。例如,如果设置为2,则每次上载
16 | */
17 | val multiplier: Int,
18 |
19 | /**
20 | * 设置每个请求的默认重试次数。
21 | */
22 | val defaultMaxRetries: Int
23 | ) {
24 | override fun toString(): String {
25 | return """{"initialWaitTimeSeconds": $initialWaitTimeSeconds, "maxWaitTimeSeconds": $maxWaitTimeSeconds, "multiplier": $multiplier, "defaultMaxRetries": $defaultMaxRetries}"""
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/data/UploadElapsedTime.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload.data
2 |
3 | /**
4 | * 上传时间
5 | */
6 | data class UploadElapsedTime(val minutes: Int, val seconds: Int) {
7 | val totalSeconds: Int
8 | get() = minutes * 60 + seconds
9 | }
10 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/data/UploadFile.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload.data
2 |
3 | import android.os.Parcelable
4 | import java.util.LinkedHashMap
5 | import kotlinx.parcelize.IgnoredOnParcel
6 | import kotlinx.parcelize.Parcelize
7 | import com.dh.quickupload.UploadConfiguration
8 | import com.dh.quickupload.tools.datapreservation.Persistable
9 | import com.dh.quickupload.tools.datapreservation.PersistableData
10 | import com.dh.quickupload.tools.translationfile.SchemeHandler
11 |
12 | @Parcelize
13 | data class UploadFile @JvmOverloads constructor(
14 | val path: String,
15 | val properties: LinkedHashMap = LinkedHashMap()
16 | ) : Parcelable, Persistable {
17 |
18 | companion object : Persistable.Creator {
19 | private const val successfulUpload = "successful_upload"
20 |
21 | private object CodingKeys {
22 | const val path = "path"
23 | const val properties = "props"
24 | }
25 |
26 | override fun createFromPersistableData(data: PersistableData) = UploadFile(
27 | path = data.getString(CodingKeys.path),
28 | properties = LinkedHashMap().apply {
29 | val bundle = data.getData(CodingKeys.properties).toBundle()
30 | bundle.keySet().forEach { propKey ->
31 | put(propKey, bundle.getString(propKey)!!)
32 | }
33 | }
34 | )
35 | }
36 |
37 | @IgnoredOnParcel
38 | val handler: SchemeHandler by lazy {
39 | UploadConfiguration.getSchemeHandler(path)
40 | }
41 |
42 | @IgnoredOnParcel
43 | var successfullyUploaded: Boolean
44 | get() = properties[successfulUpload]?.toBoolean() ?: false
45 | set(value) {
46 | properties[successfulUpload] = value.toString()
47 | }
48 |
49 | override fun toPersistableData() = PersistableData().apply {
50 | putString(CodingKeys.path, path)
51 | putData(CodingKeys.properties, PersistableData().apply {
52 | properties.entries.forEach { (propKey, propVal) ->
53 | putString(propKey, propVal)
54 | }
55 | })
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/data/UploadInfo.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload.data
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.IgnoredOnParcel
5 | import kotlinx.parcelize.Parcelize
6 | import java.util.ArrayList
7 | import java.util.Date
8 |
9 | @Parcelize
10 | data class UploadInfo @JvmOverloads constructor(
11 | /**
12 | * 上传唯一ID
13 | */
14 | val uploadId: String,
15 |
16 | /**
17 | * 上传任务的开始时间戳 (以毫秒为单位)
18 | */
19 | val startTime: Long = 0,
20 |
21 | /**
22 | * 字节上传长度
23 | */
24 | val uploadedBytes: Long = 0,
25 |
26 | /**
27 | * 上传任务总字节数。
28 | */
29 | val totalBytes: Long = 0,
30 |
31 | /**
32 | * 在上传过程中进行的重试次数。
33 | * 如果未进行重试,则此值将为零。
34 | */
35 | val numberOfRetries: Int = 0,
36 |
37 | /**
38 | * 此上传中存在的所有文件的列表。
39 | */
40 | val files: ArrayList = ArrayList()
41 | ) : Parcelable {
42 |
43 | /**
44 | * 获取上载任务的已用时间 (以毫秒为单位)。
45 | */
46 | @IgnoredOnParcel
47 | val elapsedTime: UploadElapsedTime
48 | get() {
49 | var seconds = ((Date().time - startTime) / 1000).toInt()
50 | val minutes = seconds / 60
51 | seconds -= 60 * minutes
52 |
53 | return UploadElapsedTime(minutes, seconds)
54 | }
55 |
56 | /**
57 | * 获取以Kb/s (每秒千位) 为单位的平均上传速率。
58 | */
59 | @IgnoredOnParcel
60 | val uploadRate: UploadRate
61 | get() {
62 | val elapsedSeconds = elapsedTime.totalSeconds
63 |
64 | // wait at least a second to stabilize the upload rate a little bit
65 | val kilobitPerSecond = if (elapsedSeconds < 1)
66 | 0.0
67 | else
68 | uploadedBytes.toDouble() / 1000 * 8 / elapsedSeconds
69 |
70 | return when {
71 | kilobitPerSecond < 1 -> UploadRate(
72 | value = (kilobitPerSecond * 1000).toInt(),
73 | unit = UploadRate.UploadRateUnit.BitPerSecond
74 | )
75 |
76 | kilobitPerSecond >= 1000 -> UploadRate(
77 | value = (kilobitPerSecond / 1000).toInt(),
78 | unit = UploadRate.UploadRateUnit.MegabitPerSecond
79 | )
80 |
81 | else -> UploadRate(
82 | value = kilobitPerSecond.toInt(),
83 | unit = UploadRate.UploadRateUnit.KilobitPerSecond
84 | )
85 | }
86 | }
87 |
88 | /**
89 | * 获取以百分比表示的上载进度 (从0到100)。
90 | */
91 | @IgnoredOnParcel
92 | val progressPercent: Int
93 | get() = if (totalBytes == 0L) 0 else (uploadedBytes * 100 / totalBytes).toInt()
94 |
95 | @IgnoredOnParcel
96 | val successfullyUploadedFiles: Int
97 | get() = files.count { it.successfullyUploaded }
98 | }
99 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/data/UploadRate.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload.data
2 |
3 | data class UploadRate(val value: Int = 0, val unit: UploadRateUnit = UploadRateUnit.BitPerSecond) {
4 | enum class UploadRateUnit {
5 | BitPerSecond,
6 | KilobitPerSecond,
7 | MegabitPerSecond
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/data/UploadStatus.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload.data
2 |
3 | enum class UploadStatus {
4 | DEFAULT,
5 | Wait,
6 | InProgress,
7 | Success,
8 | Error,
9 | Completed
10 | }
11 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/data/UploadTaskParameters.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload.data
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.Parcelize
5 | import com.dh.quickupload.tools.datapreservation.Persistable
6 | import com.dh.quickupload.tools.datapreservation.PersistableData
7 |
8 | @Parcelize
9 | data class UploadTaskParameters(
10 | val taskClass: String,
11 | val id: String,
12 | val serverUrl: String,
13 | val maxRetries: Int,
14 | val autoDeleteSuccessfullyUploadedFiles: Boolean,
15 | val files: ArrayList,
16 | val resumedFileStart:Long,
17 | val additionalParameters: PersistableData
18 | ) : Parcelable, Persistable {
19 | override fun toPersistableData() = PersistableData().apply {
20 | putString(CodingKeys.taskClass, taskClass)
21 | putString(CodingKeys.id, id)
22 | putString(CodingKeys.serverUrl, serverUrl)
23 | putInt(CodingKeys.maxRetries, maxRetries)
24 | putBoolean(CodingKeys.autoDeleteFiles, autoDeleteSuccessfullyUploadedFiles)
25 | putArrayData(CodingKeys.files, files.map { it.toPersistableData() })
26 | putLong(CodingKeys.resumedFileStart,resumedFileStart)
27 | putData(CodingKeys.params, additionalParameters)
28 | }
29 |
30 | companion object : Persistable.Creator {
31 | private object CodingKeys {
32 | const val taskClass = "taskClass"
33 | const val id = "id"
34 | const val serverUrl = "serverUrl"
35 | const val maxRetries = "maxRetries"
36 | const val autoDeleteFiles = "autoDeleteFiles"
37 | const val files = "files"
38 | const val resumedFileStart = "resumedFileStart"
39 | const val params = "params"
40 | }
41 |
42 | override fun createFromPersistableData(data: PersistableData) = UploadTaskParameters(
43 | taskClass = data.getString(CodingKeys.taskClass),
44 | id = data.getString(CodingKeys.id),
45 | serverUrl = data.getString(CodingKeys.serverUrl),
46 | maxRetries = data.getInt(CodingKeys.maxRetries),
47 | autoDeleteSuccessfullyUploadedFiles = data.getBoolean(CodingKeys.autoDeleteFiles),
48 | files = ArrayList(data.getArrayData(CodingKeys.files).map { UploadFile.createFromPersistableData(it) }),
49 | resumedFileStart=data.getLong(CodingKeys.resumedFileStart),
50 | additionalParameters = data.getData(CodingKeys.params)
51 | )
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/exceptions/Exceptions.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload.exceptions
2 |
3 | import com.dh.quickupload.network.ServerResponse
4 |
5 | class UserCancelledUploadException : Throwable("用户已取消上传")
6 | class UploadError(val serverResponse: ServerResponse) : Throwable("上传错误")
7 | class NoNetworkException: Throwable("网络连接断开")
8 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/extensions/Extensions.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload.extensions
2 |
3 | import android.annotation.SuppressLint
4 | import android.app.PendingIntent
5 | import android.content.BroadcastReceiver
6 | import android.content.Context
7 | import android.content.Intent
8 | import android.content.IntentFilter
9 | import android.os.Build
10 | import android.os.Parcelable
11 | import android.os.PowerManager
12 | import android.webkit.MimeTypeMap
13 | import com.dh.quickupload.UploadService
14 | import com.dh.quickupload.UploadTask
15 | import com.dh.quickupload.data.NameValue
16 | import com.dh.quickupload.data.UploadTaskParameters
17 | import com.dh.quickupload.tools.logger.Logger
18 | import com.dh.quickupload.observer.task.UploadTaskObserver
19 | import kotlinx.coroutines.CoroutineScope
20 | import java.lang.IllegalStateException
21 | import java.net.URL
22 |
23 | fun ArrayList.addHeader(name: String, value: String) {
24 | add(NameValue(name, value).validateAsHeader())
25 | }
26 |
27 | fun LinkedHashMap.setOrRemove(key: String, value: String?) {
28 | if (value == null) {
29 | remove(key)
30 | } else {
31 | this[key] = value
32 | }
33 | }
34 |
35 |
36 | /**
37 | * Context
38 | */
39 | private const val taskParametersKey = "taskParameters"
40 |
41 | fun Context.startNewUpload(
42 | params: UploadTaskParameters
43 | ): String {
44 |
45 | val intent = Intent(this, UploadService::class.java).apply {
46 | putExtra(taskParametersKey, params)
47 | }
48 | try {
49 | /*
50 | 尝试在API 26上启动服务
51 | 当应用程序在后台时,将触发IllegalStateException
52 | */
53 | startService(intent)
54 | } catch (exc: Throwable) {
55 | if (Build.VERSION.SDK_INT >= 26 && exc is IllegalStateException) {
56 | startForegroundService(intent)
57 | } else {
58 | Logger.error(
59 | component = "UploadService",
60 | uploadId = params.id,
61 | exception = exc,
62 | message = {
63 | "Error starting"
64 | }
65 | )
66 | }
67 | }
68 |
69 | return params.id
70 | }
71 |
72 | data class UploadTaskCreationParameters(
73 | val params: UploadTaskParameters
74 | )
75 |
76 | fun Intent?.getUploadTaskCreationParameters(): UploadTaskCreationParameters? {
77 | if (this == null) {
78 | Logger.error(
79 | component = UploadService.TAG,
80 | uploadId = Logger.NA,
81 | message = {
82 | "实例化新任务时出错。收到无效Intent"
83 | }
84 | )
85 | return null
86 | }
87 |
88 | val params: UploadTaskParameters = parcelableCompat(taskParametersKey) ?: run {
89 | Logger.error(
90 | component = UploadService.TAG,
91 | uploadId = Logger.NA,
92 | message = {
93 | "实例化新任务时出错。缺少任务参数"
94 | }
95 | )
96 | return null
97 | }
98 |
99 | val taskClass = try {
100 | Class.forName(params.taskClass)
101 | } catch (exc: Throwable) {
102 | Logger.error(
103 | component = UploadService.TAG,
104 | uploadId = Logger.NA,
105 | exception = exc,
106 | message = {
107 | "实例化新任务时出错。${params.taskClass} 不存在。"
108 | }
109 | )
110 | null
111 | } ?: return null
112 |
113 | if (!UploadTask::class.java.isAssignableFrom(taskClass)) {
114 | Logger.error(
115 | component = UploadService.TAG,
116 | uploadId = Logger.NA,
117 | message = {
118 | "实例化新任务时出错。${params.taskClass} 不扩展 UploadTask。"
119 | }
120 | )
121 | return null
122 | }
123 |
124 | return UploadTaskCreationParameters(
125 | params = params,
126 | )
127 | }
128 |
129 | /**
130 | * 根据intent中请求的task类创建一个新的task实例。
131 | * @ return task实例,如果task类不支持或无效,则返回null
132 | */
133 | @Suppress("UNCHECKED_CAST")
134 | fun Context.getUploadTask(
135 | creationParameters: UploadTaskCreationParameters,
136 | scope: CoroutineScope,
137 | vararg observers: UploadTaskObserver
138 | ): UploadTask? {
139 | return try {
140 | val taskClass = Class.forName(creationParameters.params.taskClass) as Class
141 | val uploadTask = taskClass.newInstance().apply {
142 | init(
143 | context = this@getUploadTask,
144 | taskParams = creationParameters.params,
145 | scope = scope,
146 | taskObservers = observers
147 | )
148 | }
149 |
150 | Logger.debug(
151 | component = UploadService.TAG,
152 | uploadId = Logger.NA,
153 | message = {
154 | "已成功创建具有类的新任务: ${taskClass.name}"
155 | }
156 | )
157 | uploadTask
158 | } catch (exc: Throwable) {
159 | Logger.error(
160 | component = UploadService.TAG,
161 | uploadId = Logger.NA,
162 | exception = exc,
163 | message = {
164 | "实例化新任务时出错"
165 | }
166 | )
167 | null
168 | }
169 | }
170 |
171 | // 调整Android 12的标志
172 | fun flagsCompat(flags: Int): Int {
173 | if (Build.VERSION.SDK_INT > 30) {
174 | return flags or PendingIntent.FLAG_IMMUTABLE
175 | }
176 |
177 | return flags
178 | }
179 |
180 | inline fun Intent.parcelableCompat(key: String): T? = when {
181 | Build.VERSION.SDK_INT >= 34 -> getParcelableExtra(key, T::class.java)
182 | else -> @Suppress("DEPRECATION") getParcelableExtra(key) as? T
183 | }
184 |
185 | @SuppressLint("UnspecifiedRegisterReceiverFlag")
186 | fun Context.registerReceiverCompat(receiver: BroadcastReceiver, filter: IntentFilter) {
187 | if (Build.VERSION.SDK_INT >= 34) {
188 | registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED)
189 | } else {
190 | registerReceiver(receiver, filter)
191 | }
192 | }
193 |
194 | /**
195 | * String
196 | */
197 | internal const val APPLICATION_OCTET_STREAM = "application/octet-stream"
198 | internal const val VIDEO_MP4 = "video/mp4"
199 |
200 | /**
201 | * 尝试自动检测特定文件的内容类型 (MIME类型)。
202 | * @ param absolutePath文件的绝对路径
203 | * @ return文件的内容类型 (MIME类型),如果没有内容,则为application/octet-stream
204 | * 类型可以自动确定
205 | */
206 | fun String.autoDetectMimeType(): String {
207 | val index = lastIndexOf(".")
208 |
209 | return if (index in 0 until lastIndex) {
210 | val extension = substring(index + 1).lowercase()
211 |
212 | if (extension == "mp4") {
213 | VIDEO_MP4
214 | } else {
215 | MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
216 | ?: APPLICATION_OCTET_STREAM
217 | }
218 | } else {
219 | APPLICATION_OCTET_STREAM
220 | }
221 | }
222 |
223 | fun String?.isASCII(): Boolean {
224 | if (this.isNullOrBlank())
225 | return false
226 |
227 | for (element in this) {
228 | if (element.code > 127) {
229 | return false
230 | }
231 | }
232 |
233 | return true
234 | }
235 |
236 | fun String.isValidHttpUrl(): Boolean {
237 | if (!startsWith("http://") && !startsWith("https://")) return false
238 |
239 | return try {
240 | URL(this)
241 | true
242 | } catch (exc: Throwable) {
243 | false
244 | }
245 | }
246 |
247 | val String.asciiBytes: ByteArray
248 | get() = toByteArray(Charsets.US_ASCII)
249 |
250 | val String.utf8Bytes: ByteArray
251 | get() = toByteArray(Charsets.UTF_8)
252 |
253 |
254 | /**
255 | * WakeLock
256 | */
257 | fun PowerManager.WakeLock?.safeRelease() {
258 | this?.apply { if (isHeld) release() }
259 | }
260 |
261 | fun Context.acquirePartialWakeLock(
262 | currentWakeLock: PowerManager.WakeLock?,
263 | tag: String
264 | ): PowerManager.WakeLock {
265 | if (currentWakeLock?.isHeld == true) {
266 | return currentWakeLock
267 | }
268 |
269 | val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
270 |
271 | return powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, tag).apply {
272 | setReferenceCounted(false)
273 | if (!isHeld) acquire()
274 | }
275 | }
276 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/network/BaseNetwork.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload.network
2 |
3 | import java.io.IOException
4 |
5 | interface BaseNetwork {
6 | /**
7 | * 为给定的URL和HTTP方法创建一个新的连接。
8 | * @ param uploadId请求此连接的上载的ID
9 | * @ param方法HTTP方法
10 | * @ param url要连接到的URL
11 | * @ return新连接对象
12 | * @ 如果在创建连接对象时发生错误,则抛出IOException
13 | */
14 | @Throws(IOException::class)
15 | fun createRequest(uploadId: String, method: String, url: String): NetworkRequest
16 | }
17 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/network/BodyWriter.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload.network
2 |
3 | import android.util.Log
4 | import java.io.Closeable
5 | import java.io.IOException
6 | import java.io.InputStream
7 | import com.dh.quickupload.UploadConfiguration
8 |
9 | abstract class BodyWriter(private val listener: OnStreamWriteListener) : Closeable {
10 |
11 | /**
12 | * 接收流写入进度并具有取消它的能力。
13 | */
14 | interface OnStreamWriteListener {
15 | /**
16 | * 指示是否应继续将流写入正文。
17 | * @ return true继续将流写入正文,false取消
18 | */
19 | fun shouldContinueWriting(): Boolean
20 |
21 | /**
22 | * 每次将一堆字节写入正文时调用
23 | * @ param字节写入的字节数
24 | */
25 | fun onBytesWritten(bytesWritten: Int)
26 | }
27 |
28 | /**
29 | * 将输入流写入请求正文。
30 | * 流将在成功写入或抛出异常后自动关闭。
31 | * @ param stream要从中读取的输入流
32 | * @ 如果发生I/O错误,则抛出IOException
33 | */
34 | @Throws(IOException::class)
35 | fun writeStream(stream: InputStream ,end :Long,start:Long) {
36 | val buffer = ByteArray(UploadConfiguration.bufferSizeBytes)
37 | stream.use {
38 | it.skip(start)
39 | var bytesRead=0
40 | var bytesRemaining = end - start
41 | while (listener.shouldContinueWriting() && bytesRemaining > 0 && it.read(buffer, 0, minOf(buffer.size.toLong(), bytesRemaining).toInt()).also { bytesRead = it }!= -1 ) {
42 | write(buffer, bytesRead)
43 | bytesRemaining -= bytesRead.toLong()
44 | }
45 | }
46 | }
47 |
48 | /**
49 | * 将一个字节数组写入请求正文。
50 | * @ param字节数组与字节写入
51 | * @ 如果在写入时出现错误,则抛出IOException
52 | */
53 | fun write(bytes: ByteArray) {
54 | internalWrite(bytes)
55 | flush()
56 | listener.onBytesWritten(bytes.size)
57 | }
58 |
59 | /**
60 | * 将字节数组的一部分写入请求正文。
61 | * @ param字节数组与字节写入
62 | * @ param lengthtowriefromstart写多少字节,从第一个开始
63 | * 数组
64 | * @ 如果在写入时出现错误,则抛出IOException
65 | */
66 | fun write(bytes: ByteArray, lengthToWriteFromStart: Int) {
67 | internalWrite(bytes, lengthToWriteFromStart)
68 | flush()
69 | listener.onBytesWritten(lengthToWriteFromStart)
70 | }
71 |
72 | @Throws(IOException::class)
73 | abstract fun internalWrite(bytes: ByteArray)
74 |
75 | @Throws(IOException::class)
76 | abstract fun internalWrite(bytes: ByteArray, lengthToWriteFromStart: Int)
77 |
78 | /**
79 | * 确保写入正文的字节全部传输到服务器并清除
80 | * 本地缓冲区。
81 | * @ 如果在刷新缓冲区时发生错误,则抛出IOException
82 | */
83 | @Throws(IOException::class)
84 | abstract fun flush()
85 | }
86 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/network/NetworkRequest.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload.network
2 |
3 | import java.io.Closeable
4 | import java.io.IOException
5 | import com.dh.quickupload.data.NameValue
6 |
7 | interface NetworkRequest : Closeable {
8 |
9 | /**
10 | * 当Body准备好被写时,委托被调用。
11 | */
12 | interface RequestBodyDelegate {
13 |
14 | /**
15 | * 处理请求正文的写入。
16 | * @ param bodyWriter用于在正文上写入的对象
17 | * @ 如果在写入正文时发生错误,则抛出IOException
18 | */
19 | @Throws(IOException::class)
20 | fun onWriteRequestBody(bodyWriter: BodyWriter)
21 | }
22 |
23 | /**
24 | * 设置请求标头。
25 | * @ param requestHeaders要设置的请求标头
26 | * @ 如果在设置请求头时发生错误,则抛出IOException
27 | * @ return实例
28 | */
29 | @Throws(IOException::class)
30 | fun setHeaders(requestHeaders: List): NetworkRequest
31 |
32 | /**
33 | * 设置总body字节数。
34 | * @ param totalBodyBytes总字节数
35 | * @ param isFixedLengthStreamingMode如果必须使用固定长度流模式,则为true。如果
36 | * 这是假的,必须使用chunked流模式。
37 | * @ return实例
38 | */
39 | fun setTotalBodyBytes(totalBodyBytes: Long, isFixedLengthStreamingMode: Boolean): NetworkRequest
40 |
41 | /**
42 | * 获取服务器响应。
43 | * @ return对象,包含服务器响应状态、标头和正文。
44 | * @ param委托处理请求正文的写入
45 | * @ param监听器,它在写入字节时得到通知,并控制是否
46 | * @ 如果在获取服务器响应时发生错误,则抛出IOException
47 | */
48 | @Throws(IOException::class)
49 | fun getResponse(
50 | delegate: RequestBodyDelegate,
51 | listener: BodyWriter.OnStreamWriteListener
52 | ): ServerResponse
53 | }
54 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/network/ServerResponse.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload.network
2 |
3 | import android.os.Parcelable
4 | import java.io.Serializable
5 | import kotlinx.parcelize.IgnoredOnParcel
6 | import kotlinx.parcelize.Parcelize
7 |
8 | @Parcelize
9 | data class ServerResponse(
10 | /**
11 | * 服务器响应响应代码。如果您正在实现非HTTP
12 | * 协议,将此设置为200以通知任务已完成
13 | * 成功。小于200或大于299表示的整数值
14 | * 来自服务器的错误响应。
15 | */
16 | val code: Int,
17 |
18 | /**
19 | * 服务器响应正文。
20 | * 如果你的服务器响应一个字符串,你可以得到它
21 | * 如果字符串是JSON,则可以使用org.json等库对其进行解析
22 | * 如果你的服务器没有返回任何东西,设置为空数组。
23 | */
24 | val body: ByteArray,
25 | /**
26 | * 服务器响应标头
27 | */
28 | val headers: LinkedHashMap
29 | ) : Parcelable, Serializable {
30 |
31 | /**
32 | * 获取服务器响应正文作为字符串。
33 | * 如果字符串是JSON,则可以使用org.json等库对其进行解析
34 | * @ 返回字符串
35 | */
36 | @IgnoredOnParcel
37 | val bodyString: String
38 | get() = String(body)
39 |
40 | @IgnoredOnParcel
41 | val isSuccessful: Boolean
42 | get() = code in 200..399
43 |
44 | companion object {
45 | fun successfulEmpty(): ServerResponse {
46 | return ServerResponse(
47 | code = 200, body = ByteArray(1), headers = LinkedHashMap()
48 | )
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/network/okhttp/OkHttpBodyWriter.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload.network.okhttp
2 |
3 | import com.dh.quickupload.network.BodyWriter
4 | import okio.BufferedSink
5 | import java.io.IOException
6 |
7 | class OkHttpBodyWriter(private val sink: BufferedSink, listener: OnStreamWriteListener) :
8 | BodyWriter(listener) {
9 | @Throws(IOException::class)
10 | override fun internalWrite(bytes: ByteArray) {
11 | sink.write(bytes)
12 | }
13 |
14 | @Throws(IOException::class)
15 | override fun internalWrite(bytes: ByteArray, lengthToWriteFromStart: Int) {
16 | sink.write(bytes, 0, lengthToWriteFromStart)
17 | }
18 |
19 | @Throws(IOException::class)
20 | override fun flush() {
21 | sink.flush()
22 | }
23 |
24 | @Throws(IOException::class)
25 | override fun close() {
26 | sink.close()
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/network/okhttp/OkHttpExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload.network.okhttp
2 |
3 | import com.dh.quickupload.network.ServerResponse
4 | import okhttp3.Response
5 |
6 | private fun String.requiresRequestBody() =
7 | this == "POST" || this == "PUT" || this == "PATCH" || this == "PROPPATCH" || this == "REPORT"
8 |
9 | private fun String.permitsRequestBody() = !(this == "GET" || this == "HEAD")
10 |
11 | internal fun String.hasBody(): Boolean {
12 | val method = trim().uppercase()
13 | return method.permitsRequestBody() || method.requiresRequestBody()
14 | }
15 |
16 | private fun Response.headersHashMap() = LinkedHashMap(headers.toMap())
17 |
18 | private fun Response.bodyBytes() = body?.bytes() ?: ByteArray(0)
19 |
20 | internal fun Response.asServerResponse() = ServerResponse(code, bodyBytes(), headersHashMap())
21 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/network/okhttp/OkHttpNetwork.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload.network.okhttp
2 |
3 |
4 | import com.dh.quickupload.UploadConfiguration
5 | import com.dh.quickupload.network.NetworkRequest
6 | import com.dh.quickupload.network.BaseNetwork
7 | import okhttp3.Interceptor
8 | import okhttp3.OkHttpClient
9 | import okhttp3.Response
10 | import java.io.IOException
11 | import java.util.concurrent.TimeUnit
12 |
13 | /**
14 | * OkHttp网络的实现.
15 | */
16 | class OkHttpNetwork(
17 | private val client: OkHttpClient =
18 | OkHttpClient.Builder()
19 | .followRedirects(true)
20 | .followSslRedirects(true)
21 | .retryOnConnectionFailure(true)
22 | .connectTimeout(15, TimeUnit.SECONDS)
23 | .writeTimeout(30, TimeUnit.SECONDS)
24 | .readTimeout(30, TimeUnit.SECONDS)
25 | .cache(null)
26 | .addInterceptor(Interceptor { chain ->
27 | val request = chain.request().newBuilder()
28 | .header("User-Agent", UploadConfiguration.defaultUserAgent)
29 | .build()
30 | chain.proceed(request)
31 | })
32 | .build()
33 | ) : BaseNetwork {
34 | @Throws(IOException::class)
35 | override fun createRequest(uploadId: String, method: String, url: String): NetworkRequest {
36 | return OkHttpNetworkRequest(uploadId, client, method, url)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/network/okhttp/OkHttpNetworkRequest.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload.network.okhttp
2 |
3 | import com.dh.quickupload.data.NameValue
4 | import com.dh.quickupload.tools.logger.Logger
5 | import com.dh.quickupload.network.BodyWriter
6 | import com.dh.quickupload.network.NetworkRequest
7 | import okhttp3.MediaType
8 | import okhttp3.MediaType.Companion.toMediaTypeOrNull
9 | import okhttp3.OkHttpClient
10 | import okhttp3.Request
11 | import okhttp3.RequestBody
12 | import okio.BufferedSink
13 | import java.io.IOException
14 | import java.net.URL
15 | import java.util.UUID
16 |
17 | /**
18 | * [NetworkRequest] 使用OkHttpClient实现。
19 | */
20 | class OkHttpNetworkRequest(
21 | private val uploadId: String,
22 | private val httpClient: OkHttpClient,
23 | private val httpMethod: String,
24 | url: String
25 | ) : NetworkRequest {
26 |
27 | private val requestBuilder = Request.Builder().url(URL(url))
28 | private var bodyLength = 0L
29 | private var contentType: MediaType? = null
30 | private val uuid = UUID.randomUUID().toString()
31 |
32 | init {
33 | Logger.debug(javaClass.simpleName, uploadId) {
34 | "创建新的OkHttp连接 (uuid: $uuid)"
35 | }
36 | }
37 |
38 | @Throws(IOException::class)
39 | override fun setHeaders(requestHeaders: List): NetworkRequest {
40 | for (param in requestHeaders) {
41 | if ("content-type" == param.name.trim().lowercase())
42 | contentType = param.value.trim().toMediaTypeOrNull()
43 |
44 | requestBuilder.header(param.name.trim(), param.value.trim())
45 | }
46 |
47 | return this
48 | }
49 |
50 | override fun setTotalBodyBytes(
51 | totalBodyBytes: Long,
52 | isFixedLengthStreamingMode: Boolean
53 | ): NetworkRequest {
54 | bodyLength = if (isFixedLengthStreamingMode) totalBodyBytes else -1
55 |
56 | return this
57 | }
58 |
59 | private fun createBody(
60 | delegate: NetworkRequest.RequestBodyDelegate,
61 | listener: BodyWriter.OnStreamWriteListener
62 | ): RequestBody? {
63 | if (!httpMethod.hasBody()) return null
64 |
65 | return object : RequestBody() {
66 | override fun contentLength() = bodyLength
67 |
68 | override fun contentType() = contentType
69 |
70 | override fun writeTo(sink: BufferedSink) {
71 | OkHttpBodyWriter(sink, listener).use {
72 | delegate.onWriteRequestBody(it)
73 | }
74 | }
75 | }
76 | }
77 |
78 | private fun request(
79 | delegate: NetworkRequest.RequestBodyDelegate,
80 | listener: BodyWriter.OnStreamWriteListener
81 | ) = requestBuilder
82 | .method(httpMethod, createBody(delegate, listener))
83 | .build()
84 |
85 | @Throws(IOException::class)
86 | override fun getResponse(
87 | delegate: NetworkRequest.RequestBodyDelegate,
88 | listener: BodyWriter.OnStreamWriteListener
89 | ) = use {
90 | httpClient.newCall(request(delegate, listener))
91 | .execute()
92 | .use { it.asServerResponse() }
93 | }
94 |
95 | override fun close() {
96 | // 资源在使用后自动释放。仅记录。
97 | Logger.debug(javaClass.simpleName, uploadId) {
98 | "正在关闭OkHttp连接 (uuid: $uuid)"
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/observer/network/NetworkMonitor.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload.observer.network
2 |
3 | import android.content.Context
4 | import android.net.ConnectivityManager
5 | import android.net.Network
6 | import android.net.NetworkCapabilities
7 | import android.net.NetworkRequest
8 | import com.dh.quickupload.UploadService
9 | import java.lang.ref.WeakReference
10 |
11 | class NetworkMonitor(context: Context) {
12 |
13 | private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
14 | private val networkCallback: ConnectivityManager.NetworkCallback
15 |
16 | init {
17 | networkCallback = object : ConnectivityManager.NetworkCallback() {
18 | override fun onLost(network: Network) {
19 | super.onLost(network)
20 | UploadService.noNetworkStopAllUploads()
21 | }
22 | }
23 | }
24 |
25 | fun register() {
26 | val networkRequest = NetworkRequest.Builder()
27 | .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
28 | .build()
29 | connectivityManager.registerNetworkCallback(networkRequest, networkCallback)
30 | }
31 |
32 | fun unregister() {
33 | connectivityManager.unregisterNetworkCallback(networkCallback)
34 | }
35 | }
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/observer/task/TaskCompletionNotifier.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload.observer.task
2 |
3 | import com.dh.quickupload.UploadService
4 | import com.dh.quickupload.data.UploadInfo
5 | import com.dh.quickupload.network.ServerResponse
6 |
7 | class TaskCompletionNotifier(private val service: UploadService) : UploadTaskObserver {
8 | override fun onWait(
9 | info: UploadInfo
10 | ) {
11 | }
12 |
13 | override fun onProgress(
14 | info: UploadInfo
15 | ) {
16 | }
17 |
18 | override fun onSuccess(
19 | info: UploadInfo,
20 |
21 | response: ServerResponse
22 | ) {
23 | }
24 |
25 | override fun onError(
26 | info: UploadInfo,
27 | exception: Throwable
28 | ) {
29 | }
30 |
31 | override fun onCompleted(
32 | info: UploadInfo
33 | ) {
34 | service.taskCompleted(info.uploadId)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/observer/task/UploadObserverBase.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload.observer.task
2 |
3 | import android.os.Handler
4 | import android.os.Looper
5 | import android.util.Log
6 | import com.dh.quickupload.UploadService
7 | import com.dh.quickupload.data.UploadInfo
8 | import com.dh.quickupload.data.UploadStatus
9 | import com.dh.quickupload.network.ServerResponse
10 | import com.dh.quickupload.quick.QuickUploadRequest
11 |
12 | open class UploadObserverBase (open val uploadId: String) : UploadTaskObserver {
13 | private var callback:((UploadStatus,UploadInfo,Throwable?,ServerResponse?)->Unit)?=null
14 | fun refresh(callback:((UploadStatus,UploadInfo,Throwable?,ServerResponse?)->Unit)){
15 | this.callback=callback
16 | }
17 | /**
18 | * 请求
19 | */
20 | var quickUploadRequest: QuickUploadRequest?=null
21 | /**
22 | * 上传返回信息
23 | */
24 | var uploadInfo: UploadInfo =UploadInfo("")
25 | var status: UploadStatus = UploadStatus.DEFAULT
26 | var exception: Throwable? = null
27 | var serverResponse: ServerResponse? = null
28 |
29 | open fun startUpload(){
30 | quickUploadRequest?.setUploadID(uploadId)
31 | quickUploadRequest?.startUpload()
32 | }
33 | open fun stopUpload(){
34 | UploadService.stopUpload(uploadId)
35 | }
36 | open fun notifyChange() {
37 | // 子类可以覆盖通知逻辑
38 | // Handler(Looper.getMainLooper()).post {
39 | callback?.invoke(status,uploadInfo,exception,serverResponse)
40 | // }
41 | }
42 |
43 | override fun onWait(info: UploadInfo) {
44 | uploadInfo = info
45 | status = UploadStatus.Wait
46 | notifyChange()
47 | }
48 |
49 | override fun onProgress(info: UploadInfo) {
50 | uploadInfo = info
51 | status = UploadStatus.InProgress
52 | notifyChange()
53 | }
54 |
55 | override fun onSuccess(info: UploadInfo, response: ServerResponse) {
56 | uploadInfo = info
57 | status = UploadStatus.Success
58 | serverResponse = response
59 | notifyChange()
60 | }
61 |
62 | override fun onError(info: UploadInfo, exception: Throwable) {
63 | uploadInfo = info
64 | status = UploadStatus.Error
65 | this.exception = exception
66 | notifyChange()
67 | }
68 | override fun onCompleted(info: UploadInfo) {
69 | uploadInfo = info
70 | if (uploadInfo.progressPercent==100){
71 | status = UploadStatus.Completed
72 | }
73 | notifyChange()
74 | }
75 | }
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/observer/task/UploadTaskObserver.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload.observer.task
2 |
3 | import com.dh.quickupload.data.UploadInfo
4 | import com.dh.quickupload.network.ServerResponse
5 |
6 | interface UploadTaskObserver {
7 | fun onWait(info: UploadInfo)
8 |
9 | fun onProgress(
10 | info: UploadInfo
11 | )
12 |
13 | fun onSuccess(
14 | info: UploadInfo,
15 | response: ServerResponse
16 | )
17 |
18 | fun onError(
19 | info: UploadInfo,
20 | exception: Throwable
21 | )
22 |
23 | fun onCompleted(
24 | info: UploadInfo
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/quick/QuickUploadRequest.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload.quick
2 |
3 | import android.content.Context
4 | import java.io.FileNotFoundException
5 | import com.dh.quickupload.GeneralUploadRequest
6 | import com.dh.quickupload.UploadTask
7 | import com.dh.quickupload.data.UploadFile
8 |
9 | /**
10 | * 上传请求。
11 | * @ param context应用程序上下文
12 | * @ param serverUrl将处理多部分表单上传的服务器端脚本的URL。
13 | */
14 | class QuickUploadRequest(context: Context, serverUrl: String) :
15 | GeneralUploadRequest(context, serverUrl) {
16 |
17 | override val taskClass: Class
18 | get() = QuickUploadTask::class.java
19 |
20 | /**
21 | * 将文件添加到此上传请求。
22 | *
23 | * @ param filePath要上传的文件的路径
24 | * @ param parameterName将包含文件数据的表单参数的名称
25 | * @ param fileName服务器端脚本看到的文件名。如果为null,则为原始文件名
26 | * 将使用
27 | * @ param contentType文件的内容类型。如果为null或empty,则mime类型将为
28 | * 自动检测。如果由于某些原因自动检测失败,
29 | * 默认情况下将使用 “application/octet-stream”
30 | * @ return [QuickUploadRequest]
31 | */
32 | @Throws(FileNotFoundException::class)
33 | @JvmOverloads
34 | fun addFileToUpload(
35 | filePath: String,
36 | parameterName: String,
37 | fileName: String? = null,
38 | contentType: String? = null
39 | ): QuickUploadRequest {
40 | require(filePath.isNotBlank() && parameterName.isNotBlank()) {
41 | "请指定有效的文件路径和参数名称。它们不能是空白的。"
42 | }
43 |
44 | files.add(UploadFile(filePath).apply {
45 | this.parameterName = parameterName
46 |
47 | this.contentType = if (contentType.isNullOrBlank()) {
48 | handler.contentType(context)
49 | } else {
50 | contentType
51 | }
52 |
53 | remoteFileName = if (fileName.isNullOrBlank()) {
54 | handler.name(context)
55 | } else {
56 | fileName
57 | }
58 | })
59 |
60 | return this
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/quick/QuickUploadTask.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload.quick
2 |
3 | import com.dh.quickupload.GeneralUploadTask
4 | import com.dh.quickupload.data.NameValue
5 | import com.dh.quickupload.data.UploadFile
6 | import com.dh.quickupload.extensions.addHeader
7 | import com.dh.quickupload.extensions.asciiBytes
8 | import com.dh.quickupload.extensions.utf8Bytes
9 | import com.dh.quickupload.network.BodyWriter
10 |
11 | /**
12 | * 实现HTTP分段上传任务。
13 | */
14 | class QuickUploadTask : GeneralUploadTask() {
15 |
16 | companion object {
17 | private const val BOUNDARY_SIGNATURE = "-------UploadService1.0.0-"
18 | private const val NEW_LINE = "\r\n"
19 | private const val TWO_HYPHENS = "--"
20 | }
21 |
22 | private val boundary = BOUNDARY_SIGNATURE + System.nanoTime()
23 | private val boundaryBytes = (TWO_HYPHENS + boundary + NEW_LINE).asciiBytes
24 | private val trailerBytes = (TWO_HYPHENS + boundary + TWO_HYPHENS + NEW_LINE).asciiBytes
25 | private val newLineBytes = NEW_LINE.utf8Bytes
26 |
27 | private val NameValue.multipartHeader: ByteArray
28 | get() = boundaryBytes + ("Content-Disposition: form-data; " +
29 | "name=\"$name\"$NEW_LINE$NEW_LINE$value$NEW_LINE").utf8Bytes
30 |
31 | private val UploadFile.multipartHeader: ByteArray
32 | get() = boundaryBytes + ("Content-Disposition: form-data; " +
33 | "name=\"$parameterName\"; " +
34 | "filename=\"$remoteFileName\"$NEW_LINE" +
35 | "Content-Type: $contentType$NEW_LINE$NEW_LINE").utf8Bytes
36 |
37 | private val UploadFile.totalMultipartBytes: Long
38 | get() = multipartHeader.size.toLong() + handler.size(context) + newLineBytes.size.toLong()
39 |
40 | private fun BodyWriter.writeRequestParameters() {
41 | httpParams.requestParameters.forEach {
42 | write(it.multipartHeader)
43 | }
44 | }
45 |
46 | private fun BodyWriter.writeFiles() {
47 |
48 | for (file in params.files) {
49 | if (!job.isActive) break
50 | write(file.multipartHeader)
51 | writeStream(file.handler.stream(context),file.handler.size(context),params.resumedFileStart)
52 | write(newLineBytes)
53 | }
54 | }
55 |
56 | private val requestParametersLength: Long
57 | get() = httpParams.requestParameters.map { it.multipartHeader.size.toLong() }.sum()
58 |
59 | private val filesLength: Long
60 | get() = params.files.map { it.totalMultipartBytes }.sum()-params.resumedFileStart
61 |
62 | override val bodyLength: Long
63 | get() = requestParametersLength + filesLength + trailerBytes.size
64 |
65 | override fun performInitialization() {
66 | httpParams.requestHeaders.apply {
67 | addHeader("Content-Type", "multipart/form-data; boundary=$boundary")
68 | addHeader("Connection", if (params.files.size <= 1) "close" else "Keep-Alive")
69 | }
70 | }
71 |
72 | override fun onWriteRequestBody(bodyWriter: BodyWriter) {
73 | // 当正文准备写入时,重置上传的字节
74 | // 因为有时这会在网络更改时调用
75 | resetUploadedBytes()
76 | setAllFilesHaveBeenSuccessfullyUploaded(false)
77 |
78 | bodyWriter.apply {
79 | writeRequestParameters()
80 | writeFiles()
81 | write(trailerBytes)
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/quick/UploadFileExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload.quick
2 |
3 | import com.dh.quickupload.data.UploadFile
4 | import com.dh.quickupload.extensions.setOrRemove
5 |
6 | //与每个文件关联的属性
7 | private const val PROPERTY_PARAM_NAME = "multipartParamName"
8 | private const val PROPERTY_REMOTE_FILE_NAME = "multipartRemoteFileName"
9 | private const val PROPERTY_CONTENT_TYPE = "multipartContentType"
10 |
11 | internal var UploadFile.parameterName: String?
12 | get() = properties[PROPERTY_PARAM_NAME]
13 | set(value) {
14 | properties.setOrRemove(PROPERTY_PARAM_NAME, value)
15 | }
16 |
17 | internal var UploadFile.remoteFileName: String?
18 | get() = properties[PROPERTY_REMOTE_FILE_NAME]
19 | set(value) {
20 | properties.setOrRemove(PROPERTY_REMOTE_FILE_NAME, value)
21 | }
22 |
23 | internal var UploadFile.contentType: String?
24 | get() = properties[PROPERTY_CONTENT_TYPE]
25 | set(value) {
26 | properties.setOrRemove(PROPERTY_CONTENT_TYPE, value)
27 | }
28 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/tools/datapreservation/Persistable.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload.tools.datapreservation
2 |
3 | interface Persistable {
4 | fun toPersistableData(): PersistableData
5 |
6 | interface Creator {
7 | fun createFromPersistableData(data: PersistableData): T
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/tools/datapreservation/PersistableData.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload.tools.datapreservation
2 |
3 | import android.annotation.SuppressLint
4 | import android.os.Bundle
5 | import android.os.Parcel
6 | import android.os.Parcelable
7 | import org.json.JSONObject
8 | import java.lang.IllegalArgumentException
9 |
10 | /**
11 | * 实用程序类用于能够轻松地序列化/反序列化复杂和嵌套的数据使用
12 | * 只有一个平面键值映射。
13 | * 它支持序列化和反序列化:
14 | * - Json
15 | * - PersistableData
16 | */
17 | open class PersistableData() : Parcelable {
18 | protected val data = HashMap()
19 |
20 | override fun equals(other: Any?): Boolean {
21 | if (other == null || other !is PersistableData) return false
22 |
23 | return data == other.data
24 | }
25 |
26 | override fun hashCode() = data.hashCode()
27 |
28 | @SuppressLint("ParcelClassLoader")
29 | private constructor(parcel: Parcel) : this() {
30 | parcel.readBundle()?.let { bundle ->
31 | bundle.keySet().forEach { key ->
32 | when (val value = bundle[key]) {
33 | is Boolean, is Double, is Int, is Long, is String -> data[key] = value
34 | }
35 | }
36 | }
37 | }
38 |
39 | override fun describeContents() = 0
40 |
41 | override fun writeToParcel(dest: Parcel, flags: Int) {
42 | toBundle().writeToParcel(dest, flags)
43 | }
44 |
45 | companion object CREATOR : Parcelable.Creator {
46 | private const val separator = "$"
47 | override fun createFromParcel(parcel: Parcel) = PersistableData(parcel)
48 | override fun newArray(size: Int): Array = arrayOfNulls(size)
49 |
50 | /**
51 | * 从PersistableData JSON表示创建 [PersistableData]。
52 | */
53 | @JvmStatic
54 | fun fromJson(rawJsonString: String): PersistableData {
55 | val json = JSONObject(rawJsonString)
56 | val data = PersistableData()
57 |
58 | json.keys().forEach { key ->
59 | when (val value = json.get(key)) {
60 | is Boolean, is Double, is Int, is Long, is String -> data.data[key] = value
61 | }
62 | }
63 |
64 | return data
65 | }
66 | }
67 |
68 | private fun String.validated(checkExists: Boolean = false): String {
69 | if (contains(separator))
70 | throw IllegalArgumentException("key cannot contain $separator as it's a reserved character, used for nested data")
71 | if (checkExists && !data.containsKey(this))
72 | throw IllegalArgumentException("no data found for key \"$this\"")
73 | return this
74 | }
75 |
76 | fun putBoolean(key: String, value: Boolean) {
77 | data[key.validated()] = value
78 | }
79 |
80 | fun getBoolean(key: String) = data[key.validated(checkExists = true)] as Boolean
81 |
82 | fun putDouble(key: String, value: Double) {
83 | data[key.validated()] = value
84 | }
85 |
86 | fun getDouble(key: String) = data[key.validated(checkExists = true)] as Double
87 |
88 | fun putInt(key: String, value: Int) {
89 | data[key.validated()] = value
90 | }
91 |
92 | fun getInt(key: String) = data[key.validated(checkExists = true)] as Int
93 |
94 | fun putLong(key: String, value: Long) {
95 | data[key.validated()] = value
96 | }
97 |
98 | fun getLong(key: String) = data[key.validated(checkExists = true)] as Long
99 |
100 | fun putString(key: String, value: String) {
101 | data[key.validated()] = value
102 | }
103 |
104 | fun getString(key: String) = data[key.validated(checkExists = true)] as String
105 |
106 | fun putData(key: String, data: PersistableData) {
107 | data.data.forEach { (dataKey, value) ->
108 | this.data["$key$separator$dataKey"] = value
109 | }
110 | }
111 |
112 | fun getData(key: String): PersistableData {
113 | val entries = data.entries.filter { it.key.startsWith("$key$separator") }
114 | if (entries.isEmpty()) return PersistableData()
115 |
116 | return PersistableData().also { extractedData ->
117 | entries.forEach { (entryKey, entryValue) ->
118 | extractedData.data[entryKey.removePrefix("$key$separator")] = entryValue
119 | }
120 | }
121 | }
122 |
123 | fun putArrayData(key: String, data: List) {
124 | data.forEachIndexed { index, persistableData ->
125 | persistableData.data.forEach { (dataKey, value) ->
126 | this.data["$key$separator$index$separator$dataKey"] = value
127 | }
128 | }
129 | }
130 |
131 | fun getArrayData(key: String): List {
132 | val entries = ArrayList(data.entries.filter { it.key.startsWith("$key$separator") })
133 | if (entries.isEmpty()) return emptyList()
134 |
135 | var index = 0
136 |
137 | var elements = entries.filter { it.key.startsWith("$key$separator$index$separator") }
138 |
139 | val outList = ArrayList()
140 |
141 | while (elements.isNotEmpty()) {
142 | outList.add(PersistableData().also { extractedData ->
143 | elements.forEach { (entryKey, entryValue) ->
144 | extractedData.data[entryKey.removePrefix("$key$separator$index$separator")] =
145 | entryValue
146 | }
147 | entries.removeAll(elements)
148 | })
149 |
150 | index += 1
151 | elements = entries.filter { it.key.startsWith("$key$separator$index$separator") }
152 | }
153 |
154 | return outList
155 | }
156 |
157 | /**
158 | * 创建一个新的包,其中包含此 [PersistableData] 中存在的所有字段。
159 | */
160 | fun toBundle() = Bundle().also { bundle ->
161 | data.keys.forEach { key ->
162 | when (val value = data[key]) {
163 | is Boolean -> bundle.putBoolean(key, value)
164 | is Double -> bundle.putDouble(key, value)
165 | is Int -> bundle.putInt(key, value)
166 | is Long -> bundle.putLong(key, value)
167 | is String -> bundle.putString(key, value)
168 | }
169 | }
170 | }
171 |
172 | /**
173 | * 创建一个包含所有字段的JSON字符串表示
174 | * 在此 [PersistableData] 中。
175 | *
176 | * 这并不意味着人类可读,而是一种方便的方式来传递复杂的
177 | * 使用字符串的结构化数据。
178 | */
179 | fun toJson() = JSONObject().also { json ->
180 | data.keys.forEach { key ->
181 | when (val value = data[key]) {
182 | is Boolean, is Double, is Int, is Long, is String -> json.put(key, value)
183 | }
184 | }
185 | }.toString()
186 | }
187 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/tools/logger/DefaultExt.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload.tools.logger
2 |
3 | import android.util.Log
4 |
5 | class DefaultExt : Logger.Ext {
6 |
7 | companion object {
8 | private const val TAG = "QuickUpload"
9 | }
10 |
11 | override fun error(component: String, uploadId: String, message: String, exception: Throwable?) {
12 | Log.e(TAG, "$component - (uploadId: $uploadId) - $message", exception)
13 | }
14 |
15 | override fun debug(component: String, uploadId: String, message: String) {
16 | Log.i(TAG, "$component - (uploadId: $uploadId) - $message")
17 | }
18 |
19 | override fun info(component: String, uploadId: String, message: String) {
20 | Log.i(TAG, "$component - (uploadId: $uploadId) - $message")
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/tools/logger/Logger.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload.tools.logger
2 |
3 | object Logger {
4 | private var logLevel = LogLevel.Off
5 | private val defaultLogger = DefaultExt()
6 | private var loggerDelegate: Ext = defaultLogger
7 |
8 | internal const val NA = "N/A"
9 |
10 | enum class LogLevel {
11 | Debug,
12 | Info,
13 | Error,
14 | Off
15 | }
16 |
17 | interface Ext {
18 | fun error(component: String, uploadId: String, message: String, exception: Throwable?)
19 | fun debug(component: String, uploadId: String, message: String)
20 | fun info(component: String, uploadId: String, message: String)
21 | }
22 |
23 | @Synchronized
24 | @JvmStatic
25 | fun setDelegate(delegate: Ext?) {
26 | loggerDelegate = delegate ?: defaultLogger
27 | }
28 |
29 | @Synchronized
30 | @JvmStatic
31 | fun setLogLevel(level: LogLevel) {
32 | logLevel = level
33 | }
34 |
35 | @Synchronized
36 | @JvmStatic
37 | fun setDevelopmentMode(devModeOn: Boolean) {
38 | logLevel = if (devModeOn) LogLevel.Debug else LogLevel.Off
39 | }
40 |
41 | private fun loggerWithLevel(minLevel: LogLevel) =
42 | if (logLevel > minLevel || logLevel == LogLevel.Off) null else loggerDelegate
43 |
44 | @JvmOverloads
45 | @JvmStatic
46 | fun error(component: String, uploadId: String, exception: Throwable? = null, message: () -> String) {
47 | loggerWithLevel(LogLevel.Error)?.error(component, uploadId, message(), exception)
48 | }
49 |
50 | @JvmStatic
51 | fun info(component: String, uploadId: String, message: () -> String) {
52 | loggerWithLevel(LogLevel.Info)?.info(component, uploadId, message())
53 | }
54 |
55 | @JvmStatic
56 | fun debug(component: String, uploadId: String, message: () -> String) {
57 | loggerWithLevel(LogLevel.Debug)?.debug(component, uploadId, message())
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/tools/translationfile/ContentResolverSchemeHandler.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload.tools.translationfile
2 |
3 | import android.content.Context
4 | import android.net.Uri
5 | import android.provider.OpenableColumns
6 | import com.dh.quickupload.extensions.APPLICATION_OCTET_STREAM
7 | import com.dh.quickupload.tools.logger.Logger
8 | import com.dh.quickupload.tools.logger.Logger.NA
9 | import java.io.File
10 | import java.io.IOException
11 |
12 | internal class ContentResolverSchemeHandler : SchemeHandler {
13 |
14 | private lateinit var uri: Uri
15 |
16 | override fun init(path: String) {
17 | uri = Uri.parse(path)
18 | }
19 |
20 | override fun size(context: Context): Long {
21 | return context.contentResolver.query(uri, null, null, null, null)?.use {
22 | if (it.moveToFirst()) {
23 | val sizeColumn = it.getColumnIndex(OpenableColumns.SIZE)
24 | if (sizeColumn >= 0) it.getLong(sizeColumn) else null
25 | } else {
26 | null
27 | }
28 | } ?: run {
29 | Logger.error(javaClass.simpleName, NA) { "没有 ${uri}的游标数据,返回大小为0" }
30 | 0L
31 | }
32 | }
33 |
34 | override fun stream(context: Context) = context.contentResolver.openInputStream(uri)
35 | ?: throw IOException("无法打开 ${uri}的输入流")
36 |
37 | override fun contentType(context: Context): String {
38 | val type = context.contentResolver.getType(uri)
39 |
40 | return if (type.isNullOrBlank()) {
41 | APPLICATION_OCTET_STREAM
42 | } else {
43 | type
44 | }
45 | }
46 |
47 | override fun name(context: Context): String {
48 | return context.contentResolver.query(uri, null, null, null, null)?.use {
49 | if (it.moveToFirst()) {
50 | val displayNameColumn = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
51 | if (displayNameColumn >= 0) it.getString(displayNameColumn) else null
52 | } else {
53 | null
54 | }
55 | } ?: uri.toString().split(File.separator).last()
56 | }
57 |
58 | override fun delete(context: Context) = try {
59 | context.contentResolver.delete(uri, null, null) > 0
60 | } catch (exc: Throwable) {
61 | Logger.error(javaClass.simpleName, NA, exc) { "文件删除错误" }
62 | false
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/tools/translationfile/FileSchemeHandler.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload.tools.translationfile
2 |
3 | import android.content.Context
4 | import com.dh.quickupload.extensions.autoDetectMimeType
5 | import com.dh.quickupload.tools.logger.Logger
6 | import com.dh.quickupload.tools.logger.Logger.NA
7 | import java.io.File
8 | import java.io.FileInputStream
9 | import java.io.IOException
10 |
11 | internal class FileSchemeHandler : SchemeHandler {
12 | private lateinit var file: File
13 |
14 | override fun init(path: String) {
15 | file = File(path)
16 | }
17 |
18 | override fun size(context: Context) = file.length()
19 |
20 | override fun stream(context: Context) = FileInputStream(file)
21 |
22 | override fun contentType(context: Context) = file.absolutePath.autoDetectMimeType()
23 |
24 | override fun name(context: Context) = file.name
25 | ?: throw IOException("无法获取 ${file.absolutePath} 的文件名")
26 |
27 | override fun delete(context: Context) = try {
28 | file.delete()
29 | } catch (exc: Throwable) {
30 | Logger.error(javaClass.simpleName, NA, exc) { "文件删除错误" }
31 | false
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/quickupload/src/main/java/com/dh/quickupload/tools/translationfile/SchemeHandler.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload.tools.translationfile
2 |
3 | import android.content.Context
4 | import java.io.InputStream
5 |
6 | interface SchemeHandler {
7 | /**
8 | * 使用文件路径初始化实例。
9 | */
10 | fun init(path: String)
11 |
12 | /**
13 | * 获取文件大小 (以字节为单位)。
14 | */
15 | fun size(context: Context): Long
16 |
17 | /**
18 | *获取文件输入流以读取它
19 | */
20 | fun stream(context: Context): InputStream
21 |
22 | /**
23 | * 获取文件内容类型
24 | */
25 | fun contentType(context: Context): String
26 |
27 | /**
28 | * 获取文件名
29 | */
30 | fun name(context: Context): String
31 |
32 | /**
33 | * 删除文件
34 | */
35 | fun delete(context: Context): Boolean
36 | }
37 |
--------------------------------------------------------------------------------
/quickupload/src/test/java/com/dh/quickupload/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.dh.quickupload
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 |
7 | }
8 | }
9 | dependencyResolutionManagement {
10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
11 | repositories {
12 | google()
13 | mavenCentral()
14 | maven { url 'https://jitpack.io' }
15 | }
16 | }
17 |
18 | rootProject.name = "UpDemo"
19 | include ':app'
20 | include ':quickupload'
21 |
--------------------------------------------------------------------------------