├── .gitignore
├── .idea
├── .gitignore
├── compiler.xml
├── dbnavigator.xml
├── gradle.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── misc.xml
└── vcs.xml
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── zjh
│ │ └── simpledownload
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── zjh
│ │ │ └── simpledownload
│ │ │ ├── MainActivity.kt
│ │ │ └── MyApplication.kt
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ └── ic_launcher_background.xml
│ │ ├── layout
│ │ └── activity_main.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.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-night
│ │ └── themes.xml
│ │ └── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ └── test
│ └── java
│ └── com
│ └── zjh
│ └── simpledownload
│ └── ExampleUnitTest.kt
├── build.gradle
├── download
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── zjh
│ │ └── download
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── zjh
│ │ │ └── download
│ │ │ ├── SimpleDownload.kt
│ │ │ ├── core
│ │ │ ├── DownloadConfig.kt
│ │ │ ├── DownloadDispatcher.kt
│ │ │ ├── DownloadParam.kt
│ │ │ ├── DownloadQueue.kt
│ │ │ ├── DownloadTask.kt
│ │ │ ├── Downloader.kt
│ │ │ ├── FileValidator.kt
│ │ │ ├── NormalDownloader.kt
│ │ │ ├── RangeDownloader.kt
│ │ │ ├── RangeTmpFile.kt
│ │ │ └── TaskManager.kt
│ │ │ ├── helper
│ │ │ ├── Default.kt
│ │ │ ├── Progress.kt
│ │ │ ├── Request.kt
│ │ │ ├── State.kt
│ │ │ └── StateHolder.kt
│ │ │ └── utils
│ │ │ ├── DownloadUtils.kt
│ │ │ ├── FileUtils.kt
│ │ │ ├── HttpUtils.kt
│ │ │ ├── LogUtils.kt
│ │ │ └── Utils.kt
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ └── ic_launcher_background.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.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-night
│ │ └── themes.xml
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ │ └── xml
│ │ └── network_config.xml
│ └── test
│ └── java
│ └── com
│ └── zjh
│ └── download
│ └── ExampleUnitTest.kt
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | .idea
4 | /local.properties
5 | /.idea/caches
6 | /.idea/libraries
7 | /.idea/modules.xml
8 | /.idea/workspace.xml
9 | /.idea/navEditor.xml
10 | /.idea/assetWizardSettings.xml
11 | .DS_Store
12 | /build
13 | /captures
14 | .externalNativeBuild
15 | .cxx
16 | local.properties
17 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/dbnavigator.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
363 |
364 |
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 |
385 |
386 |
387 |
388 |
389 |
390 |
391 |
392 |
393 |
394 |
395 |
396 |
397 |
398 |
399 |
400 |
401 |
402 |
403 |
404 |
405 |
406 |
407 |
408 |
409 |
410 |
411 |
412 |
413 |
414 |
415 |
416 |
417 |
418 |
419 |
420 |
421 |
422 |
423 |
424 |
425 |
426 |
427 |
428 |
429 |
430 |
431 |
432 |
433 |
434 |
435 |
436 |
437 |
438 |
439 |
440 |
441 |
442 |
443 |
444 |
445 |
446 |
447 |
448 |
449 |
450 |
451 |
452 |
453 |
454 |
455 |
456 |
457 |
458 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
20 |
21 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SimpleDownload
2 |
3 | kotlin协程+channel实现下载,支持多任务下载、断点续传
4 |
5 | ## 添加依赖库
6 |
7 | - 项目build.gradle
8 |
9 | ```
10 | allprojects {
11 | repositories {
12 | ...
13 | maven { url 'https://jitpack.io' }
14 | }
15 | }
16 | ```
17 |
18 | - 添加依赖
19 |
20 | ```
21 | dependencies {
22 | implementation 'com.github.LetMeOff:SimpleDownload:v1.0.0'
23 | }
24 | ```
25 |
26 | ## 使用
27 |
28 | 1. Application中初始化
29 |
30 | ```
31 | SimpleDownload.instance.init(this)
32 | ```
33 |
34 | 2. 简单使用
35 |
36 | ```kotlin
37 | //创建下载任务
38 | val downloadTask = scope.download(downloadUrl)
39 |
40 | //状态监听
41 | downloadTask.state().onEach {
42 | when (it) {
43 | is State.None -> logD("未开始任务")
44 | is State.Waiting -> logD("等待中")
45 | is State.Downloading -> logD("下载中")
46 | is State.Failed -> logD("下载失败")
47 | is State.Stopped -> logD("下载已暂停")
48 | is State.Succeed -> logD("下载成功")
49 | }
50 | logD("state : $it")
51 | }.launchIn(scope)
52 |
53 | //进度监听
54 | downloadTask.progress().onEach {
55 | logD("name : $saveName , progress : ${it.percentStr()}")
56 | }.launchIn(scope)
57 |
58 | //开始下载任务
59 | downloadTask.start()
60 | ```
61 |
62 | ## 创建任务
63 |
64 | - 可以在指定的协程中调用***download***方法开始任务,任务的生命周期取决于协程的生命周期。如:
65 |
66 | ```kotlin
67 | val downloadTask = lifecycleScope.download(downloadUrl)
68 | ```
69 |
70 | 此下载任务会在***Activity***销毁后结束
71 |
72 | - 默认保存名会从下载链接中获取,默认保存路径为```/data/data/包名/files```下,可以自定义
73 |
74 | ```kotlin
75 | val downloadTask = scope.download(downloadUrl, saveName, savePath)
76 | ```
77 |
78 | 或者自定义参数
79 |
80 | ```kotlin
81 | //自定义参数
82 | val params = DownloadParam(downloadUrl, saveName, savePath)
83 | val config = DownloadConfig()
84 | config.baseUrl = "http://www.example.com"
85 | val downloadTask = scope.download(params, config)
86 | ```
87 |
88 | - 下载任务默认使用下载链接作为唯一标识,可以自定义
89 |
90 | ```kotlin
91 | class CustomDownloadParam(url: String, saveName: String, savePath: String) :
92 | DownloadParam(url, saveName, savePath) {
93 | override fun tag(): String {
94 | //定义唯一标示
95 | return savePath + saveName
96 | }
97 | }
98 |
99 | //使用
100 | val customParams = CustomDownloadParam(downloadUrl, saveName, savePath)
101 | val downloadTask = scope.download(customParams)
102 | ```
103 |
104 | - 在多个页面使用同样的标识创建任务时,将会返回同一个任务,因此可以在不同界面监听同一任务的进度和状态
105 |
106 | ## 状态和进度
107 |
108 | 通过```launchIn(scope)```指定监听所在的协程,以便于销毁监听
109 |
110 | - 状态监听
111 |
112 | ```kotlin
113 | //状态监听
114 | downloadTask.state().onEach {
115 | when (it) {
116 | is State.None -> logD("未开始任务")
117 | is State.Waiting -> logD("等待中")
118 | is State.Downloading -> logD("下载中")
119 | is State.Failed -> logD("下载失败")
120 | is State.Stopped -> logD("下载已暂停")
121 | is State.Succeed -> logD("下载成功")
122 | }
123 | logD("state : $it")
124 | }.launchIn(scope)
125 | ```
126 |
127 | - 进度监听
128 |
129 | ```kotlin
130 | //进度监听
131 | downloadTask.progress().onEach {
132 | logD("name : $saveName , progress : ${it.percentStr()}")
133 | }.launchIn(scope)
134 | ```
135 |
136 | 可以自定义监听间隔时间```downloadTask.state(500)```,```downloadTask.progress(500)```,默认200ms
137 |
138 | ## 任务操作
139 |
140 | - 开始
141 |
142 | ```kotlin
143 | downloadTask.start()
144 | ```
145 |
146 | - 停止
147 |
148 | ```kotlin
149 | downloadTask.stop()
150 | ```
151 |
152 | - 删除(true : 删除文件,false : 不删除,默认false)
153 |
154 | ```kotlin
155 | downloadTask.remove()
156 | ```
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'kotlin-android'
4 | id 'kotlin-kapt'
5 | }
6 |
7 | android {
8 | compileSdk 31
9 |
10 | defaultConfig {
11 | applicationId "com.zjh.simpledownload"
12 | minSdk 21
13 | targetSdk 31
14 | versionCode 1
15 | versionName "1.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 | dataBinding = true
35 | }
36 | }
37 |
38 | dependencies {
39 |
40 | implementation 'androidx.core:core-ktx:1.3.2'
41 | implementation 'androidx.appcompat:appcompat:1.2.0'
42 | implementation 'com.google.android.material:material:1.3.0'
43 | implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
44 | testImplementation 'junit:junit:4.+'
45 | androidTestImplementation 'androidx.test.ext:junit:1.1.2'
46 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
47 |
48 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0'
49 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
50 | implementation project(':download')
51 | // implementation 'com.github.LetMeOff:SimpleDownload:v1.0.0'
52 | }
--------------------------------------------------------------------------------
/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/zjh/simpledownload/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.zjh.simpledownload
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.zjh.simpledownload", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
14 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zjh/simpledownload/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.zjh.simpledownload
2 |
3 | import android.os.Bundle
4 | import androidx.appcompat.app.AppCompatActivity
5 | import com.zjh.download.SimpleDownload
6 | import com.zjh.download.utils.download
7 | import com.zjh.download.helper.State
8 | import com.zjh.download.utils.logD
9 | import kotlinx.coroutines.CoroutineScope
10 | import kotlinx.coroutines.Dispatchers
11 | import kotlinx.coroutines.Job
12 | import kotlinx.coroutines.flow.launchIn
13 | import kotlinx.coroutines.flow.onEach
14 |
15 | class MainActivity : AppCompatActivity() {
16 |
17 | private val nameLit = listOf("新浪微博", "腾讯手机管家", "腾讯浏览器")
18 |
19 | private val urlList = listOf(
20 | "https://imtt.dd.qq.com/16891/apk/96881CC7639E84F35E86421691CBBA5D.apk?fsname=com.sina.weibo_11.1.3_4842.apk&csr=3554",
21 | "https://imtt.dd.qq.com/16891/apk/DE071539CCD23453643F24779B052788.apk?fsname=com.tencent.qqpimsecure_8.10.0_1417.apk&csr=3554",
22 | "https://imtt.dd.qq.com/16891/apk/0F9F42DB8B17D9BA27A60D8D556F936C.apk?fsname=com.tencent.mtt_11.2.1.1506_11211500.apk&csr=3554"
23 | )
24 |
25 | private val scope by lazy {
26 | CoroutineScope(Dispatchers.IO + Job())
27 | }
28 |
29 | private val savePath by lazy {
30 | SimpleDownload.instance.context.filesDir.path + "/apks"
31 | }
32 |
33 | override fun onCreate(savedInstanceState: Bundle?) {
34 | super.onCreate(savedInstanceState)
35 | setContentView(R.layout.activity_main)
36 |
37 | urlList.forEachIndexed { index, s ->
38 | download(s, nameLit[index])
39 | }
40 | }
41 |
42 | private fun download(downloadUrl: String, saveName: String) {
43 | //创建下载任务
44 | // val downloadTask = lifecycleScope.download(downloadUrl)
45 |
46 | val downloadTask = scope.download(downloadUrl)
47 |
48 | // val downloadTask = scope.download(downloadUrl, saveName, savePath)
49 |
50 | //自定义参数
51 | // val params = DownloadParam(downloadUrl, saveName, savePath)
52 | // val config = DownloadConfig()
53 | // config.baseUrl = "http://www.example.com"
54 | // val downloadTask = scope.download(params,config)
55 |
56 | //状态监听
57 | downloadTask.state().onEach {
58 | // when {
59 | // downloadTask.isStarted() -> logD("正在下载中")
60 | // downloadTask.isFailed() -> logD("下载失败")
61 | // downloadTask.isSucceed() -> logD("下载成功")
62 | // }
63 | when (it) {
64 | is State.None -> logD("未开始任务")
65 | is State.Waiting -> logD("等待中")
66 | is State.Downloading -> logD("下载中")
67 | is State.Failed -> logD("下载失败")
68 | is State.Stopped -> logD("下载已暂停")
69 | is State.Succeed -> logD("下载成功")
70 | }
71 | logD("state : $it")
72 | }.launchIn(scope)
73 |
74 | //进度监听
75 | downloadTask.progress().onEach {
76 | logD("name : $saveName , progress : ${it.percentStr()}")
77 | }.launchIn(scope)
78 |
79 | //开始下载任务
80 | downloadTask.start()
81 | }
82 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/zjh/simpledownload/MyApplication.kt:
--------------------------------------------------------------------------------
1 | package com.zjh.simpledownload
2 |
3 | import android.app.Application
4 | import com.zjh.download.SimpleDownload
5 |
6 | /**
7 | * desc :
8 | * @author zjh
9 | * on 2021/9/14
10 | */
11 | class MyApplication : Application() {
12 | override fun onCreate() {
13 | super.onCreate()
14 |
15 | SimpleDownload.instance.init(this)
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/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/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
14 |
15 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LetMeOff/SimpleDownload/9f6cb788d91ee1886cfafabc3daa6371f3d97b53/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LetMeOff/SimpleDownload/9f6cb788d91ee1886cfafabc3daa6371f3d97b53/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LetMeOff/SimpleDownload/9f6cb788d91ee1886cfafabc3daa6371f3d97b53/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LetMeOff/SimpleDownload/9f6cb788d91ee1886cfafabc3daa6371f3d97b53/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LetMeOff/SimpleDownload/9f6cb788d91ee1886cfafabc3daa6371f3d97b53/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LetMeOff/SimpleDownload/9f6cb788d91ee1886cfafabc3daa6371f3d97b53/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LetMeOff/SimpleDownload/9f6cb788d91ee1886cfafabc3daa6371f3d97b53/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LetMeOff/SimpleDownload/9f6cb788d91ee1886cfafabc3daa6371f3d97b53/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LetMeOff/SimpleDownload/9f6cb788d91ee1886cfafabc3daa6371f3d97b53/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LetMeOff/SimpleDownload/9f6cb788d91ee1886cfafabc3daa6371f3d97b53/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/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/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | SimpleDownload
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/app/src/test/java/com/zjh/simpledownload/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.zjh.simpledownload
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 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | buildscript {
3 | repositories {
4 | google()
5 | mavenCentral()
6 | }
7 | dependencies {
8 | classpath "com.android.tools.build:gradle:4.2.2"
9 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21"
10 |
11 | // NOTE: Do not place your application dependencies here; they belong
12 | // in the individual module build.gradle files
13 | }
14 | }
15 |
16 | task clean(type: Delete) {
17 | delete rootProject.buildDir
18 | }
--------------------------------------------------------------------------------
/download/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/download/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'kotlin-android'
4 | }
5 |
6 | android {
7 | compileSdk 31
8 |
9 | defaultConfig {
10 | minSdk 21
11 | targetSdk 31
12 | versionCode 1
13 | versionName "1.0"
14 |
15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
16 | }
17 |
18 | buildTypes {
19 | release {
20 | minifyEnabled false
21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
22 | }
23 | }
24 | compileOptions {
25 | sourceCompatibility JavaVersion.VERSION_1_8
26 | targetCompatibility JavaVersion.VERSION_1_8
27 | }
28 | kotlinOptions {
29 | jvmTarget = '1.8'
30 | }
31 | }
32 |
33 | dependencies {
34 |
35 | implementation 'androidx.core:core-ktx:1.3.2'
36 | implementation 'androidx.appcompat:appcompat:1.2.0'
37 | implementation 'com.google.android.material:material:1.3.0'
38 | testImplementation 'junit:junit:4.+'
39 | androidTestImplementation 'androidx.test.ext:junit:1.1.2'
40 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
41 |
42 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0'
43 | implementation 'com.squareup.okhttp3:okhttp:4.9.0'
44 | implementation 'com.squareup.retrofit2:retrofit:2.9.0'
45 | implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
46 | }
--------------------------------------------------------------------------------
/download/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
--------------------------------------------------------------------------------
/download/src/androidTest/java/com/zjh/download/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.zjh.download
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.zjh.download", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/download/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/download/src/main/java/com/zjh/download/SimpleDownload.kt:
--------------------------------------------------------------------------------
1 | package com.zjh.download
2 |
3 | import android.content.Context
4 |
5 | /**
6 | * desc : 初始化
7 | * @author zjh
8 | * on 2021/9/14
9 | */
10 | class SimpleDownload private constructor() {
11 |
12 | lateinit var context: Context
13 |
14 | fun init(context: Context) {
15 | this.context = context.applicationContext
16 | }
17 |
18 | companion object {
19 | val instance by lazy {
20 | SimpleDownload()
21 | }
22 | }
23 | }
24 |
25 |
26 |
--------------------------------------------------------------------------------
/download/src/main/java/com/zjh/download/core/DownloadConfig.kt:
--------------------------------------------------------------------------------
1 | package com.zjh.download.core
2 |
3 | import com.zjh.download.helper.DefaultHttpClientFactory
4 | import com.zjh.download.helper.HttpClientFactory
5 | import com.zjh.download.helper.apiCreator
6 | import com.zjh.download.helper.Default
7 | import okhttp3.ResponseBody
8 | import retrofit2.Response
9 |
10 | /**
11 | * desc : 下载配置
12 | * @author zjh
13 | * on 2021/8/24
14 | */
15 | class DownloadConfig(
16 | /**
17 | *下载管理
18 | */
19 | var taskManager: TaskManager = DefaultTaskManager,
20 | /**
21 | * 下载队列
22 | */
23 | var queue: DownloadQueue = DefaultDownloadQueue.get(),
24 |
25 | /**
26 | * 自定义header
27 | */
28 | var customHeader: Map = emptyMap(),
29 |
30 | /**
31 | * 下载器分发
32 | */
33 | var dispatcher: DownloadDispatcher = DefaultDownloadDispatcher,
34 |
35 | /**
36 | * 分片下载每片的大小
37 | */
38 | var rangeSize: Long = Default.DEFAULT_RANGE_SIZE,
39 |
40 | /**
41 | * 分片下载并行数量
42 | */
43 | var rangeCurrency: Int = Default.DEFAULT_RANGE_CURRENCY,
44 |
45 | /**
46 | * 文件校验
47 | */
48 | var validator: FileValidator = DefaultFileValidator,
49 |
50 | /**
51 | * http client
52 | */
53 | var httpClientFactory: HttpClientFactory = DefaultHttpClientFactory,
54 |
55 | /**
56 | * http base url
57 | */
58 | var baseUrl: String = "http://www.example.com"
59 | ) {
60 |
61 | private val api = apiCreator(httpClientFactory.create(), baseUrl)
62 |
63 | /**
64 | * download
65 | */
66 | suspend fun request(url: String, header: Map): Response {
67 | val tempHeader = mutableMapOf().also {
68 | it.putAll(customHeader)
69 | it.putAll(header)
70 | }
71 | return api.get(url, tempHeader)
72 | }
73 |
74 | }
--------------------------------------------------------------------------------
/download/src/main/java/com/zjh/download/core/DownloadDispatcher.kt:
--------------------------------------------------------------------------------
1 | package com.zjh.download.core
2 |
3 | import com.zjh.download.utils.isSupportRange
4 | import okhttp3.ResponseBody
5 | import retrofit2.Response
6 |
7 | /**
8 | * desc : 下载器分发
9 | * @author zjh
10 | * on 2021/8/24
11 | */
12 |
13 | interface DownloadDispatcher{
14 | fun dispatch(downloadTask: DownloadTask, resp: Response): Downloader
15 | }
16 |
17 | object DefaultDownloadDispatcher : DownloadDispatcher {
18 | override fun dispatch(downloadTask: DownloadTask, resp: Response): Downloader {
19 | return if (resp.isSupportRange()) {
20 | RangeDownloader(downloadTask.coroutineScope)
21 | } else {
22 | NormalDownloader(downloadTask.coroutineScope)
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/download/src/main/java/com/zjh/download/core/DownloadParam.kt:
--------------------------------------------------------------------------------
1 | package com.zjh.download.core
2 |
3 | /**
4 | * desc : 下载参数
5 | *
6 | * 自定义使用示例:
7 | *
8 | * class CustomDownloadParam(url: String, saveName: String, savePath: String) : DownloadParam(url, saveName, savePath) {
9 | * override fun tag(): String {
10 | * // 使用文件路径作为唯一标示
11 | * return savePath + saveName
12 | * }
13 | * }
14 | *
15 | * @author zjh
16 | * on 2021/8/24
17 | */
18 | open class DownloadParam(
19 | var url: String,
20 | var saveName: String = "",
21 | var savePath: String = "",
22 | ) {
23 |
24 | /**
25 | * 默认使用url作为每个下载任务唯一标识,可重写此方法自定义
26 | */
27 | open fun tag() = url
28 |
29 | override fun equals(other: Any?): Boolean {
30 | if (other == null) return false
31 | if (this === other) return true
32 |
33 | return if (other is DownloadParam) {
34 | tag() == other.tag()
35 | } else {
36 | false
37 | }
38 | }
39 |
40 | override fun hashCode(): Int {
41 | return tag().hashCode()
42 | }
43 |
44 | }
--------------------------------------------------------------------------------
/download/src/main/java/com/zjh/download/core/DownloadQueue.kt:
--------------------------------------------------------------------------------
1 | package com.zjh.download.core
2 |
3 | import com.zjh.download.helper.Default
4 | import kotlinx.coroutines.CoroutineScope
5 | import kotlinx.coroutines.Dispatchers
6 | import kotlinx.coroutines.Job
7 | import kotlinx.coroutines.channels.Channel
8 | import kotlinx.coroutines.channels.consumeEach
9 | import kotlinx.coroutines.launch
10 | import java.util.concurrent.ConcurrentHashMap
11 |
12 | /**
13 | * desc : 下载队列
14 | * @author zjh
15 | * on 2021/8/24
16 | */
17 | interface DownloadQueue {
18 | /**
19 | * 入队
20 | */
21 | suspend fun enqueue(task: DownloadTask)
22 |
23 | /**
24 | * 出队
25 | */
26 | suspend fun dequeue(task: DownloadTask)
27 | }
28 |
29 | /**
30 | * 默认下载队列
31 | */
32 | class DefaultDownloadQueue private constructor(private val maxTask: Int) : DownloadQueue {
33 |
34 | /**
35 | * 通道处理协程通信
36 | */
37 | private val channel = Channel()
38 |
39 | /**
40 | * 任务map
41 | */
42 | private val tempMap = ConcurrentHashMap()
43 |
44 | init {
45 | CoroutineScope(Dispatchers.IO + Job()).launch {
46 | //循环接收
47 | repeat(maxTask) {
48 | launch {
49 | channel.consumeEach {
50 | //接收到任务开始下载 移除任务
51 | if (tempMap[it.param.tag()] != null) {
52 | it.suspendStart()
53 | dequeue(it)
54 | }
55 | }
56 | }
57 | }
58 | }
59 | }
60 |
61 | override suspend fun enqueue(task: DownloadTask) {
62 | //加入map 并发送通道数据
63 | tempMap[task.param.tag()] = task
64 | channel.send(task)
65 | }
66 |
67 | override suspend fun dequeue(task: DownloadTask) {
68 | tempMap.remove(task.param.tag())
69 | }
70 |
71 | companion object {
72 | private val lock = Any()
73 |
74 | @Volatile
75 | private var instance: DefaultDownloadQueue? = null
76 |
77 | /**
78 | * 单例
79 | */
80 | fun get(maxTask: Int = Default.MAX_TASK_NUMBER): DefaultDownloadQueue =
81 | instance ?: synchronized(lock) {
82 | instance ?: DefaultDownloadQueue(maxTask).also { instance = it }
83 | }
84 |
85 | }
86 |
87 | }
88 |
--------------------------------------------------------------------------------
/download/src/main/java/com/zjh/download/core/DownloadTask.kt:
--------------------------------------------------------------------------------
1 | package com.zjh.download.core
2 |
3 | import com.zjh.download.helper.Progress
4 | import com.zjh.download.helper.State
5 | import com.zjh.download.helper.StateHolder
6 | import com.zjh.download.helper.Default
7 | import com.zjh.download.utils.clear
8 | import com.zjh.download.utils.closeQuietly
9 | import com.zjh.download.utils.fileName
10 | import com.zjh.download.utils.logD
11 | import kotlinx.coroutines.*
12 | import kotlinx.coroutines.flow.*
13 | import java.io.File
14 |
15 | /**
16 | * desc : 下载任务
17 | * @author zjh
18 | * on 2021/8/24
19 | */
20 | @OptIn(ObsoleteCoroutinesApi::class, FlowPreview::class, ExperimentalCoroutinesApi::class)
21 | class DownloadTask(
22 | val coroutineScope: CoroutineScope,
23 | val param: DownloadParam,
24 | val config: DownloadConfig
25 | ) {
26 |
27 | /**
28 | * 下载状态
29 | */
30 | private val stateHolder by lazy { StateHolder() }
31 |
32 | /**
33 | * 下载任务
34 | */
35 | private var downloadJob: Job? = null
36 | private var downloader: Downloader? = null
37 |
38 | /**
39 | * 校验下载任务是否正在执行
40 | */
41 | private fun checkJob() = downloadJob?.isActive == true
42 |
43 | /**
44 | * 下载状态
45 | */
46 | private val downloadStateFlow = MutableStateFlow(stateHolder.none)
47 |
48 | /**
49 | * 下载进度
50 | */
51 | private val downloadProgressFlow = MutableStateFlow(0)
52 |
53 | fun isStarted(): Boolean {
54 | return stateHolder.isStarted()
55 | }
56 |
57 | fun isFailed(): Boolean {
58 | return stateHolder.isFailed()
59 | }
60 |
61 | fun isSucceed(): Boolean {
62 | return stateHolder.isSucceed()
63 | }
64 |
65 | fun canStart(): Boolean {
66 | return stateHolder.canStart()
67 | }
68 |
69 | /**
70 | * 开始下载,添加到下载队列
71 | */
72 | fun start() {
73 | coroutineScope.launch {
74 | if (checkJob()) {
75 | return@launch
76 | }
77 | //修改状态
78 | notifyWaiting()
79 | try {
80 | //入队
81 | config.queue.enqueue(this@DownloadTask)
82 | } catch (e: Exception) {
83 | if (e !is CancellationException) {
84 | notifyFailed()
85 | }
86 | logD(e)
87 | }
88 | }
89 | }
90 |
91 | /**
92 | * 开始下载并等待下载完成,直接开始下载,不添加到下载队列
93 | */
94 | suspend fun suspendStart() {
95 | if (checkJob()) {
96 | return
97 | }
98 |
99 | downloadJob?.cancel()
100 | val errorHandler = CoroutineExceptionHandler { _, throwable ->
101 | logD(throwable.toString())
102 | if (throwable !is CancellationException) {
103 | coroutineScope.launch {
104 | notifyFailed()
105 | }
106 | }
107 | }
108 | downloadJob = coroutineScope.launch(errorHandler + Dispatchers.IO) {
109 | val response = config.request(param.url, Default.RANGE_CHECK_HEADER)
110 | try {
111 | if (!response.isSuccessful || response.body() == null) {
112 | throw RuntimeException("request failed")
113 | }
114 |
115 | if (param.saveName.isEmpty()) {
116 | //文件名
117 | param.saveName = response.fileName()
118 | }
119 | if (param.savePath.isEmpty()) {
120 | //保存路径
121 | param.savePath = Default.DEFAULT_SAVE_PATH
122 | }
123 | if (downloader == null) {
124 | downloader = config.dispatcher.dispatch(this@DownloadTask, response)
125 | }
126 |
127 | notifyStarted()
128 |
129 | val deferred =
130 | async(Dispatchers.IO) { downloader?.download(param, config, response) }
131 | deferred.await()
132 |
133 | notifySucceed()
134 | } catch (e: Exception) {
135 | if (e !is CancellationException) {
136 | notifyFailed()
137 | }
138 | logD(e.message.toString())
139 | } finally {
140 | response.closeQuietly()
141 | }
142 | }
143 | downloadJob?.join()
144 | }
145 |
146 | /**
147 | * 停止下载
148 | */
149 | fun stop() {
150 | coroutineScope.launch {
151 | if (isStarted()) {
152 | config.queue.dequeue(this@DownloadTask)
153 | downloadJob?.cancel()
154 | notifyStopped()
155 | }
156 | }
157 | }
158 |
159 | /**
160 | * 移除任务
161 | */
162 | fun remove(deleteFile: Boolean = true) {
163 | stop()
164 | config.taskManager.remove(this)
165 | if (deleteFile) {
166 | file()?.clear()
167 | }
168 | }
169 |
170 | /**
171 | * @param interval 更新进度间隔时间,单位ms
172 | */
173 | fun state(interval: Long = 200): Flow {
174 | return downloadStateFlow.combine(
175 | progress(
176 | interval,
177 | ensureLast = false
178 | )
179 | ) { l, r -> l.apply { progress = r } }
180 | }
181 |
182 | /**
183 | * @param interval 更新进度间隔时间,单位ms
184 | * @param ensureLast 能否收到最后一个进度
185 | */
186 | fun progress(interval: Long = 200, ensureLast: Boolean = true): Flow