├── .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 | 36 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 14 | 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 { 187 | return downloadProgressFlow.flatMapConcat { 188 | //确保只发送一次 189 | var hasSend = false 190 | channelFlow { 191 | while (currentCoroutineContext().isActive) { 192 | val progress = getProgress() 193 | 194 | if (hasSend && stateHolder.isEnd()) { 195 | if (!ensureLast) { 196 | break 197 | } 198 | } 199 | 200 | send(progress) 201 | hasSend = true 202 | 203 | if (progress.isComplete()) break 204 | 205 | delay(interval) 206 | } 207 | } 208 | } 209 | } 210 | 211 | /** 212 | * 获取进度 213 | */ 214 | suspend fun getProgress(): Progress { 215 | return downloader?.queryProgress() ?: Progress() 216 | } 217 | 218 | /** 219 | * 获取下载文件 220 | */ 221 | fun file(): File? { 222 | return if (param.saveName.isNotEmpty() && param.savePath.isNotEmpty()) { 223 | File(param.savePath, param.saveName) 224 | } else { 225 | null 226 | } 227 | } 228 | 229 | /** 230 | * 改变下载状态为等待 231 | */ 232 | private suspend fun notifyWaiting() { 233 | stateHolder.updateState(stateHolder.waiting, getProgress()) 234 | downloadStateFlow.value = stateHolder.currentState 235 | logD("url ${param.url} download task waiting.") 236 | } 237 | 238 | /** 239 | * 改变下载状态为失败 240 | */ 241 | private suspend fun notifyFailed() { 242 | stateHolder.updateState(stateHolder.failed, getProgress()) 243 | downloadStateFlow.value = stateHolder.currentState 244 | logD("url ${param.url} download task failed.") 245 | } 246 | 247 | /** 248 | * 改变下载状态为开始 249 | */ 250 | private suspend fun notifyStarted() { 251 | stateHolder.updateState(stateHolder.downloading, getProgress()) 252 | downloadStateFlow.value = stateHolder.currentState 253 | downloadProgressFlow.value = downloadProgressFlow.value + 1 254 | logD("url ${param.url} download task start.") 255 | } 256 | 257 | /** 258 | * 改变下载状态为成功 259 | */ 260 | private suspend fun notifySucceed() { 261 | stateHolder.updateState(stateHolder.succeed, getProgress()) 262 | downloadStateFlow.value = stateHolder.currentState 263 | logD("url ${param.url} download task succeed.") 264 | } 265 | 266 | /** 267 | * 改变下载状态为停止 268 | */ 269 | private suspend fun notifyStopped() { 270 | stateHolder.updateState(stateHolder.stopped, getProgress()) 271 | downloadStateFlow.value = stateHolder.currentState 272 | logD("url ${param.url} download task stopped.") 273 | } 274 | 275 | private fun Progress.isComplete(): Boolean { 276 | return totalSize > 0 && totalSize == downloadSize 277 | } 278 | 279 | fun getState() = stateHolder.currentState 280 | 281 | } -------------------------------------------------------------------------------- /download/src/main/java/com/zjh/download/core/Downloader.kt: -------------------------------------------------------------------------------- 1 | package com.zjh.download.core 2 | 3 | import com.zjh.download.helper.Progress 4 | import kotlinx.coroutines.* 5 | import kotlinx.coroutines.channels.SendChannel 6 | import kotlinx.coroutines.channels.actor 7 | import okhttp3.ResponseBody 8 | import retrofit2.Response 9 | import java.io.File 10 | 11 | /** 12 | * CompletableDeferred 携带回复的请求 13 | */ 14 | class QueryProgress(val completableDeferred: CompletableDeferred) 15 | 16 | interface Downloader { 17 | var actor: SendChannel 18 | 19 | suspend fun queryProgress(): Progress 20 | 21 | suspend fun download( 22 | downloadParam: DownloadParam, 23 | downloadConfig: DownloadConfig, 24 | response: Response 25 | ) 26 | } 27 | 28 | @OptIn(ObsoleteCoroutinesApi::class) 29 | abstract class BaseDownloader(protected val coroutineScope: CoroutineScope) : Downloader { 30 | protected var totalSize: Long = 0L 31 | protected var downloadSize: Long = 0L 32 | protected var isChunked: Boolean = false 33 | 34 | private val progress = Progress() 35 | 36 | /** 37 | * 定义一个sendChannel -> actor,用于发送请求,并接收一个 ReceiveChannel 回调 38 | */ 39 | override var actor = 40 | CoroutineScope(Dispatchers.IO + Job()).actor(Dispatchers.IO) { 41 | //接收消息的迭代器 循环查询所有下载任务 42 | for (each in channel) { 43 | //接收一个携带回复的请求 44 | each.completableDeferred.complete(progress.also { 45 | it.downloadSize = downloadSize 46 | it.totalSize = totalSize 47 | it.isChunked = isChunked 48 | }) 49 | } 50 | } 51 | 52 | override suspend fun queryProgress(): Progress { 53 | //发送一个请求 查询下载进度 54 | val ack = CompletableDeferred() 55 | val queryProgress = QueryProgress(ack) 56 | actor.send(queryProgress) 57 | return ack.await() 58 | } 59 | 60 | fun DownloadParam.dir(): File { 61 | return File(savePath) 62 | } 63 | 64 | fun DownloadParam.file(): File { 65 | return File(savePath, saveName) 66 | } 67 | } -------------------------------------------------------------------------------- /download/src/main/java/com/zjh/download/core/FileValidator.kt: -------------------------------------------------------------------------------- 1 | package com.zjh.download.core 2 | 3 | import com.zjh.download.utils.contentLength 4 | import okhttp3.ResponseBody 5 | import retrofit2.Response 6 | import java.io.File 7 | 8 | /** 9 | * desc : 文件校验 10 | * @author zjh 11 | * on 2021/8/24 12 | */ 13 | interface FileValidator { 14 | fun validate( 15 | file: File, 16 | param: DownloadParam, 17 | response: Response 18 | ): Boolean 19 | } 20 | 21 | object DefaultFileValidator : FileValidator { 22 | override fun validate( 23 | file: File, 24 | param: DownloadParam, 25 | response: Response 26 | ): Boolean { 27 | return file.length() == response.contentLength() 28 | } 29 | } -------------------------------------------------------------------------------- /download/src/main/java/com/zjh/download/core/NormalDownloader.kt: -------------------------------------------------------------------------------- 1 | package com.zjh.download.core 2 | 3 | import com.zjh.download.utils.* 4 | import kotlinx.coroutines.* 5 | import okhttp3.ResponseBody 6 | import okio.buffer 7 | import okio.sink 8 | import retrofit2.Response 9 | import java.io.File 10 | 11 | @OptIn(ObsoleteCoroutinesApi::class) 12 | class NormalDownloader(coroutineScope: CoroutineScope) : BaseDownloader(coroutineScope) { 13 | companion object { 14 | private const val BUFFER_SIZE = 8192L 15 | } 16 | 17 | private var alreadyDownloaded = false 18 | 19 | private lateinit var file: File 20 | private lateinit var shadowFile: File 21 | 22 | override suspend fun download( 23 | downloadParam: DownloadParam, 24 | downloadConfig: DownloadConfig, 25 | response: Response 26 | ) { 27 | try { 28 | file = downloadParam.file() 29 | shadowFile = file.shadow() 30 | 31 | val contentLength = response.contentLength() 32 | val isChunked = response.isChunked() 33 | 34 | downloadPrepare(downloadParam, contentLength) 35 | 36 | if (alreadyDownloaded) { 37 | this.downloadSize = contentLength 38 | this.totalSize = contentLength 39 | this.isChunked = isChunked 40 | } else { 41 | this.totalSize = contentLength 42 | this.downloadSize = 0 43 | this.isChunked = isChunked 44 | startDownload(response.body()!!) 45 | } 46 | } finally { 47 | response.closeQuietly() 48 | } 49 | } 50 | 51 | private fun downloadPrepare(downloadParam: DownloadParam, contentLength: Long) { 52 | //make sure dir is exists 53 | val fileDir = downloadParam.dir() 54 | if (!fileDir.exists() || !fileDir.isDirectory) { 55 | fileDir.mkdirs() 56 | } 57 | 58 | if (file.exists()) { 59 | if (file.length() == contentLength) { 60 | alreadyDownloaded = true 61 | } else { 62 | file.delete() 63 | shadowFile.recreate() 64 | } 65 | } else { 66 | shadowFile.recreate() 67 | } 68 | } 69 | 70 | private suspend fun startDownload(body: ResponseBody) = coroutineScope { 71 | val deferred = async(Dispatchers.IO) { 72 | val source = body.source() 73 | val sink = shadowFile.sink().buffer() 74 | val buffer = sink.buffer 75 | 76 | var readLen = source.read(buffer, BUFFER_SIZE) 77 | while (isActive && readLen != -1L) { 78 | downloadSize += readLen 79 | readLen = source.read(buffer, BUFFER_SIZE) 80 | } 81 | } 82 | deferred.await() 83 | 84 | if (isActive) { 85 | shadowFile.renameTo(file) 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /download/src/main/java/com/zjh/download/core/RangeDownloader.kt: -------------------------------------------------------------------------------- 1 | package com.zjh.download.core 2 | 3 | import com.zjh.download.core.Range.Companion.RANGE_SIZE 4 | import com.zjh.download.utils.* 5 | import kotlinx.coroutines.* 6 | import kotlinx.coroutines.channels.SendChannel 7 | import kotlinx.coroutines.channels.actor 8 | import kotlinx.coroutines.channels.consumeEach 9 | import okhttp3.ResponseBody 10 | import retrofit2.Response 11 | import java.io.File 12 | 13 | /** 14 | * desc : 分片下载器 15 | * @author zjh 16 | * on 2021/8/24 17 | */ 18 | @OptIn(ObsoleteCoroutinesApi::class, ExperimentalCoroutinesApi::class) 19 | class RangeDownloader(coroutineScope: CoroutineScope) : BaseDownloader(coroutineScope) { 20 | private lateinit var file: File 21 | private lateinit var shadowFile: File 22 | private lateinit var tmpFile: File 23 | private lateinit var rangeTmpFile: RangeTmpFile 24 | 25 | /** 26 | * 下载前判断 27 | */ 28 | override suspend fun download( 29 | downloadParam: DownloadParam, 30 | downloadConfig: DownloadConfig, 31 | response: Response 32 | ) { 33 | try { 34 | file = downloadParam.file() 35 | shadowFile = file.shadow() 36 | tmpFile = file.tmp() 37 | 38 | val alreadyDownloaded = checkFiles(downloadParam, downloadConfig, response) 39 | 40 | if (alreadyDownloaded) { 41 | downloadSize = response.contentLength() 42 | totalSize = response.contentLength() 43 | } else { 44 | val last = rangeTmpFile.lastProgress() 45 | downloadSize = last.downloadSize 46 | totalSize = last.totalSize 47 | startDownload(downloadParam, downloadConfig) 48 | } 49 | } finally { 50 | response.closeQuietly() 51 | } 52 | } 53 | 54 | private fun checkFiles( 55 | param: DownloadParam, 56 | config: DownloadConfig, 57 | response: Response 58 | ): Boolean { 59 | var alreadyDownloaded = false 60 | 61 | //确保目录存在 62 | val fileDir = param.dir() 63 | if (!fileDir.exists() || !fileDir.isDirectory) { 64 | fileDir.mkdirs() 65 | } 66 | 67 | val contentLength = response.contentLength() 68 | val rangeSize = config.rangeSize 69 | val totalRanges = response.calcRanges(rangeSize) 70 | 71 | if (file.exists()) { 72 | if (config.validator.validate(file, param, response)) { 73 | alreadyDownloaded = true 74 | } else { 75 | file.delete() 76 | recreateFiles(contentLength, totalRanges, rangeSize) 77 | } 78 | } else { 79 | if (shadowFile.exists() && tmpFile.exists()) { 80 | rangeTmpFile = RangeTmpFile(tmpFile) 81 | rangeTmpFile.read() 82 | 83 | if (!rangeTmpFile.isValid(contentLength, totalRanges)) { 84 | recreateFiles(contentLength, totalRanges, rangeSize) 85 | } 86 | } else { 87 | recreateFiles(contentLength, totalRanges, rangeSize) 88 | } 89 | } 90 | 91 | return alreadyDownloaded 92 | } 93 | 94 | private fun recreateFiles(contentLength: Long, totalRanges: Long, rangeSize: Long) { 95 | tmpFile.recreate() 96 | shadowFile.recreate(contentLength) 97 | rangeTmpFile = RangeTmpFile(tmpFile) 98 | rangeTmpFile.write(contentLength, totalRanges, rangeSize) 99 | } 100 | 101 | /** 102 | * 开始下载 103 | */ 104 | private suspend fun startDownload(param: DownloadParam, config: DownloadConfig) { 105 | //定义一个sendChannel 106 | val progressChannel = coroutineScope.actor { 107 | //接收ReceiveChannel 108 | channel.consumeEach { 109 | //执行操作后取消通道 110 | downloadSize += it 111 | } 112 | } 113 | 114 | rangeTmpFile.undoneRanges().parallel(max = config.rangeCurrency) { 115 | it.download(param, config, progressChannel) 116 | } 117 | 118 | //关闭通道 119 | progressChannel.close() 120 | 121 | shadowFile.renameTo(file) 122 | tmpFile.delete() 123 | } 124 | 125 | private suspend fun Range.download( 126 | param: DownloadParam, 127 | config: DownloadConfig, 128 | sendChannel: SendChannel 129 | ) = coroutineScope { 130 | val deferred = async(Dispatchers.IO) { 131 | val url = param.url 132 | val rangeHeader = mapOf("Range" to "bytes=${current}-${end}") 133 | 134 | //请求图片 135 | val response = config.request(url, rangeHeader) 136 | if (!response.isSuccessful || response.body() == null) { 137 | throw RuntimeException("Request failed!") 138 | } 139 | 140 | response.body()?.use { 141 | it.byteStream().use { source -> 142 | val tmpFileBuffer = tmpFile.mappedByteBuffer(startByte(), RANGE_SIZE) 143 | val shadowFileBuffer = shadowFile.mappedByteBuffer(current, remainSize()) 144 | 145 | val buffer = ByteArray(8192) 146 | var readLen = source.read(buffer) 147 | 148 | while (isActive && readLen != -1) { 149 | shadowFileBuffer.put(buffer, 0, readLen) 150 | current += readLen 151 | 152 | tmpFileBuffer.putLong(16, current) 153 | 154 | //send发送数据 155 | sendChannel.send(readLen) 156 | 157 | readLen = source.read(buffer) 158 | } 159 | } 160 | } 161 | } 162 | deferred.await() 163 | } 164 | } -------------------------------------------------------------------------------- /download/src/main/java/com/zjh/download/core/RangeTmpFile.kt: -------------------------------------------------------------------------------- 1 | package com.zjh.download.core 2 | 3 | import com.zjh.download.helper.Progress 4 | import okio.* 5 | import okio.ByteString.Companion.decodeHex 6 | import java.io.File 7 | 8 | class RangeTmpFile(private val tmpFile: File) { 9 | private val fileHeader = FileHeader() 10 | private val fileContent = FileContent() 11 | 12 | fun write(totalSize: Long, totalRanges: Long, rangeSize: Long) { 13 | tmpFile.sink().buffer().use { 14 | fileHeader.write(it, totalSize, totalRanges) 15 | fileContent.write(it, totalSize, totalRanges, rangeSize) 16 | } 17 | } 18 | 19 | fun read() { 20 | tmpFile.source().buffer().use { 21 | fileHeader.read(it) 22 | fileContent.read(it, fileHeader.totalRanges) 23 | } 24 | } 25 | 26 | fun isValid(totalSize: Long, totalRanges: Long): Boolean { 27 | return fileHeader.check(totalSize, totalRanges) 28 | } 29 | 30 | fun undoneRanges(): List { 31 | return fileContent.ranges.filter { !it.isComplete() } 32 | } 33 | 34 | fun lastProgress(): Progress { 35 | val totalSize = fileHeader.totalSize 36 | val downloadSize = fileContent.downloadSize() 37 | 38 | return Progress(downloadSize, totalSize) 39 | } 40 | } 41 | 42 | /** 43 | * Save tmp file base info 44 | */ 45 | private class FileHeader( 46 | var totalSize: Long = 0L, 47 | var totalRanges: Long = 0L 48 | ) { 49 | 50 | companion object { 51 | const val FILE_HEADER_MAGIC_NUMBER = "a1b2c3d4e5f6" 52 | 53 | //How to calc: ByteString.decodeHex(FILE_HEADER_MAGIC_NUMBER).size() = 6 54 | const val FILE_HEADER_MAGIC_NUMBER_SIZE = 6L 55 | 56 | //total header size 57 | const val FILE_HEADER_SIZE = FILE_HEADER_MAGIC_NUMBER_SIZE + 16L 58 | } 59 | 60 | fun write(sink: BufferedSink, totalSize: Long, totalRanges: Long) { 61 | this.totalSize = totalSize 62 | this.totalRanges = totalRanges 63 | 64 | sink.apply { 65 | write(FILE_HEADER_MAGIC_NUMBER.decodeHex()) 66 | writeLong(totalSize) 67 | writeLong(totalRanges) 68 | } 69 | } 70 | 71 | fun read(source: BufferedSource) { 72 | val header = source.readByteString(FILE_HEADER_MAGIC_NUMBER_SIZE).hex() 73 | if (header != FILE_HEADER_MAGIC_NUMBER) { 74 | throw IllegalStateException("not a tmp file") 75 | } 76 | totalSize = source.readLong() 77 | totalRanges = source.readLong() 78 | } 79 | 80 | fun check(totalSize: Long, totalRanges: Long): Boolean { 81 | return this.totalSize == totalSize && 82 | this.totalRanges == totalRanges 83 | } 84 | } 85 | 86 | /** 87 | * Save file range info 88 | */ 89 | private class FileContent { 90 | val ranges = mutableListOf() 91 | 92 | fun write( 93 | sink: BufferedSink, 94 | totalSize: Long, 95 | totalRanges: Long, 96 | rangeSize: Long 97 | ) { 98 | ranges.clear() 99 | 100 | slice(totalSize, totalRanges, rangeSize) 101 | 102 | ranges.forEach { 103 | it.write(sink) 104 | } 105 | } 106 | 107 | fun read(source: BufferedSource, totalRanges: Long) { 108 | ranges.clear() 109 | for (i in 0 until totalRanges) { 110 | ranges.add(Range().read(source)) 111 | } 112 | } 113 | 114 | fun downloadSize(): Long { 115 | var downloadSize = 0L 116 | ranges.forEach { 117 | downloadSize += it.completeSize() 118 | } 119 | return downloadSize 120 | } 121 | 122 | private fun slice(totalSize: Long, totalRanges: Long, rangeSize: Long) { 123 | var start = 0L 124 | 125 | for (i in 0 until totalRanges) { 126 | val end = if (i == totalRanges - 1) { 127 | totalSize - 1 128 | } else { 129 | start + rangeSize - 1 130 | } 131 | 132 | ranges.add(Range(i, start, start, end)) 133 | 134 | start += rangeSize 135 | } 136 | } 137 | } 138 | 139 | class Range( 140 | var index: Long = 0L, 141 | var start: Long = 0L, 142 | var current: Long = 0L, 143 | var end: Long = 0L 144 | ) { 145 | 146 | companion object { 147 | const val RANGE_SIZE = 32L //each Long is 8 bytes 148 | } 149 | 150 | fun write(sink: BufferedSink): Range { 151 | sink.apply { 152 | writeLong(index) 153 | writeLong(start) 154 | writeLong(current) 155 | writeLong(end) 156 | } 157 | return this 158 | } 159 | 160 | fun read(source: BufferedSource): Range { 161 | val buffer = Buffer() 162 | source.readFully(buffer, RANGE_SIZE) 163 | 164 | buffer.apply { 165 | index = readLong() 166 | start = readLong() 167 | current = readLong() 168 | end = readLong() 169 | } 170 | 171 | return this 172 | } 173 | 174 | fun isComplete() = (current - end) == 1L 175 | 176 | fun remainSize() = end - current + 1 177 | 178 | fun completeSize() = current - start 179 | 180 | /** 181 | * Return the starting position of the range 182 | */ 183 | fun startByte() = FileHeader.FILE_HEADER_SIZE + RANGE_SIZE * index 184 | } -------------------------------------------------------------------------------- /download/src/main/java/com/zjh/download/core/TaskManager.kt: -------------------------------------------------------------------------------- 1 | package com.zjh.download.core 2 | 3 | import java.util.concurrent.ConcurrentHashMap 4 | 5 | /** 6 | * 任务管理 7 | */ 8 | interface TaskManager { 9 | fun add(task: DownloadTask): DownloadTask 10 | 11 | fun remove(task: DownloadTask) 12 | } 13 | 14 | /** 15 | * 任务管理实现类 16 | */ 17 | object DefaultTaskManager : TaskManager { 18 | /** 19 | * 任务列表 20 | */ 21 | private val taskMap = ConcurrentHashMap() 22 | 23 | /** 24 | * 添加任务 25 | */ 26 | override fun add(task: DownloadTask): DownloadTask { 27 | if (taskMap[task.param.tag()] == null) { 28 | taskMap[task.param.tag()] = task 29 | } 30 | return taskMap[task.param.tag()]!! 31 | } 32 | 33 | /** 34 | * 移除任务 35 | */ 36 | override fun remove(task: DownloadTask) { 37 | taskMap.remove(task.param.tag()) 38 | } 39 | } -------------------------------------------------------------------------------- /download/src/main/java/com/zjh/download/helper/Default.kt: -------------------------------------------------------------------------------- 1 | package com.zjh.download.helper 2 | 3 | import com.zjh.download.SimpleDownload 4 | 5 | /** 6 | * desc : 7 | * @author zjh 8 | * on 2021/8/24 9 | */ 10 | object Default { 11 | 12 | /** 13 | * 默认保存路径 14 | */ 15 | val DEFAULT_SAVE_PATH: String = SimpleDownload.instance.context.filesDir.path 16 | 17 | /** 18 | * 同时下载的任务数量 19 | */ 20 | const val MAX_TASK_NUMBER = 3 21 | 22 | /** 23 | * 默认的Header 24 | */ 25 | val RANGE_CHECK_HEADER = mapOf("Range" to "bytes=0-") 26 | 27 | /** 28 | * 默认的分片大小 29 | */ 30 | const val DEFAULT_RANGE_SIZE = 5L * 1024 * 1024 31 | 32 | /** 33 | * 单个任务同时下载的分片数量 34 | */ 35 | const val DEFAULT_RANGE_CURRENCY = 5 36 | } -------------------------------------------------------------------------------- /download/src/main/java/com/zjh/download/helper/Progress.kt: -------------------------------------------------------------------------------- 1 | package com.zjh.download.helper 2 | 3 | import com.zjh.download.utils.formatSize 4 | import com.zjh.download.utils.ratio 5 | 6 | /** 7 | * desc : 下载进度 8 | * @author zjh 9 | * on 2021/8/24 10 | */ 11 | class Progress( 12 | /** 13 | * 已下载大小 14 | */ 15 | var downloadSize: Long = 0, 16 | /** 17 | * 总大小 18 | */ 19 | var totalSize: Long = 0, 20 | /** 21 | * 用于标识一个链接是否是分块下载, 如果该值为true, 那么totalSize为-1 22 | */ 23 | var isChunked: Boolean = false 24 | ) { 25 | /** 26 | * 返回总大小 如:10M 27 | */ 28 | fun totalSizeStr(): String { 29 | return totalSize.formatSize() 30 | } 31 | 32 | /** 33 | * 返回已下载大小 如:3M 34 | */ 35 | fun downloadSizeStr(): String { 36 | return downloadSize.formatSize() 37 | } 38 | 39 | /** 40 | * 返回百分比数字 41 | */ 42 | fun percent(): Double { 43 | if (isChunked) return 0.0 44 | return downloadSize ratio totalSize 45 | } 46 | 47 | /** 48 | * 返回百分比字符串 49 | */ 50 | fun percentStr(): String { 51 | return "${percent()}%" 52 | } 53 | } -------------------------------------------------------------------------------- /download/src/main/java/com/zjh/download/helper/Request.kt: -------------------------------------------------------------------------------- 1 | package com.zjh.download.helper 2 | 3 | import okhttp3.OkHttpClient 4 | import okhttp3.ResponseBody 5 | import retrofit2.Response 6 | import retrofit2.Retrofit 7 | import retrofit2.converter.gson.GsonConverterFactory 8 | import retrofit2.http.GET 9 | import retrofit2.http.HeaderMap 10 | import retrofit2.http.Streaming 11 | import retrofit2.http.Url 12 | import java.util.concurrent.TimeUnit 13 | 14 | interface Api { 15 | @GET 16 | @Streaming 17 | suspend fun get( 18 | @Url url: String, 19 | @HeaderMap headers: Map 20 | ): Response 21 | } 22 | 23 | interface HttpClientFactory { 24 | fun create(): OkHttpClient 25 | } 26 | 27 | object DefaultHttpClientFactory : HttpClientFactory { 28 | override fun create(): OkHttpClient { 29 | return OkHttpClient().newBuilder() 30 | .connectTimeout(15, TimeUnit.SECONDS) 31 | .readTimeout(120, TimeUnit.SECONDS) 32 | .writeTimeout(120, TimeUnit.SECONDS) 33 | .build() 34 | } 35 | } 36 | 37 | internal fun apiCreator(client: OkHttpClient, baseUrl: String): Api { 38 | val retrofit = Retrofit.Builder() 39 | .baseUrl(baseUrl) 40 | .client(client) 41 | .addConverterFactory(GsonConverterFactory.create()) 42 | .build() 43 | return retrofit.create(Api::class.java) 44 | } -------------------------------------------------------------------------------- /download/src/main/java/com/zjh/download/helper/State.kt: -------------------------------------------------------------------------------- 1 | package com.zjh.download.helper 2 | 3 | /** 4 | * desc : 当前下载状态标识 5 | * @author zjh 6 | * on 2021/8/24 7 | */ 8 | sealed class State { 9 | var progress: Progress = Progress() 10 | internal set 11 | 12 | class None : State() 13 | class Waiting : State() 14 | class Downloading : State() 15 | class Stopped : State() 16 | class Failed : State() 17 | class Succeed : State() 18 | } -------------------------------------------------------------------------------- /download/src/main/java/com/zjh/download/helper/StateHolder.kt: -------------------------------------------------------------------------------- 1 | package com.zjh.download.helper 2 | 3 | /** 4 | * desc : 当前状态 5 | * @author zjh 6 | * on 2021/8/24 7 | */ 8 | class StateHolder { 9 | val none by lazy { State.None() } 10 | val waiting by lazy { State.Waiting() } 11 | val downloading by lazy { State.Downloading() } 12 | val stopped by lazy { State.Stopped() } 13 | val failed by lazy { State.Failed() } 14 | val succeed by lazy { State.Succeed() } 15 | 16 | var currentState: State = none 17 | 18 | fun isStarted(): Boolean { 19 | return currentState is State.Waiting || currentState is State.Downloading 20 | } 21 | 22 | fun isFailed(): Boolean { 23 | return currentState is State.Failed 24 | } 25 | 26 | fun isSucceed(): Boolean { 27 | return currentState is State.Succeed 28 | } 29 | 30 | fun canStart(): Boolean { 31 | return currentState is State.None || currentState is State.Failed || currentState is State.Stopped 32 | } 33 | 34 | fun isEnd(): Boolean { 35 | return currentState is State.None || currentState is State.Waiting || currentState is State.Stopped || currentState is State.Failed || currentState is State.Succeed 36 | } 37 | 38 | /** 39 | * 更新状态 40 | */ 41 | fun updateState(new: State, progress: Progress): State { 42 | currentState = new.apply { this.progress = progress } 43 | return currentState 44 | } 45 | } -------------------------------------------------------------------------------- /download/src/main/java/com/zjh/download/utils/DownloadUtils.kt: -------------------------------------------------------------------------------- 1 | package com.zjh.download.utils 2 | 3 | import com.zjh.download.core.DownloadParam 4 | import com.zjh.download.core.DownloadTask 5 | import com.zjh.download.core.DownloadConfig 6 | import com.zjh.download.helper.Default 7 | import kotlinx.coroutines.CoroutineScope 8 | 9 | 10 | /** 11 | * 扩展下载方法 12 | */ 13 | fun CoroutineScope.download( 14 | url: String, 15 | saveName: String = "", 16 | savePath: String = Default.DEFAULT_SAVE_PATH, 17 | downloadConfig: DownloadConfig = DownloadConfig() 18 | ): DownloadTask { 19 | logD("saveName : $saveName , path : $savePath") 20 | val downloadParams = DownloadParam(url, saveName, savePath) 21 | val task = DownloadTask(this, downloadParams, downloadConfig) 22 | return downloadConfig.taskManager.add(task) 23 | } 24 | 25 | /** 26 | * 扩展下载方法(自定义下载参数) 27 | */ 28 | fun CoroutineScope.download( 29 | downloadParam: DownloadParam, 30 | downloadConfig: DownloadConfig = DownloadConfig() 31 | ): DownloadTask { 32 | val task = DownloadTask(this, downloadParam, downloadConfig) 33 | return downloadConfig.taskManager.add(task) 34 | } -------------------------------------------------------------------------------- /download/src/main/java/com/zjh/download/utils/FileUtils.kt: -------------------------------------------------------------------------------- 1 | package com.zjh.download.utils 2 | 3 | import java.io.File 4 | import java.io.RandomAccessFile 5 | import java.nio.MappedByteBuffer 6 | import java.nio.channels.FileChannel 7 | 8 | /** 9 | * desc :文件相关 10 | * @author zjh 11 | * on 2021/8/24 12 | */ 13 | 14 | fun File.shadow(): File { 15 | val shadowPath = "$canonicalPath.download" 16 | return File(shadowPath) 17 | } 18 | 19 | fun File.tmp(): File { 20 | val tmpPath = "$canonicalPath.tmp" 21 | return File(tmpPath) 22 | } 23 | 24 | fun File.recreate(length: Long = 0L) { 25 | delete() 26 | val created = createNewFile() 27 | if (created) { 28 | setLength(length) 29 | } else { 30 | throw IllegalStateException("File create failed!") 31 | } 32 | } 33 | 34 | fun File.setLength(length: Long = 0L) { 35 | RandomAccessFile(this, "rw").setLength(length) 36 | } 37 | 38 | fun File.mappedByteBuffer(position: Long, size: Long): MappedByteBuffer { 39 | val channel = channel() 40 | val map = channel.map(FileChannel.MapMode.READ_WRITE, position, size) 41 | channel.closeQuietly() 42 | return map 43 | } 44 | 45 | fun File.channel(): FileChannel { 46 | return RandomAccessFile(this, "rw").channel 47 | } 48 | 49 | fun File.clear() { 50 | val shadow = shadow() 51 | val tmp = tmp() 52 | shadow.delete() 53 | tmp.delete() 54 | delete() 55 | } -------------------------------------------------------------------------------- /download/src/main/java/com/zjh/download/utils/HttpUtils.kt: -------------------------------------------------------------------------------- 1 | package com.zjh.download.utils 2 | 3 | import okhttp3.ResponseBody 4 | import retrofit2.Response 5 | import java.io.Closeable 6 | import java.util.* 7 | import java.util.regex.Pattern 8 | 9 | /** 10 | * desc : http相关 11 | * @author zjh 12 | * on 2021/8/24 13 | */ 14 | 15 | /** 16 | * 释放 17 | */ 18 | fun Closeable.closeQuietly() { 19 | try { 20 | close() 21 | } catch (rethrown: RuntimeException) { 22 | throw rethrown 23 | } catch (_: Exception) { 24 | } 25 | } 26 | 27 | fun Response.closeQuietly() { 28 | body()?.closeQuietly() 29 | errorBody()?.closeQuietly() 30 | } 31 | 32 | /** 33 | * 请求url 34 | */ 35 | fun Response<*>.url(): String { 36 | return raw().request.url.toString() 37 | } 38 | 39 | /** 40 | * 文件名 41 | */ 42 | fun Response<*>.fileName(): String { 43 | val url = url() 44 | 45 | var fileName = contentDisposition() 46 | if (fileName.isEmpty()) { 47 | fileName = getFileNameFromUrl(url) 48 | } 49 | 50 | return fileName 51 | } 52 | 53 | private fun Response<*>.contentDisposition(): String { 54 | val contentDisposition = header("Content-Disposition").lowercase(Locale.getDefault()) 55 | 56 | if (contentDisposition.isEmpty()) { 57 | return "" 58 | } 59 | 60 | val matcher = Pattern.compile(".*filename=(.*)").matcher(contentDisposition) 61 | if (!matcher.find()) { 62 | return "" 63 | } 64 | 65 | var result = matcher.group(1) 66 | if (result.startsWith("\"")) { 67 | result = result.substring(1) 68 | } 69 | if (result.endsWith("\"")) { 70 | result = result.substring(0, result.length - 1) 71 | } 72 | 73 | result = result.replace("/", "_", false) 74 | 75 | return result 76 | } 77 | 78 | /** 79 | * 从url中获取文件名 80 | */ 81 | fun getFileNameFromUrl(url: String): String { 82 | var temp = url 83 | if (temp.isNotEmpty()) { 84 | val fragment = temp.lastIndexOf('#') 85 | if (fragment > 0) { 86 | temp = temp.substring(0, fragment) 87 | } 88 | 89 | val query = temp.lastIndexOf('?') 90 | if (query > 0) { 91 | temp = temp.substring(0, query) 92 | } 93 | 94 | val filenamePos = temp.lastIndexOf('/') 95 | val filename = if (0 <= filenamePos) temp.substring(filenamePos + 1) else temp 96 | 97 | if (filename.isNotEmpty() && Pattern.matches("[a-zA-Z_0-9.\\-()%]+", filename)) { 98 | return filename 99 | } 100 | } 101 | 102 | return "" 103 | } 104 | 105 | /** 106 | * 是否支持分片下载 107 | */ 108 | fun Response<*>.isSupportRange(): Boolean { 109 | if (code() == 206 110 | || header("Content-Range").isNotEmpty() 111 | || header("Accept-Ranges") == "bytes" 112 | ) { 113 | return true 114 | } 115 | return false 116 | } 117 | 118 | /** 119 | * contentLength 120 | */ 121 | fun Response<*>.contentLength(): Long { 122 | return header("Content-Length").toLongOrDefault(-1) 123 | } 124 | 125 | /** 126 | * 计算分片 127 | */ 128 | fun Response<*>.calcRanges(rangeSize: Long): Long { 129 | val totalSize = contentLength() 130 | val remainder = totalSize % rangeSize 131 | val result = totalSize / rangeSize 132 | 133 | return if (remainder == 0L) { 134 | result 135 | } else { 136 | result + 1 137 | } 138 | } 139 | 140 | /** 141 | * 获取header 142 | */ 143 | private fun Response<*>.header(key: String): String { 144 | val header = headers()[key] 145 | return header ?: "" 146 | } 147 | 148 | fun Response<*>.isChunked(): Boolean { 149 | return header("Transfer-Encoding") == "chunked" 150 | } -------------------------------------------------------------------------------- /download/src/main/java/com/zjh/download/utils/LogUtils.kt: -------------------------------------------------------------------------------- 1 | package com.zjh.download.utils 2 | 3 | import android.util.Log 4 | 5 | /** 6 | * desc : log 7 | * @author zjh 8 | * on 2021/8/24 9 | */ 10 | const val TAG = "SimpleDownload" 11 | 12 | private const val LOG_TYPE_i = "log_type_i" 13 | private const val LOG_TYPE_D = "log_type_d" 14 | private const val LOG_TYPE_V = "log_type_v" 15 | private const val LOG_TYPE_W = "log_type_w" 16 | private const val LOG_TYPE_E = "log_type_e" 17 | 18 | fun logI(tag: String = TAG, message: Any) { 19 | printLog(tag, LOG_TYPE_i, message.toString()) 20 | } 21 | 22 | fun logI(message: Any) { 23 | printLog(TAG, LOG_TYPE_i, message.toString()) 24 | } 25 | 26 | fun logD(tag: String = TAG, message: Any) { 27 | printLog(tag, LOG_TYPE_D, message.toString()) 28 | } 29 | 30 | fun logD(message: Any) { 31 | printLog(TAG, LOG_TYPE_D, message.toString()) 32 | } 33 | 34 | fun logV(tag: String = TAG, message: Any) { 35 | printLog(tag, LOG_TYPE_V, message.toString()) 36 | } 37 | 38 | fun logV(message: Any) { 39 | printLog(TAG, LOG_TYPE_V, message.toString()) 40 | } 41 | 42 | fun logW(tag: String = TAG, message: Any) { 43 | printLog(tag, LOG_TYPE_W, message.toString()) 44 | } 45 | 46 | fun logW(message: Any) { 47 | printLog(TAG, LOG_TYPE_W, message.toString()) 48 | } 49 | 50 | fun logE(tag: String = TAG, message: Any) { 51 | printLog(tag, LOG_TYPE_E, message.toString()) 52 | } 53 | 54 | fun logE(message: Any) { 55 | printLog(TAG, LOG_TYPE_E, message.toString()) 56 | } 57 | 58 | fun T.log(tag: String = TAG, logType: String = LOG_TYPE_E): T { 59 | if (this is Throwable) { 60 | printLog(tag, logType, message) 61 | } else { 62 | printLog(tag, logType, toString()) 63 | } 64 | return this 65 | } 66 | 67 | private fun printLog(tag: String = TAG, logType: String, message: String?) { 68 | //栈堆追踪 69 | val stackTrace = Thread.currentThread().stackTrace 70 | //这个值不一定是4 当调用时的封装层数越多,此值也会越高 71 | val index = 4 72 | //类名 73 | val className = stackTrace[index].fileName 74 | //行数 75 | val lineNumber = stackTrace[index].lineNumber 76 | //方法名 77 | val methodName = stackTrace[index].methodName 78 | //append显示数据 79 | val stringBuilder = StringBuilder() 80 | stringBuilder.append("[ (").append(className).append(":").append(lineNumber).append(")#") 81 | .append(methodName).append(" ] ") 82 | //添加log 83 | stringBuilder.append(message) 84 | 85 | //打印日志 86 | when (logType) { 87 | LOG_TYPE_i -> Log.i(tag, stringBuilder.toString()) 88 | LOG_TYPE_D -> Log.d(tag, stringBuilder.toString()) 89 | LOG_TYPE_V -> Log.v(tag, stringBuilder.toString()) 90 | LOG_TYPE_W -> Log.w(tag, stringBuilder.toString()) 91 | LOG_TYPE_E -> Log.e(tag, stringBuilder.toString()) 92 | } 93 | } -------------------------------------------------------------------------------- /download/src/main/java/com/zjh/download/utils/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.zjh.download.utils 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.os.Build 7 | import androidx.core.content.FileProvider 8 | import kotlinx.coroutines.* 9 | import kotlinx.coroutines.channels.Channel 10 | import kotlinx.coroutines.channels.consumeEach 11 | import java.io.File 12 | import java.math.BigDecimal 13 | import java.util.concurrent.atomic.AtomicInteger 14 | 15 | /** 16 | * desc : 单位转换 17 | * @author zjh 18 | * on 2021/8/24 19 | */ 20 | 21 | /** 22 | * 格式化文件大小 23 | */ 24 | fun Long.formatSize(): String { 25 | require(this >= 0) { "Size must larger than 0." } 26 | 27 | val byte = this.toDouble() 28 | val kb = byte / 1024.0 29 | val mb = byte / 1024.0 / 1024.0 30 | val gb = byte / 1024.0 / 1024.0 / 1024.0 31 | val tb = byte / 1024.0 / 1024.0 / 1024.0 / 1024.0 32 | 33 | return when { 34 | tb >= 1 -> "${tb.decimal(2)} TB" 35 | gb >= 1 -> "${gb.decimal(2)} GB" 36 | mb >= 1 -> "${mb.decimal(2)} MB" 37 | kb >= 1 -> "${kb.decimal(2)} KB" 38 | else -> "${byte.decimal(2)} B" 39 | } 40 | } 41 | 42 | /** 43 | * 保留两位小数点 44 | */ 45 | fun Double.decimal(digits: Int): Double { 46 | return this.toBigDecimal() 47 | .setScale(digits, BigDecimal.ROUND_HALF_UP) 48 | .toDouble() 49 | } 50 | 51 | /** 52 | * 百分比 53 | */ 54 | infix fun Long.ratio(bottom: Long): Double { 55 | if (bottom <= 0) { 56 | return 0.0 57 | } 58 | val result = (this * 100.0).toBigDecimal() 59 | .divide((bottom * 1.0).toBigDecimal(), 2, BigDecimal.ROUND_FLOOR) 60 | return result.toDouble() 61 | } 62 | 63 | fun String.toLongOrDefault(defaultValue: Long): Long { 64 | return try { 65 | toLong() 66 | } catch (_: NumberFormatException) { 67 | defaultValue 68 | } 69 | } 70 | 71 | suspend fun (Collection).parallel( 72 | dispatcher: CoroutineDispatcher = Dispatchers.Default, 73 | max: Int = 2, 74 | action: suspend CoroutineScope.(T) -> R 75 | ): Iterable = coroutineScope { 76 | val list = this@parallel 77 | if (list.isEmpty()) return@coroutineScope listOf() 78 | 79 | val channel = Channel() 80 | val output = Channel() 81 | 82 | val counter = AtomicInteger(0) 83 | 84 | launch { 85 | list.forEach { channel.send(it) } 86 | channel.close() 87 | } 88 | 89 | repeat(max) { 90 | launch(dispatcher) { 91 | channel.consumeEach { 92 | output.send(action(it)) 93 | val completed = counter.incrementAndGet() 94 | if (completed == list.size) { 95 | output.close() 96 | } 97 | } 98 | } 99 | } 100 | 101 | val results = mutableListOf() 102 | for (item in output) { 103 | results.add(item) 104 | } 105 | 106 | return@coroutineScope results 107 | } 108 | 109 | fun Context.installApk(file: File) { 110 | val intent = Intent(Intent.ACTION_VIEW) 111 | val authority = "$packageName.provider" 112 | val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 113 | FileProvider.getUriForFile(this, authority, file) 114 | } else { 115 | Uri.fromFile(file) 116 | } 117 | intent.setDataAndType(uri, "application/vnd.android.package-archive") 118 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 119 | intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) 120 | startActivity(intent) 121 | } -------------------------------------------------------------------------------- /download/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /download/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 | -------------------------------------------------------------------------------- /download/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /download/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /download/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LetMeOff/SimpleDownload/9f6cb788d91ee1886cfafabc3daa6371f3d97b53/download/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /download/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LetMeOff/SimpleDownload/9f6cb788d91ee1886cfafabc3daa6371f3d97b53/download/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /download/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LetMeOff/SimpleDownload/9f6cb788d91ee1886cfafabc3daa6371f3d97b53/download/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /download/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LetMeOff/SimpleDownload/9f6cb788d91ee1886cfafabc3daa6371f3d97b53/download/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /download/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LetMeOff/SimpleDownload/9f6cb788d91ee1886cfafabc3daa6371f3d97b53/download/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /download/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LetMeOff/SimpleDownload/9f6cb788d91ee1886cfafabc3daa6371f3d97b53/download/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /download/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LetMeOff/SimpleDownload/9f6cb788d91ee1886cfafabc3daa6371f3d97b53/download/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /download/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LetMeOff/SimpleDownload/9f6cb788d91ee1886cfafabc3daa6371f3d97b53/download/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /download/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LetMeOff/SimpleDownload/9f6cb788d91ee1886cfafabc3daa6371f3d97b53/download/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /download/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LetMeOff/SimpleDownload/9f6cb788d91ee1886cfafabc3daa6371f3d97b53/download/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /download/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /download/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /download/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | download 3 | -------------------------------------------------------------------------------- /download/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /download/src/main/res/xml/network_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /download/src/test/java/com/zjh/download/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.zjh.download 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 | } -------------------------------------------------------------------------------- /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 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LetMeOff/SimpleDownload/9f6cb788d91ee1886cfafabc3daa6371f3d97b53/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Sep 14 10:46:33 CST 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.2-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 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 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 3 | repositories { 4 | google() 5 | mavenCentral() 6 | jcenter() // Warning: this repository is going to shut down soon 7 | maven { url 'https://jitpack.io' } 8 | } 9 | } 10 | rootProject.name = "SimpleDownload" 11 | include ':app' 12 | include ':download' 13 | --------------------------------------------------------------------------------