├── .github └── workflows │ └── gradle.yml ├── .gitignore ├── Art └── down.gif ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── github │ │ └── why168 │ │ └── filedownloader │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── github │ │ │ └── why168 │ │ │ └── filedownloader │ │ │ ├── BaseApplication.kt │ │ │ ├── DataUtils.kt │ │ │ ├── ListViewActivity.kt │ │ │ ├── MainActivity.kt │ │ │ └── RecViewActivity.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_list_view.xml │ │ ├── activity_main.xml │ │ ├── activity_rec_view.xml │ │ └── item_down.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── github │ └── why168 │ └── filedownloader │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── multifiledownloader ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── github │ │ └── why168 │ │ └── multifiledownloader │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── github │ │ │ └── why168 │ │ │ └── multifiledownloader │ │ │ ├── Constants.kt │ │ │ ├── DownLoadBean.kt │ │ │ ├── DownLoadExecutors.kt │ │ │ ├── DownLoadService.kt │ │ │ ├── DownLoadState.kt │ │ │ ├── DownloadManager.kt │ │ │ ├── ICallback.kt │ │ │ ├── call │ │ │ ├── AsyncConnectCall.kt │ │ │ ├── AsyncDownCall.kt │ │ │ └── NickRunnable.kt │ │ │ ├── db │ │ │ ├── DBHelper.kt │ │ │ └── DataBaseUtil.kt │ │ │ ├── notify │ │ │ ├── DownFileObserver.kt │ │ │ └── DownLoadObservable.kt │ │ │ └── utlis │ │ │ ├── DownLoadConfig.kt │ │ │ └── FileUtilities.kt │ └── res │ │ ├── values │ │ └── strings.xml │ │ └── xml │ │ └── network_security_config.xml │ └── test │ └── java │ └── com │ └── github │ └── why168 │ └── multifiledownloader │ └── ExampleUnitTest.java └── settings.gradle /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Gradle 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle 3 | 4 | name: Java CI with Gradle 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Set up JDK 1.8 20 | uses: actions/setup-java@v4 21 | with: 22 | java-version: '1.8' 23 | distribution: 'adopt' 24 | - name: Grant execute permission for gradlew 25 | run: chmod +x gradlew 26 | - name: Build with Gradle 27 | run: ./gradlew build 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | # Android Studio captures folder 30 | captures/ 31 | 32 | # Intellij 33 | *.iml 34 | .idea 35 | 36 | # Keystore files 37 | *.jks -------------------------------------------------------------------------------- /Art/down.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/why168/FileDownloader/833204d1d0455ad258ba8ccb8031fd6b0e1b33f2/Art/down.gif -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Edwin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FileDownloader 介绍 2 | ### [apk最新体验包v1.0下载](https://github.com/why168/FileDownloader/releases/download/v1.0/debug.v1.0.build1.apk) 3 | 4 | * 多任务断点下载 5 | * 支持线程数控制:DownLoadConfig.getConfig().setMaxTasks(3); 6 | * 支持service后台下载 7 | * 两种布局:ListViewActivity + RecViewActivity 8 | * 使用hanlder线程切换、activity注册Observer回调刷新UI 9 | 10 | 11 | ### 文件下载流程-状态 12 | 1. 默认(点击下载) 13 | 2. 连接中 14 | 3. 下载中 15 | 4. 等待中(排队状态) 16 | 5. 下载完毕 17 | 6. 下载失败 18 | 7. 暂停 19 | 8. 删除 20 | 21 | 22 | ### 效果图 23 | ![Image of 示例](./Art/down.gif) 24 | 25 | ### 待开发任务清单及组件规划 26 | 1. 跟上潮流改写kotlin[老早前写的kotlin教程](https://github.com/why168/AndroidProjects/tree/master/KotlinLearning) 27 | 2. 优化点 28 | * 线程 + service 29 | * 数据库 30 | * UI回调方式 31 | 3. 网络请求是否改成okhttp4.0(kotlin版本)?(待定) 32 | * 最开始用的是HttpURLConnection因为想着别用使用此组件方便(解决包重复、版本冲突) 33 | 4. 有什么疑问想法直接上[Issues](https://github.com/why168/FileDownloader/issues) 34 | 35 | 36 | 37 |
38 |
39 |
40 | 41 | ## MIT License 42 | 43 | ``` 44 | MIT License 45 | 46 | Copyright (c) 2017 Edwin 47 | 48 | Permission is hereby granted, free of charge, to any person obtaining a copy 49 | of this software and associated documentation files (the "Software"), to deal 50 | in the Software without restriction, including without limitation the rights 51 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 52 | copies of the Software, and to permit persons to whom the Software is 53 | furnished to do so, subject to the following conditions: 54 | 55 | The above copyright notice and this permission notice shall be included in all 56 | copies or substantial portions of the Software. 57 | 58 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 59 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 60 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 61 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 62 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 63 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 64 | SOFTWARE. 65 | ``` 66 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | apply plugin: 'kotlin-android' 4 | 5 | apply plugin: 'kotlin-android-extensions' 6 | 7 | android { 8 | compileSdkVersion 30 9 | defaultConfig { 10 | applicationId "com.github.why168.filedownloader" 11 | minSdkVersion 19 12 | targetSdkVersion 30 13 | versionCode 1 14 | versionName "1.0" 15 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 16 | } 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 21 | } 22 | 23 | android.applicationVariants.all { variant -> 24 | variant.outputs.all { 25 | outputFileName = "${variant.baseName}.v${variant.versionName}.build${variant.versionCode}.apk" 26 | } 27 | } 28 | } 29 | 30 | compileOptions { 31 | sourceCompatibility JavaVersion.VERSION_11 32 | targetCompatibility JavaVersion.VERSION_11 33 | } 34 | } 35 | 36 | 37 | androidExtensions { 38 | experimental = true 39 | } 40 | 41 | dependencies { 42 | implementation fileTree(dir: 'libs', include: ['*.jar']) 43 | implementation project(':multifiledownloader') 44 | 45 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 46 | implementation 'com.android.support:appcompat-v7:28.0.0' 47 | implementation 'com.android.support:recyclerview-v7:28.0.0' 48 | implementation 'com.android.support.constraint:constraint-layout:2.0.4' 49 | 50 | testImplementation 'junit:junit:4.13.2' 51 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 52 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 53 | } 54 | -------------------------------------------------------------------------------- /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 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/github/why168/filedownloader/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.why168.filedownloader 2 | 3 | import android.support.test.InstrumentationRegistry 4 | import android.support.test.runner.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.getTargetContext() 22 | assertEquals("com.github.why168.filedownloader", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 18 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/why168/filedownloader/BaseApplication.kt: -------------------------------------------------------------------------------- 1 | package com.github.why168.filedownloader 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | 6 | import com.github.why168.multifiledownloader.utlis.DownLoadConfig 7 | 8 | /** 9 | * Application 10 | * 11 | * @author Edwin.Wu 12 | * @version 2016/12/28 14:49 13 | * @since JDK1.8 14 | */ 15 | class BaseApplication : Application() { 16 | 17 | override fun onCreate() { 18 | super.onCreate() 19 | // 初始化DownLoad 20 | DownLoadConfig.setMaxTasks(3) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/why168/filedownloader/DataUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.why168.filedownloader 2 | 3 | import com.github.why168.multifiledownloader.DownLoadBean 4 | import com.github.why168.multifiledownloader.utlis.FileUtilities 5 | import java.util.ArrayList 6 | 7 | /** 8 | * @author Edwin.Wu edwin.wu05@gmail.com 9 | * @version 2019-11-04 01:57 10 | * @since JDK1.8 11 | */ 12 | object DataUtils { 13 | 14 | @JvmStatic 15 | public fun mockData(collections: ArrayList) { 16 | val bean1 = DownLoadBean() 17 | bean1.appName = "爱奇艺" 18 | bean1.appIcon = 19 | "http://f.hiphotos.bdimg.com/wisegame/pic/item/1fd98d1001e93901b446c6217cec54e736d1966d.jpg" 20 | bean1.url = "http://124.192.151.146/cdn/qiyiapp/20160912/180818/ap/qiyi.196.apk" 21 | bean1.id = FileUtilities.getMd5FileName(bean1.url) 22 | 23 | val bean2 = DownLoadBean() 24 | bean2.appName = "微信" 25 | bean2.appIcon = 26 | "http://f.hiphotos.bdimg.com/wisegame/pic/item/db0e7bec54e736d17a907ba993504fc2d4626994.jpg" 27 | bean2.url = "http://dldir1.qq.com/weixin/android/weixin6325android861.apk" 28 | bean2.id = FileUtilities.getMd5FileName(bean2.url) 29 | 30 | val bean3 = DownLoadBean() 31 | bean3.appName = "淘宝" 32 | bean3.appIcon = "http://p1.qhimg.com/dr/160_160_/t01c513232212e2d915.png" 33 | bean3.url = 34 | "http://m.shouji.360tpcdn.com/160317/0a2c6811b5fc9bada8e7e082fb5a9324/com.taobao.trip_3001049.apk" 35 | bean3.id = FileUtilities.getMd5FileName(bean3.url) 36 | 37 | val bean4 = DownLoadBean() 38 | bean4.appName = "酷狗音乐" 39 | bean4.appIcon = 40 | "http://c.hiphotos.bdimg.com/wisegame/pic/item/252309f7905298226013ce57dfca7bcb0a46d406.jpg" 41 | bean4.url = 42 | "http://downmobile.kugou.com/Android/KugouPlayer/8281/KugouPlayer_219_V8.2.8.apk" 43 | bean4.id = FileUtilities.getMd5FileName(bean4.url) 44 | 45 | val bean5 = DownLoadBean() 46 | bean5.appName = "网易云音乐" 47 | bean5.appIcon = 48 | "http://d.hiphotos.bdimg.com/wisegame/pic/item/354e9258d109b3decfae38fec4bf6c81800a4c17.jpg" 49 | bean5.url = "http://s1.music.126.net/download/android/CloudMusic_official_3.7.2_150253.apk" 50 | bean5.id = FileUtilities.getMd5FileName(bean5.url) 51 | 52 | val bean6 = DownLoadBean() 53 | bean6.appName = "百度手机卫士" 54 | bean6.appIcon = 55 | "http://a.hiphotos.bdimg.com/wisegame/pic/item/6955b319ebc4b7452322b1b9c7fc1e178b8215ee.jpg" 56 | bean6.url = 57 | "http://gdown.baidu.com/data/wisegame/6c795b7a341e0c69/baidushoujiweishi_3263.apk" 58 | bean6.id = FileUtilities.getMd5FileName(bean6.url) 59 | 60 | val bean7 = DownLoadBean() 61 | bean7.appName = "语玩" 62 | bean7.appIcon = "http://www.12nav.com/interface/res/icons/yuwan.png" 63 | bean7.url = "http://125.32.30.10/Yuwan-0.6.25.0-81075.apk" 64 | bean7.id = FileUtilities.getMd5FileName(bean7.url) 65 | 66 | val bean8 = DownLoadBean() 67 | bean8.appName = "全民K歌" 68 | bean8.appIcon = 69 | "http://e.hiphotos.bdimg.com/wisegame/pic/item/db99a9014c086e0639999b2f0a087bf40ad1cba5.jpg" 70 | bean8.url = 71 | "http://d3g.qq.com/musicapp/kge/877/karaoke_3.6.8.278_android_r31018_20160725154442_release_GW_D.apk" 72 | bean8.id = FileUtilities.getMd5FileName(bean8.url) 73 | 74 | 75 | val bean9 = DownLoadBean() 76 | bean9.appName = "魔秀桌面" 77 | bean9.appIcon = 78 | "http://e.hiphotos.bdimg.com/wisegame/pic/item/db99a9014c086e0639999b2f0a087bf40ad1cba5.jpg" 79 | bean9.url = 80 | "http://211.161.126.174/imtt.dd.qq.com/16891/41C80B55FE1051D8C09D2C2B3D17F9F3.apk?mkey=5874800846b6ee89&f=8f5d&c=0&fsname=com.moxiu.launcher_5.8.5_585.apk&csr=4d5s&p=.apk" 81 | bean9.id = FileUtilities.getMd5FileName(bean9.url) 82 | 83 | collections.clear() 84 | collections.add(bean1) 85 | collections.add(bean2) 86 | collections.add(bean3) 87 | collections.add(bean4) 88 | collections.add(bean5) 89 | collections.add(bean6) 90 | collections.add(bean7) 91 | collections.add(bean8) 92 | collections.add(bean9) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/why168/filedownloader/ListViewActivity.kt: -------------------------------------------------------------------------------- 1 | package com.github.why168.filedownloader 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.Bundle 5 | import android.support.v7.app.AppCompatActivity 6 | import android.util.Log 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import android.widget.BaseAdapter 11 | 12 | import com.github.why168.multifiledownloader.DownLoadBean 13 | import com.github.why168.multifiledownloader.DownLoadState 14 | import com.github.why168.multifiledownloader.DownloadManager 15 | import com.github.why168.multifiledownloader.db.DataBaseUtil 16 | import com.github.why168.multifiledownloader.notify.DownLoadObservable 17 | import com.github.why168.multifiledownloader.utlis.FileUtilities 18 | import kotlinx.android.extensions.LayoutContainer 19 | import kotlinx.android.synthetic.main.activity_list_view.* 20 | import kotlinx.android.synthetic.main.item_down.* 21 | 22 | import java.io.File 23 | import java.util.ArrayList 24 | import java.util.Observable 25 | import java.util.Observer 26 | 27 | /** 28 | * ListViewActivity 29 | * 30 | * @author Edwin.Wu 31 | * @version 2017/6/28 16:37 32 | * @since JDK1.8 33 | */ 34 | class ListViewActivity : AppCompatActivity(), Observer { 35 | private var collections: ArrayList = arrayListOf() 36 | private var mDownloadManager: DownloadManager? = null 37 | private var adapter: ViewAdapter? = null 38 | 39 | override fun onCreate(savedInstanceState: Bundle?) { 40 | super.onCreate(savedInstanceState) 41 | setContentView(R.layout.activity_list_view) 42 | 43 | initData() 44 | 45 | adapter = ViewAdapter() 46 | listView.adapter = adapter 47 | } 48 | 49 | private fun initData() { 50 | mDownloadManager = DownloadManager.getInstance(this) 51 | DataUtils.mockData(collections) 52 | 53 | val downLoad = DataBaseUtil.getDownLoad(this) 54 | for (i in downLoad.indices) { 55 | val beanI = downLoad[i] 56 | 57 | for (j in collections.indices) { 58 | val beanJ = collections[j] 59 | if (beanI.id == beanJ.id) { 60 | collections[j] = beanI 61 | break 62 | } 63 | } 64 | } 65 | 66 | } 67 | 68 | 69 | override fun update(o: Observable, arg: Any) { 70 | if (o !is DownLoadObservable) { 71 | return 72 | } 73 | 74 | val bean = arg as DownLoadBean 75 | val index = collections.indexOf(bean) 76 | Log.d("Edwin", "index = $index bean = $bean") 77 | val downloadState = bean.downloadState 78 | 79 | if (index != -1) { 80 | if (downloadState == DownLoadState.STATE_DELETE.index) { 81 | collections.removeAt(index) 82 | try { 83 | val file = File(bean.path) 84 | val delete = file.delete() 85 | Log.d("Edwin", "删除 state = $delete") 86 | } catch (e: Exception) { 87 | e.printStackTrace() 88 | } 89 | 90 | adapter!!.notifyDataSetChanged() 91 | } else { 92 | collections[index] = bean 93 | updateItem(index, bean) 94 | } 95 | 96 | } 97 | } 98 | 99 | override fun onStart() { 100 | super.onStart() 101 | DownLoadObservable.addObserver(this) 102 | } 103 | 104 | override fun onStop() { 105 | super.onStop() 106 | DownLoadObservable.deleteObserver(this) 107 | DownLoadObservable.deleteObserver(this) 108 | } 109 | 110 | private fun updateItem(position: Int, bean: DownLoadBean) { 111 | val firstVisible = listView.firstVisiblePosition 112 | val lastVisible = listView.lastVisiblePosition 113 | if (position in firstVisible..lastVisible) { 114 | val view = listView.getChildAt(position - firstVisible) 115 | val holder = view.tag as ViewHolder 116 | when (bean.downloadState) { 117 | DownLoadState.STATE_NONE.index -> { 118 | holder.button_start.text = DownLoadState.STATE_NONE.content 119 | } 120 | DownLoadState.STATE_WAITING.index -> { 121 | holder.button_start.text = DownLoadState.STATE_WAITING.content 122 | } 123 | DownLoadState.STATE_DOWNLOADING.index -> { 124 | holder.button_start.text = DownLoadState.STATE_DOWNLOADING.content 125 | } 126 | DownLoadState.STATE_PAUSED.index -> { 127 | holder.button_start.text = DownLoadState.STATE_PAUSED.content 128 | } 129 | DownLoadState.STATE_DOWNLOADED.index -> { 130 | holder.button_start.text = DownLoadState.STATE_DOWNLOADED.content 131 | } 132 | DownLoadState.STATE_ERROR.index -> { 133 | holder.button_start.text = DownLoadState.STATE_ERROR.content 134 | } 135 | DownLoadState.STATE_CONNECTION.index -> { 136 | holder.button_start.text = DownLoadState.STATE_CONNECTION.content 137 | } 138 | } 139 | 140 | holder.button_delete.setOnClickListener { v -> mDownloadManager?.deleteTask(bean) } 141 | 142 | holder.button_start.setOnClickListener { v -> mDownloadManager?.addTask(bean) } 143 | 144 | holder.text_name.text = bean.appName 145 | holder.text_range.text = bean.isSupportRange.toString() 146 | holder.text_progress.text = 147 | FileUtilities.convertFileSize(bean.currentSize) + "/" + FileUtilities.convertFileSize( 148 | bean.totalSize 149 | ) 150 | holder.progressBar.max = bean.totalSize.toInt() 151 | holder.progressBar.progress = bean.currentSize.toInt() 152 | } 153 | 154 | } 155 | 156 | @SuppressLint("SetTextI18n") 157 | private inner class ViewAdapter : BaseAdapter() { 158 | 159 | override fun getCount(): Int { 160 | return collections.size 161 | } 162 | 163 | override fun getItem(position: Int): DownLoadBean { 164 | return collections[position] 165 | } 166 | 167 | override fun getItemId(position: Int): Long { 168 | return position.toLong() 169 | } 170 | 171 | override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { 172 | val viewHolder: ViewHolder 173 | val contentView: View 174 | if (convertView == null) { 175 | // contentView = LinearLayout.inflate(parent.context, R.layout.item_down, null) 176 | contentView = LayoutInflater.from(parent.context) 177 | .inflate(R.layout.item_down, parent, false) 178 | viewHolder = ViewHolder(contentView) 179 | contentView.tag = viewHolder 180 | } else { 181 | viewHolder = convertView.tag as ViewHolder 182 | contentView = convertView 183 | } 184 | 185 | val bean = collections[position] 186 | viewHolder.text_name.text = bean.appName 187 | 188 | when (bean.downloadState) { 189 | DownLoadState.STATE_NONE.index -> { 190 | viewHolder.button_start.text = DownLoadState.STATE_NONE.content 191 | } 192 | DownLoadState.STATE_WAITING.index -> { 193 | viewHolder.button_start.text = DownLoadState.STATE_WAITING.content 194 | } 195 | DownLoadState.STATE_DOWNLOADING.index -> { 196 | viewHolder.button_start.text = DownLoadState.STATE_DOWNLOADING.content 197 | } 198 | DownLoadState.STATE_PAUSED.index -> { 199 | viewHolder.button_start.text = DownLoadState.STATE_PAUSED.content 200 | } 201 | DownLoadState.STATE_DOWNLOADED.index -> { 202 | viewHolder.button_start.text = DownLoadState.STATE_DOWNLOADED.content 203 | } 204 | DownLoadState.STATE_ERROR.index -> { 205 | viewHolder.button_start.text = DownLoadState.STATE_ERROR.content 206 | } 207 | DownLoadState.STATE_CONNECTION.index -> { 208 | viewHolder.button_start.text = DownLoadState.STATE_CONNECTION.content 209 | } 210 | } 211 | 212 | viewHolder.button_delete.setOnClickListener { v -> 213 | mDownloadManager?.deleteTask(bean) 214 | } 215 | 216 | viewHolder.button_start.setOnClickListener { v -> 217 | mDownloadManager?.addTask(bean) 218 | } 219 | viewHolder.text_range.text = bean.isSupportRange.toString() 220 | viewHolder.text_progress.text = 221 | FileUtilities.convertFileSize(bean.currentSize) + "/" + FileUtilities.convertFileSize( 222 | bean.totalSize 223 | ) 224 | viewHolder.progressBar.max = bean.totalSize.toInt() 225 | viewHolder.progressBar.progress = bean.currentSize.toInt() 226 | 227 | return contentView 228 | } 229 | } 230 | 231 | private class ViewHolder 232 | internal constructor(override val containerView: View) : LayoutContainer 233 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/why168/filedownloader/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.github.why168.filedownloader 2 | 3 | import android.Manifest 4 | import android.content.ContextWrapper 5 | import android.content.Intent 6 | import android.content.pm.PackageManager 7 | import android.os.Build 8 | import android.os.Bundle 9 | import android.support.v4.app.ActivityCompat 10 | import android.support.v4.content.ContextCompat 11 | import android.support.v7.app.AppCompatActivity 12 | import android.view.View 13 | import android.widget.Toast 14 | import com.github.why168.multifiledownloader.Constants 15 | import com.github.why168.multifiledownloader.DownloadManager 16 | import com.github.why168.multifiledownloader.db.DBHelper 17 | import com.github.why168.multifiledownloader.notify.DownLoadObservable 18 | import com.github.why168.multifiledownloader.utlis.FileUtilities 19 | import java.io.File 20 | import java.nio.file.Files 21 | import java.nio.file.Path 22 | import java.nio.file.Paths 23 | import java.util.* 24 | 25 | 26 | /** 27 | * 多任务下载 28 | * 29 | * @author Edwin.Wu 30 | * @version 2016/12/25 15:55 31 | * @since JDK1.8 32 | */ 33 | class MainActivity : AppCompatActivity() { 34 | 35 | companion object { 36 | private const val WRITE_EXTERNAL_STORAGE_REQUEST_CODE = 1 37 | } 38 | 39 | override fun onCreate(savedInstanceState: Bundle?) { 40 | super.onCreate(savedInstanceState) 41 | setContentView(R.layout.activity_main) 42 | 43 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 44 | val permission = 45 | ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) 46 | if (permission != PackageManager.PERMISSION_GRANTED) { 47 | ActivityCompat.requestPermissions( 48 | this, 49 | arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 50 | WRITE_EXTERNAL_STORAGE_REQUEST_CODE 51 | ) 52 | } 53 | } 54 | } 55 | 56 | fun jumpList(view: View) { 57 | startActivity(Intent(this, ListViewActivity::class.java)) 58 | } 59 | 60 | fun jumpRec(view: View) { 61 | startActivity(Intent(this, RecViewActivity::class.java)) 62 | } 63 | 64 | 65 | override fun onRequestPermissionsResult( 66 | requestCode: Int, 67 | permissions: Array, 68 | grantResults: IntArray 69 | ) { 70 | super.onRequestPermissionsResult(requestCode, permissions, grantResults) 71 | 72 | if (requestCode == WRITE_EXTERNAL_STORAGE_REQUEST_CODE) { 73 | if (grantResults[0] != PackageManager.PERMISSION_GRANTED) { 74 | Toast.makeText(this, "请打开文件读写权限", Toast.LENGTH_SHORT).show() 75 | ActivityCompat.requestPermissions( 76 | this, 77 | arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 78 | WRITE_EXTERNAL_STORAGE_REQUEST_CODE 79 | ) 80 | } 81 | } 82 | } 83 | 84 | fun resetData(view: View) { 85 | DownloadManager.getInstance(this)?.stopAll() 86 | FileUtilities.clearFileDownloader() 87 | DBHelper.clearTable(this) 88 | Toast.makeText(this, "reset successful!", Toast.LENGTH_SHORT).show() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/why168/filedownloader/RecViewActivity.kt: -------------------------------------------------------------------------------- 1 | package com.github.why168.filedownloader 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.Bundle 5 | import android.support.v7.app.AppCompatActivity 6 | import android.support.v7.widget.LinearLayoutManager 7 | import android.support.v7.widget.RecyclerView 8 | import android.util.Log 9 | import android.view.LayoutInflater 10 | import android.view.View 11 | import android.view.ViewGroup 12 | 13 | import com.github.why168.multifiledownloader.DownLoadBean 14 | import com.github.why168.multifiledownloader.DownLoadState 15 | import com.github.why168.multifiledownloader.DownloadManager 16 | import com.github.why168.multifiledownloader.db.DataBaseUtil 17 | import com.github.why168.multifiledownloader.notify.DownLoadObservable 18 | import com.github.why168.multifiledownloader.utlis.FileUtilities 19 | import kotlinx.android.extensions.LayoutContainer 20 | import kotlinx.android.synthetic.main.activity_rec_view.* 21 | import kotlinx.android.synthetic.main.item_down.* 22 | 23 | import java.io.File 24 | import java.util.ArrayList 25 | import java.util.Observable 26 | import java.util.Observer 27 | 28 | /** 29 | * RecViewActivity 30 | * 31 | * @author Edwin.Wu 32 | * @version 2017/6/28 15:34 33 | * @since JDK1.8 34 | */ 35 | class RecViewActivity : AppCompatActivity(), Observer { 36 | 37 | private val collections = ArrayList() 38 | private var viewAdapter: RecyclerView.Adapter? = null 39 | private var mDownloadManager: DownloadManager? = null 40 | 41 | override fun onCreate(savedInstanceState: Bundle?) { 42 | super.onCreate(savedInstanceState) 43 | setContentView(R.layout.activity_rec_view) 44 | 45 | initData() 46 | 47 | recView.layoutManager = LinearLayoutManager(this) 48 | viewAdapter = ViewAdapter() 49 | recView.adapter = viewAdapter 50 | } 51 | 52 | private fun initData() { 53 | mDownloadManager = DownloadManager.getInstance(this) 54 | 55 | DataUtils.mockData(collections) 56 | 57 | val downLoad = DataBaseUtil.getDownLoad(this) 58 | for (i in downLoad.indices) { 59 | val beanI = downLoad[i] 60 | for (j in collections.indices) { 61 | val beanJ = collections[j] 62 | if (beanI.id == beanJ.id) { 63 | collections[j] = beanI 64 | break 65 | } 66 | } 67 | } 68 | 69 | } 70 | 71 | override fun update(o: Observable, arg: Any) { 72 | if (o !is DownLoadObservable) { 73 | return 74 | } 75 | 76 | val bean = arg as DownLoadBean 77 | val index = collections.indexOf(bean) 78 | Log.d("Edwin", "index = $index bean = $bean") 79 | val downloadState = bean.downloadState 80 | 81 | if (index != -1 && isCurrentListViewItemVisible(index)) { 82 | if (downloadState == DownLoadState.STATE_DELETE.index) { 83 | viewAdapter!!.notifyItemRemoved(index) 84 | collections.removeAt(index) 85 | if (index != collections.size) { 86 | notifyChange(bean, index) 87 | } 88 | try { 89 | val file = File(bean.path) 90 | val delete = file.delete() 91 | Log.d("Edwin", "删除 state = $delete") 92 | } catch (e: Exception) { 93 | e.printStackTrace() 94 | } 95 | 96 | } else { 97 | collections[index] = bean 98 | notifyChange(bean, index) 99 | } 100 | } 101 | } 102 | 103 | 104 | /** 105 | * 数据改变 106 | * 107 | * @param bean 108 | * @param index 109 | */ 110 | @SuppressLint("SetTextI18n") 111 | private fun notifyChange(bean: DownLoadBean, index: Int) { 112 | val holder = getViewHolder(index) 113 | 114 | when (bean.downloadState) { 115 | DownLoadState.STATE_NONE.index -> { 116 | holder.button_start.text = DownLoadState.STATE_NONE.content 117 | } 118 | DownLoadState.STATE_WAITING.index -> { 119 | holder.button_start.text = DownLoadState.STATE_WAITING.content 120 | } 121 | DownLoadState.STATE_DOWNLOADING.index -> { 122 | holder.button_start.text = DownLoadState.STATE_DOWNLOADING.content 123 | } 124 | DownLoadState.STATE_PAUSED.index -> { 125 | holder.button_start.text = DownLoadState.STATE_PAUSED.content 126 | } 127 | DownLoadState.STATE_DOWNLOADED.index -> { 128 | holder.button_start.text = DownLoadState.STATE_DOWNLOADED.content 129 | } 130 | DownLoadState.STATE_ERROR.index -> { 131 | holder.button_start.text = DownLoadState.STATE_ERROR.content 132 | } 133 | DownLoadState.STATE_CONNECTION.index -> { 134 | holder.button_start.text = DownLoadState.STATE_CONNECTION.content 135 | } 136 | } 137 | 138 | holder.button_delete.setOnClickListener { 139 | mDownloadManager?.deleteTask(bean) 140 | } 141 | 142 | holder.button_start.setOnClickListener { 143 | mDownloadManager?.addTask(bean) 144 | } 145 | 146 | holder.text_name.text = bean.appName 147 | holder.text_range.text = bean.isSupportRange.toString() 148 | holder.text_progress.text = 149 | FileUtilities.convertFileSize(bean.currentSize) + "/" + FileUtilities.convertFileSize( 150 | bean.totalSize 151 | ) 152 | holder.progressBar.max = bean.totalSize.toInt() 153 | holder.progressBar.progress = bean.currentSize.toInt() 154 | } 155 | 156 | override fun onStart() { 157 | super.onStart() 158 | DownLoadObservable.addObserver(this) 159 | } 160 | 161 | override fun onStop() { 162 | super.onStop() 163 | DownLoadObservable.deleteObserver(this) 164 | } 165 | 166 | @SuppressLint("SetTextI18n") 167 | private inner class ViewAdapter : RecyclerView.Adapter() { 168 | 169 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 170 | return ViewHolder( 171 | LayoutInflater.from(parent.context).inflate( 172 | R.layout.item_down, 173 | parent, 174 | false 175 | ) 176 | ) 177 | } 178 | 179 | override fun getItemId(position: Int): Long { 180 | return position.toLong() 181 | } 182 | 183 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 184 | val bean = collections[position] 185 | holder.text_name.text = bean.appName 186 | 187 | when (bean.downloadState) { 188 | DownLoadState.STATE_NONE.index -> { 189 | holder.button_start.text = DownLoadState.STATE_NONE.content 190 | } 191 | DownLoadState.STATE_WAITING.index -> { 192 | holder.button_start.text = DownLoadState.STATE_WAITING.content 193 | } 194 | DownLoadState.STATE_DOWNLOADING.index -> { 195 | holder.button_start.text = DownLoadState.STATE_DOWNLOADING.content 196 | } 197 | DownLoadState.STATE_PAUSED.index -> { 198 | holder.button_start.text = DownLoadState.STATE_PAUSED.content 199 | } 200 | DownLoadState.STATE_DOWNLOADED.index -> { 201 | holder.button_start.text = DownLoadState.STATE_DOWNLOADED.content 202 | } 203 | DownLoadState.STATE_ERROR.index -> { 204 | holder.button_start.text = DownLoadState.STATE_ERROR.content 205 | } 206 | DownLoadState.STATE_CONNECTION.index -> { 207 | holder.button_start.text = DownLoadState.STATE_CONNECTION.content 208 | } 209 | } 210 | 211 | holder.button_delete.setOnClickListener { 212 | mDownloadManager?.deleteTask(bean) 213 | } 214 | 215 | holder.button_start.setOnClickListener { 216 | mDownloadManager?.addTask(bean) 217 | } 218 | 219 | holder.text_range.text = bean.isSupportRange.toString() 220 | holder.text_progress.text = 221 | FileUtilities.convertFileSize(bean.currentSize) + "/" + FileUtilities.convertFileSize( 222 | bean.totalSize 223 | ) 224 | holder.progressBar.max = bean.totalSize.toInt() 225 | holder.progressBar.progress = bean.currentSize.toInt() 226 | } 227 | 228 | override fun getItemCount(): Int { 229 | return collections.size 230 | } 231 | } 232 | 233 | internal class ViewHolder(override val containerView: View) : 234 | RecyclerView.ViewHolder(containerView), LayoutContainer 235 | 236 | private fun getViewHolder(position: Int): ViewHolder { 237 | return recView.findViewHolderForLayoutPosition(position) as ViewHolder 238 | } 239 | 240 | private fun isCurrentListViewItemVisible(position: Int): Boolean { 241 | val layoutManager = recView.layoutManager as LinearLayoutManager? 242 | val first = layoutManager!!.findFirstVisibleItemPosition() 243 | val last = layoutManager.findLastVisibleItemPosition() 244 | return position in first..last 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_list_view.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | 15 | 18 | 19 | 24 | 25 | 28 | 29 | 30 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_rec_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_down.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 20 | 21 | 28 | 29 | 32 | 33 | 39 | 40 | 47 | 48 | 49 | 54 | 55 | 61 | 62 | 65 | 66 | 72 | 73 | 74 | 82 | 83 | 91 | 92 | -------------------------------------------------------------------------------- /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.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/why168/FileDownloader/833204d1d0455ad258ba8ccb8031fd6b0e1b33f2/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/why168/FileDownloader/833204d1d0455ad258ba8ccb8031fd6b0e1b33f2/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/why168/FileDownloader/833204d1d0455ad258ba8ccb8031fd6b0e1b33f2/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/why168/FileDownloader/833204d1d0455ad258ba8ccb8031fd6b0e1b33f2/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/why168/FileDownloader/833204d1d0455ad258ba8ccb8031fd6b0e1b33f2/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/why168/FileDownloader/833204d1d0455ad258ba8ccb8031fd6b0e1b33f2/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/why168/FileDownloader/833204d1d0455ad258ba8ccb8031fd6b0e1b33f2/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/why168/FileDownloader/833204d1d0455ad258ba8ccb8031fd6b0e1b33f2/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/why168/FileDownloader/833204d1d0455ad258ba8ccb8031fd6b0e1b33f2/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/why168/FileDownloader/833204d1d0455ad258ba8ccb8031fd6b0e1b33f2/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #008577 4 | #00574B 5 | #D81B60 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | FileDownloader 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/test/java/com/github/why168/filedownloader/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.why168.filedownloader 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 | } 18 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.kotlin_version = '2.0.20' 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:7.4.2' 11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 12 | 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | google() 21 | jcenter() 22 | } 23 | } 24 | 25 | task clean(type: Delete) { 26 | delete rootProject.buildDir 27 | } 28 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/why168/FileDownloader/833204d1d0455ad258ba8ccb8031fd6b0e1b33f2/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original 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 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | -------------------------------------------------------------------------------- /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 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /multifiledownloader/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /multifiledownloader/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | apply plugin: 'kotlin-android' 4 | 5 | apply plugin: 'kotlin-android-extensions' 6 | 7 | android { 8 | compileSdkVersion 30 9 | 10 | defaultConfig { 11 | minSdkVersion 19 12 | targetSdkVersion 30 13 | 14 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 15 | 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | 25 | compileOptions { 26 | sourceCompatibility JavaVersion.VERSION_11 27 | targetCompatibility JavaVersion.VERSION_11 28 | } 29 | 30 | } 31 | 32 | dependencies { 33 | implementation fileTree(dir: 'libs', include: ['*.jar']) 34 | 35 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 36 | implementation 'com.android.support:appcompat-v7:28.0.0' 37 | testImplementation 'junit:junit:4.13.2' 38 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 39 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 40 | } 41 | -------------------------------------------------------------------------------- /multifiledownloader/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 22 | -------------------------------------------------------------------------------- /multifiledownloader/src/androidTest/java/com/github/why168/multifiledownloader/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.github.why168.multifiledownloader; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.github.why168.multifiledownloader.test", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /multifiledownloader/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /multifiledownloader/src/main/java/com/github/why168/multifiledownloader/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.github.why168.multifiledownloader 2 | 3 | import android.os.Environment 4 | 5 | /** 6 | * 常量 7 | * 8 | * @author Edwin.Wu 9 | * @version 2016/12/28 11:29 10 | * @since JDK1.8 11 | */ 12 | public object Constants { 13 | 14 | /** 15 | * 数据库版本 16 | */ 17 | const val DATA_BASE_VERSION = 1 18 | 19 | /** 20 | * 数据库名字 21 | */ 22 | const val DATA_BASE_DOWN = "file_downloader.db" 23 | 24 | /** 25 | * 默认地址 26 | */ 27 | val PATH_BASE = Environment.getExternalStorageDirectory().absolutePath + "/file_downloader/" 28 | 29 | // 下载表 30 | const val TABLE_DOWN = "table_down" 31 | 32 | // 自增长id 33 | const val ID = "id" 34 | 35 | // 下载id 36 | const val DOWN_ID = "down_id" 37 | 38 | // app名字 39 | const val DOWN_NAME = "down_name" 40 | 41 | // app图片 42 | const val DOWN_ICON = "down_icon" 43 | 44 | // app存放路径 45 | const val DOWN_FILE_PATH = "down_file_path" 46 | 47 | // app下载地址 48 | const val DOWN_URL = "down_url" 49 | 50 | // 下载状态 51 | const val DOWN_STATE = "down_state" 52 | 53 | // app总大小 54 | const val DOWN_FILE_SIZE = "down_file_size" 55 | 56 | // app下载进度 57 | const val DOWN_FILE_SIZE_ING = "down_file_size_ing" 58 | 59 | // aap下载是否支持断点下载 60 | const val DOWN_SUPPORT_RANGE = "down_support_range" 61 | 62 | 63 | const val CONNECT_TIME = 30 * 1000 64 | const val READ_TIME = 30 * 1000 65 | 66 | // 下载的实体类key 67 | const val KEY_DOWNLOAD_ENTRY = "key_download_entry" 68 | 69 | // 下载操作状态key 70 | const val KEY_OPERATING_STATE = "key_operating_state" 71 | 72 | const val ACTION_DOWNLOAD_BROAD_CAST = "action_download_broad_cast" 73 | 74 | const val action = "com.github.why168.multifiledownloader.downloadservice" 75 | const val packageName = "com.github.why168.multifiledownloader" 76 | } 77 | -------------------------------------------------------------------------------- /multifiledownloader/src/main/java/com/github/why168/multifiledownloader/DownLoadBean.kt: -------------------------------------------------------------------------------- 1 | package com.github.why168.multifiledownloader 2 | 3 | import java.io.Serializable 4 | 5 | /** 6 | * 下载任务实体类 7 | * 8 | * @author Edwin.Wu 9 | * @version 2016/12/28 11:26 10 | * @since JDK1.8 11 | */ 12 | data class DownLoadBean( 13 | @JvmField 14 | var id: String = "", //app的id url-md5 15 | @JvmField 16 | var appName: String = "", //app的软件名称 17 | @JvmField 18 | var appIcon: String = "", //app的图片 19 | @JvmField 20 | var totalSize: Long = 0, //app的size 21 | @JvmField 22 | var currentSize: Long = 0, //当前的size 23 | @JvmField 24 | var downloadState: Int = DownLoadState.STATE_NONE.index, //下载的状态 25 | @JvmField 26 | var url: String = "", //下载地址 27 | @JvmField 28 | var path: String = "", //保存路径 29 | @JvmField 30 | var isSupportRange: Boolean = false //是否支持断点下载 31 | ) : Serializable, Cloneable { 32 | 33 | override fun clone(): DownLoadBean { 34 | return super.clone() as DownLoadBean 35 | } 36 | } -------------------------------------------------------------------------------- /multifiledownloader/src/main/java/com/github/why168/multifiledownloader/DownLoadExecutors.kt: -------------------------------------------------------------------------------- 1 | package com.github.why168.multifiledownloader 2 | 3 | import java.util.concurrent.SynchronousQueue 4 | import java.util.concurrent.ThreadFactory 5 | import java.util.concurrent.ThreadPoolExecutor 6 | import java.util.concurrent.TimeUnit 7 | 8 | /** 9 | * @author Edwin.Wu 10 | * @version 2017/6/28 23:46 11 | * @since JDK1.8 12 | */ 13 | class DownLoadExecutors internal constructor() : ThreadPoolExecutor( 14 | 0, Int.MAX_VALUE, 60, TimeUnit.SECONDS, SynchronousQueue(), 15 | threadFactory("downLoad executors"), 16 | AbortPolicy() 17 | ) { 18 | override fun execute(command: Runnable) { 19 | super.execute(command) 20 | } 21 | 22 | companion object { 23 | private fun threadFactory(name: String): ThreadFactory { 24 | return ThreadFactory { runnable -> 25 | val result = Thread(runnable, name) 26 | result.isDaemon = false 27 | return@ThreadFactory result 28 | } 29 | } 30 | } 31 | 32 | init { 33 | allowCoreThreadTimeOut(true) 34 | } 35 | } -------------------------------------------------------------------------------- /multifiledownloader/src/main/java/com/github/why168/multifiledownloader/DownLoadService.kt: -------------------------------------------------------------------------------- 1 | package com.github.why168.multifiledownloader 2 | 3 | import android.app.Service 4 | import android.content.Context 5 | import com.github.why168.multifiledownloader.notify.DownLoadObservable.dataChange 6 | import com.github.why168.multifiledownloader.db.DataBaseUtil.getDownLoadById 7 | import com.github.why168.multifiledownloader.db.DataBaseUtil.updateDownLoadById 8 | import com.github.why168.multifiledownloader.db.DataBaseUtil.insertDown 9 | import com.github.why168.multifiledownloader.db.DataBaseUtil.deleteDownLoadById 10 | import com.github.why168.multifiledownloader.utlis.DownLoadConfig.getMaxTasks 11 | import android.content.Intent 12 | import android.os.Handler 13 | import android.os.IBinder 14 | import com.github.why168.multifiledownloader.call.AsyncConnectCall 15 | import com.github.why168.multifiledownloader.call.AsyncDownCall 16 | import android.os.Looper 17 | import android.os.Message 18 | import android.util.Log 19 | import java.lang.Exception 20 | import java.lang.UnsupportedOperationException 21 | import java.util.concurrent.ConcurrentHashMap 22 | import java.util.concurrent.LinkedBlockingDeque 23 | 24 | class DownLoadService : Service() { 25 | private val TAG = this@DownLoadService.javaClass.name 26 | 27 | companion object { 28 | private val downLoadExecutor = DownLoadExecutors() 29 | val connectionTaskMap = ConcurrentHashMap() 30 | val downTaskMap = ConcurrentHashMap() 31 | val waitingQueue = LinkedBlockingDeque() 32 | 33 | /** 34 | * 当下载状态发送改变的时候回调 35 | */ 36 | private val handler: Handler = object : Handler(Looper.getMainLooper()) { 37 | override fun handleMessage(msg: Message) { 38 | super.handleMessage(msg) 39 | val bean = msg.obj as DownLoadBean 40 | dataChange(bean) 41 | } 42 | } 43 | 44 | /** 45 | * 当下载状态发送改变的时候调用 46 | */ 47 | private fun notifyDownloadStateChanged(context: Context, bean: DownLoadBean, state: Int) { 48 | val message = handler.obtainMessage() 49 | message.obj = bean 50 | message.what = state 51 | handler.sendMessage(message) 52 | if (state == DownLoadState.STATE_ERROR.index || state == DownLoadState.STATE_DOWNLOADING.index || state == DownLoadState.STATE_DOWNLOADED.index || state == DownLoadState.STATE_DELETE.index || state == DownLoadState.STATE_PAUSED.index) { 53 | Log.d( 54 | "Edwin", 55 | String.format( 56 | "notifyDownloadStateChanged---> id = %s , state = %d", 57 | bean.id, 58 | bean.downloadState 59 | ) 60 | ) 61 | val downLoadTask = downTaskMap[bean.id] 62 | if (downLoadTask != null) { 63 | downLoadTask.cancel() 64 | downTaskMap.remove(bean.id) 65 | } else { 66 | waitingQueue.remove(bean) 67 | } 68 | val poll = waitingQueue.poll() 69 | if (poll != null) { 70 | downNone(context, poll) 71 | } 72 | } 73 | } 74 | 75 | /** 76 | * 下载 77 | * 78 | * @param loadBean object 79 | */ 80 | fun addTask(context: Context, loadBean: DownLoadBean) { 81 | // 先判断是否有这个app的下载信息,更新信息 82 | if (getDownLoadById(context, loadBean.id) != null) { 83 | updateDownLoadById(context, loadBean) 84 | } else { 85 | // 插入数据库 86 | insertDown(context, loadBean) 87 | } 88 | val state = loadBean.downloadState 89 | if (state == DownLoadState.STATE_NONE.index) { 90 | //默认 91 | downNone(context, loadBean) 92 | } else if (state == DownLoadState.STATE_WAITING.index) { 93 | //等待中 94 | downWaiting(context, loadBean) 95 | } else if (state == DownLoadState.STATE_PAUSED.index) { 96 | //暂停 97 | downPaused(context, loadBean) 98 | } else if (state == DownLoadState.STATE_DOWNLOADING.index) { 99 | //下载中 100 | downLoading(context, loadBean) 101 | } else if (state == DownLoadState.STATE_CONNECTION.index) { 102 | //连接中 103 | downConning(loadBean) 104 | } else if (state == DownLoadState.STATE_ERROR.index) { 105 | //下载失败 106 | downError(context, loadBean) 107 | } else if (state == DownLoadState.STATE_DOWNLOADED.index) { 108 | //下载失败 109 | Log.d("Edwin", "----" + loadBean.appName + "->下载完毕") 110 | } 111 | } 112 | 113 | fun deleteTask(context: Context, item: DownLoadBean) { 114 | // 删除文件,删除数据库 115 | try { 116 | waitingQueue.remove(item) 117 | val downLoadTask = downTaskMap[item.id] 118 | if (downLoadTask != null) { 119 | downLoadTask.cancel() 120 | downTaskMap.remove(item.id) 121 | } 122 | item.downloadState = DownLoadState.STATE_DELETE.index 123 | deleteDownLoadById(context, item.id) 124 | notifyDownloadStateChanged(context, item, DownLoadState.STATE_DELETE.index) 125 | } catch (e: Exception) { 126 | e.printStackTrace() 127 | } 128 | } 129 | 130 | private fun downNone(context: Context, loadBean: DownLoadBean) { 131 | // 最最最--->先判断任务数是否 132 | if (downTaskMap.size >= getMaxTasks()) { 133 | waitingQueue.offer(loadBean) 134 | loadBean.downloadState = DownLoadState.STATE_WAITING.index 135 | // 更新数据库 136 | updateDownLoadById(context, loadBean) 137 | // 每次状态发生改变,都需要回调该方法通知所有观察者 138 | notifyDownloadStateChanged(context, loadBean, DownLoadState.STATE_WAITING.index) 139 | } else { 140 | if (loadBean.totalSize <= 0) { 141 | val connectThread = AsyncConnectCall( 142 | context, 143 | handler, 144 | connectionTaskMap, 145 | downTaskMap, 146 | downLoadExecutor, 147 | loadBean 148 | ) 149 | downLoadExecutor.execute(connectThread) 150 | } else { 151 | val downLoadTask = AsyncDownCall(context, handler, loadBean) 152 | downTaskMap[loadBean.id] = downLoadTask 153 | downLoadExecutor.execute(downLoadTask) 154 | } 155 | } 156 | } 157 | 158 | /** 159 | * 等待状态 160 | */ 161 | private fun downWaiting(context: Context, loadBean: DownLoadBean) { 162 | // 1.移出去队列 163 | waitingQueue.remove(loadBean) 164 | Log.d("Edwin", "waitingQueue size = " + waitingQueue.size) 165 | 166 | // 2.TaskMap获取线程对象,移除线程; 167 | val downLoadTask = downTaskMap[loadBean.id] 168 | if (downLoadTask != null) { 169 | downLoadTask.cancel() 170 | downTaskMap.remove(loadBean.id) 171 | } 172 | 173 | // 3.状态修改成STATE_PAUSED; 174 | loadBean.downloadState = DownLoadState.STATE_PAUSED.index 175 | 176 | // 4.更新数据库 177 | updateDownLoadById(context, loadBean) 178 | 179 | // 5.每次状态发生改变,都需要回调该方法通知所有观察者 180 | notifyDownloadStateChanged(context, loadBean, DownLoadState.STATE_PAUSED.index) 181 | } 182 | 183 | /** 184 | * 暂停状态 185 | */ 186 | private fun downPaused(context: Context, loadBean: DownLoadBean) { 187 | // 1.状态修改成STATE_WAITING; 188 | loadBean.downloadState = DownLoadState.STATE_WAITING.index 189 | downNone(context, loadBean) 190 | } 191 | 192 | /** 193 | * 下载中 194 | */ 195 | private fun downLoading(context: Context, loadBean: DownLoadBean) { 196 | notifyDownloadStateChanged(context, loadBean, DownLoadState.STATE_DOWNLOADING.index) 197 | } 198 | 199 | /** 200 | * 连接中 201 | */ 202 | private fun downConning(loadBean: DownLoadBean) { 203 | val asyncConnectCall = connectionTaskMap[loadBean.id] 204 | if (asyncConnectCall != null) { 205 | asyncConnectCall.cancel() 206 | connectionTaskMap.remove(loadBean.id) 207 | } 208 | } 209 | 210 | /** 211 | * 下载失败 212 | */ 213 | private fun downError(context: Context, loadBean: DownLoadBean) { 214 | // 1.删除本地文件文件 215 | Log.d("Edwin", "删除本地文件文件 Id = " + loadBean.id) 216 | // 2.更新数据库数据库 217 | updateDownLoadById(context, loadBean) 218 | loadBean.downloadState = DownLoadState.STATE_NONE.index 219 | 220 | // /*********以下操作与默认状态一样*********/ 221 | // // 4.状态修改成STATE_WAITING; 222 | // // 5.创建一个线程; 223 | // // 6.放入TaskMap集合; 224 | // // 7.启动执行线程execute 225 | addTask(context, loadBean) 226 | } 227 | } 228 | 229 | override fun onCreate() { 230 | super.onCreate() 231 | Log.d(TAG, "DownLoadService --- onCreate") 232 | } 233 | 234 | override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { 235 | Log.d(TAG, "onStartCommand") 236 | return super.onStartCommand(intent, flags, startId) 237 | } 238 | 239 | override fun onBind(intent: Intent): IBinder { 240 | // TODO: Return the communication channel to the service. 241 | throw UnsupportedOperationException("Not yet implemented") 242 | } 243 | 244 | override fun onDestroy() { 245 | Log.d(TAG, "onDestroy") 246 | handler.removeCallbacksAndMessages(null) 247 | super.onDestroy() 248 | } 249 | } -------------------------------------------------------------------------------- /multifiledownloader/src/main/java/com/github/why168/multifiledownloader/DownLoadState.kt: -------------------------------------------------------------------------------- 1 | package com.github.why168.multifiledownloader 2 | 3 | /** 4 | * 下载状态 5 | * 6 | * @author Edwin.Wu 7 | * @version 2016/12/28 11:27 8 | * @since JDK1.8 9 | */ 10 | enum class DownLoadState private constructor(val index: Int, val content: String) { 11 | 12 | STATE_NONE(0, "任务添加"), 13 | STATE_CONNECTION(1, "任务连接中"), 14 | STATE_DOWNLOADING(2, "下载中"), 15 | STATE_DOWNLOADED(3, "下载完毕"), 16 | STATE_ERROR(4, "下载失败"), 17 | STATE_PAUSED(5, "任务暂停"), 18 | STATE_WAITING(6, "任务排队Queue"), 19 | STATE_DELETE(7, "任务删除") 20 | } 21 | -------------------------------------------------------------------------------- /multifiledownloader/src/main/java/com/github/why168/multifiledownloader/DownloadManager.kt: -------------------------------------------------------------------------------- 1 | package com.github.why168.multifiledownloader 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import java.lang.ref.SoftReference 6 | 7 | /** 8 | * DownloadManager 9 | * 10 | * @author Edwin.Wu 11 | * @version 2017/1/16 15:27 12 | * @since JDK1.8 13 | */ 14 | class DownloadManager { 15 | 16 | private var context: Context? = null 17 | 18 | companion object { 19 | 20 | @Volatile 21 | private var instance: SoftReference? = null 22 | 23 | fun getInstance(context: Context): DownloadManager? { 24 | if (instance == null && instance?.get() == null) { 25 | synchronized(DownloadManager::class.java) { 26 | if (instance == null && instance?.get() == null) { 27 | instance = SoftReference(DownloadManager(context)) 28 | } 29 | } 30 | } 31 | return instance?.get() 32 | } 33 | } 34 | 35 | private constructor() {} 36 | 37 | private constructor(context: Context) { 38 | this.context = context 39 | context.startService(Intent(context, DownLoadService::class.java)) 40 | } 41 | 42 | fun addTask(item: DownLoadBean) { 43 | context?.let { 44 | DownLoadService.addTask(it, item) 45 | } 46 | } 47 | 48 | fun deleteTask(item: DownLoadBean) { 49 | context?.let { 50 | DownLoadService.deleteTask(it, item) 51 | } 52 | } 53 | 54 | fun stopAll() { 55 | DownLoadService.waitingQueue.clear() 56 | 57 | DownLoadService.connectionTaskMap.forEach { 58 | it.value.cancel() 59 | } 60 | 61 | DownLoadService.downTaskMap.forEach { 62 | it.value.cancel() 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /multifiledownloader/src/main/java/com/github/why168/multifiledownloader/ICallback.kt: -------------------------------------------------------------------------------- 1 | package com.github.why168.multifiledownloader 2 | 3 | /** 4 | * @author Edwin.Wu 5 | * @version 2017/6/29 11:47 6 | * @since JDK1.8 7 | */ 8 | interface ICallback { 9 | fun onConnection() 10 | fun onProgress() 11 | fun onPaused() 12 | fun onCompleted() 13 | fun onError() 14 | fun onRetry() 15 | } -------------------------------------------------------------------------------- /multifiledownloader/src/main/java/com/github/why168/multifiledownloader/call/AsyncConnectCall.kt: -------------------------------------------------------------------------------- 1 | package com.github.why168.multifiledownloader.call 2 | 3 | import com.github.why168.multifiledownloader.db.DataBaseUtil.updateDownLoadById 4 | import android.annotation.SuppressLint 5 | import android.content.Context 6 | import android.os.Handler 7 | import android.util.Log 8 | import com.github.why168.multifiledownloader.Constants 9 | import com.github.why168.multifiledownloader.DownLoadBean 10 | import com.github.why168.multifiledownloader.DownLoadState 11 | import java.io.IOException 12 | import java.net.HttpURLConnection 13 | import java.net.URL 14 | import java.text.SimpleDateFormat 15 | import java.util.* 16 | import java.util.concurrent.ConcurrentHashMap 17 | import java.util.concurrent.ExecutorService 18 | import java.util.concurrent.atomic.AtomicBoolean 19 | 20 | /** 21 | * 连接的线程 22 | * 23 | * @author Edwin.Wu 24 | * @version 2017/1/16 14:15 25 | * @since JDK1.8 26 | */ 27 | @SuppressLint("SimpleDateFormat") 28 | class AsyncConnectCall constructor( 29 | private val context: Context, private val handler: Handler, 30 | connectionTaskMap: ConcurrentHashMap, 31 | private val downTaskMap: ConcurrentHashMap, 32 | private val executorService: ExecutorService, 33 | private val bean: DownLoadBean 34 | ) : NickRunnable( 35 | format = "AndroidHttp %s", 36 | args = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS").format(Calendar.getInstance().time) 37 | ) { 38 | private val connectionTaskMap: ConcurrentHashMap 39 | private val isRunning: AtomicBoolean = AtomicBoolean(true) 40 | 41 | init { 42 | this.connectionTaskMap = connectionTaskMap 43 | this.connectionTaskMap[bean.id] = this 44 | } 45 | 46 | override fun execute() { 47 | bean.downloadState = DownLoadState.STATE_CONNECTION.index 48 | updateDownLoadById(context, bean) 49 | notifyDownloadStateChanged(bean, DownLoadState.STATE_CONNECTION.index) 50 | var connection: HttpURLConnection? = null 51 | try { 52 | connection = URL(bean.url).openConnection() as HttpURLConnection 53 | connection.requestMethod = "GET" 54 | connection.connectTimeout = Constants.CONNECT_TIME 55 | connection.readTimeout = Constants.READ_TIME 56 | val responseCode = connection.responseCode 57 | val contentLength = connection.contentLength 58 | if (responseCode == HttpURLConnection.HTTP_OK) { 59 | val ranges = connection.getHeaderField("Accept-Ranges") 60 | if ("bytes".equals(ranges, ignoreCase = true)) { 61 | bean.isSupportRange = true 62 | } 63 | bean.totalSize = (contentLength.toString() + "").toLong() 64 | } else { 65 | bean.downloadState = DownLoadState.STATE_ERROR.index 66 | } 67 | 68 | // UpdateDownLoadById(context, bean); 69 | // notifyDownloadStateChanged(bean, DownLoadState.STATE_CONNECTION.index); 70 | Log.d("Edwin", "连接成功--isSupportRange = " + bean.isSupportRange) 71 | 72 | // 开始下载咯 73 | val downLoadTask = AsyncDownCall(context, handler, bean) 74 | if (downTaskMap[bean.id] == null) { 75 | downTaskMap[bean.id] = downLoadTask 76 | executorService.execute(downLoadTask) 77 | 78 | // 移除 79 | connectionTaskMap.remove(bean.id) 80 | } 81 | } catch (e: IOException) { 82 | e.printStackTrace() 83 | isRunning.set(false) 84 | bean.downloadState = DownLoadState.STATE_ERROR.index 85 | updateDownLoadById(context, bean) 86 | notifyDownloadStateChanged(bean, DownLoadState.STATE_ERROR.index) 87 | Log.d("Edwin", "连接失败") 88 | } finally { 89 | connection?.disconnect() 90 | } 91 | } 92 | 93 | fun cancel() { 94 | isRunning.set(true) 95 | } 96 | 97 | fun isCancel(): Boolean { 98 | return isRunning.get() 99 | } 100 | 101 | /** 102 | * 当下载状态发送改变的时候调用 103 | */ 104 | private fun notifyDownloadStateChanged(bean: DownLoadBean, state: Int) { 105 | val message = handler.obtainMessage() 106 | message.obj = bean 107 | message.what = state 108 | handler.sendMessage(message) 109 | } 110 | } -------------------------------------------------------------------------------- /multifiledownloader/src/main/java/com/github/why168/multifiledownloader/call/AsyncDownCall.kt: -------------------------------------------------------------------------------- 1 | package com.github.why168.multifiledownloader.call 2 | 3 | import com.github.why168.multifiledownloader.utlis.FileUtilities.getDownloadFile 4 | import com.github.why168.multifiledownloader.db.DataBaseUtil.updateDownLoadById 5 | import android.annotation.SuppressLint 6 | import android.content.Context 7 | import android.os.Handler 8 | import android.util.Log 9 | import com.github.why168.multifiledownloader.Constants 10 | import com.github.why168.multifiledownloader.DownLoadBean 11 | import com.github.why168.multifiledownloader.DownLoadState 12 | import java.io.FileOutputStream 13 | import java.io.IOException 14 | import java.io.InputStream 15 | import java.io.RandomAccessFile 16 | import java.lang.Exception 17 | import java.net.HttpURLConnection 18 | import java.net.URL 19 | import java.text.SimpleDateFormat 20 | import java.util.* 21 | import java.util.concurrent.atomic.AtomicBoolean 22 | 23 | /** 24 | * 文件下载 25 | * 26 | * @author Edwin.Wu 27 | * @version 2017/1/16 14:17 28 | * @since JDK1.8 29 | */ 30 | class AsyncDownCall @SuppressLint("SimpleDateFormat") constructor( 31 | private val context: Context, 32 | private val handler: Handler, 33 | private val bean: DownLoadBean 34 | ) : NickRunnable( 35 | "AndroidHttp %s", SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS").format( 36 | Calendar.getInstance().time 37 | ) 38 | ) { 39 | private val isPaused: AtomicBoolean = AtomicBoolean(false) 40 | 41 | public override fun execute() { 42 | var raf: RandomAccessFile? = null 43 | var fos: FileOutputStream? = null 44 | var `is`: InputStream? = null 45 | val destFile = getDownloadFile(bean.url) 46 | 47 | bean.path = destFile.path 48 | var connection: HttpURLConnection? = null 49 | bean.downloadState = DownLoadState.STATE_DOWNLOADING.index 50 | try { 51 | connection = URL(bean.url).openConnection() as HttpURLConnection 52 | connection.requestMethod = "GET" 53 | if (bean.isSupportRange) { 54 | connection.setRequestProperty( 55 | "Range", 56 | "bytes=" + bean.currentSize + "-" + bean.totalSize 57 | ) 58 | } 59 | connection.connectTimeout = Constants.CONNECT_TIME 60 | connection.readTimeout = Constants.READ_TIME 61 | val responseCode = connection.responseCode 62 | val contentLength = connection.contentLength 63 | if (responseCode == HttpURLConnection.HTTP_PARTIAL) { 64 | Log.d("Edwin", bean.appName + " code = " + HttpURLConnection.HTTP_PARTIAL) 65 | bean.isSupportRange = true 66 | raf = RandomAccessFile(destFile, "rws") 67 | raf.seek(bean.currentSize) 68 | `is` = connection.inputStream 69 | 70 | val buffer = ByteArray(2048) 71 | var len: Int 72 | while (`is`.read(buffer).also { len = it } != -1) { 73 | if (isPaused.get()) { 74 | break 75 | } 76 | raf.write(buffer, 0, len) 77 | bean.currentSize += len.toLong() 78 | updateDownLoadById(context, bean) 79 | notifyDownloadStateChanged(bean, DownLoadState.STATE_DOWNLOADING.index) 80 | } 81 | } else if (responseCode == HttpURLConnection.HTTP_OK) { 82 | val ranges = connection.getHeaderField("Accept-Ranges") 83 | if ("bytes".equals(ranges, ignoreCase = true)) { 84 | bean.isSupportRange = true 85 | } 86 | Log.d("Edwin", bean.appName + " code = " + HttpURLConnection.HTTP_OK) 87 | bean.currentSize = 0L 88 | fos = FileOutputStream(destFile) 89 | fos.channel.force(true) // 文件数据和元数据强制写到磁盘 90 | `is` = connection.inputStream 91 | val buffer = ByteArray(2048) 92 | var len: Int 93 | while (`is`.read(buffer).also { len = it } != -1) { 94 | if (isPaused.get()) { 95 | break 96 | } 97 | fos.write(buffer, 0, len) 98 | fos.flush() 99 | fos.fd.sync() 100 | bean.currentSize += len.toLong() 101 | updateDownLoadById(context, bean) 102 | notifyDownloadStateChanged(bean, DownLoadState.STATE_DOWNLOADING.index) 103 | } 104 | } else { 105 | bean.downloadState = DownLoadState.STATE_ERROR.index 106 | updateDownLoadById(context, bean) 107 | notifyDownloadStateChanged(bean, DownLoadState.STATE_ERROR.index) 108 | } 109 | if (isPaused.get()) { 110 | bean.downloadState = DownLoadState.STATE_PAUSED.index 111 | updateDownLoadById(context, bean) 112 | notifyDownloadStateChanged(bean, DownLoadState.STATE_PAUSED.index) 113 | } 114 | } catch (e: Exception) { 115 | e.printStackTrace() 116 | bean.downloadState = DownLoadState.STATE_ERROR.index 117 | updateDownLoadById(context, bean) 118 | notifyDownloadStateChanged(bean, DownLoadState.STATE_ERROR.index) 119 | } finally { 120 | try { 121 | raf?.close() 122 | fos?.close() 123 | `is`?.close() 124 | } catch (e: IOException) { 125 | e.printStackTrace() 126 | } finally { 127 | connection?.disconnect() 128 | // 判断是否下载完成 129 | if (bean.currentSize == bean.totalSize) { 130 | bean.downloadState = DownLoadState.STATE_DOWNLOADED.index 131 | updateDownLoadById(context, bean) 132 | notifyDownloadStateChanged(bean, DownLoadState.STATE_DOWNLOADED.index) 133 | } 134 | } 135 | } 136 | } 137 | 138 | val isCanceled: Boolean 139 | get() = isPaused.get() 140 | 141 | fun cancel() { 142 | isPaused.set(true) 143 | } 144 | 145 | /** 146 | * 当下载状态发送改变的时候调用 147 | */ 148 | private fun notifyDownloadStateChanged(bean: DownLoadBean, state: Int) { 149 | val message = handler.obtainMessage() 150 | message.obj = bean 151 | message.what = state 152 | handler.sendMessage(message) 153 | } 154 | 155 | } -------------------------------------------------------------------------------- /multifiledownloader/src/main/java/com/github/why168/multifiledownloader/call/NickRunnable.kt: -------------------------------------------------------------------------------- 1 | package com.github.why168.multifiledownloader.call 2 | 3 | import android.os.Process 4 | import java.util.* 5 | 6 | /** 7 | * NickRunnable 8 | * 9 | * @author Edwin.Wu 10 | * @version 2017/6/13 16:15 11 | * @since JDK1.8 12 | */ 13 | abstract class NickRunnable internal constructor(format: String = "", args: String) : Runnable { 14 | protected val name: String 15 | 16 | init { 17 | name = String.format(Locale.US, format, args) 18 | } 19 | 20 | override fun run() { 21 | Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND) 22 | val oldName = Thread.currentThread().name 23 | Thread.currentThread().name = "$name --- $oldName" 24 | try { 25 | execute() 26 | } finally { 27 | Thread.currentThread().name = oldName 28 | } 29 | } 30 | 31 | protected abstract fun execute() 32 | 33 | } -------------------------------------------------------------------------------- /multifiledownloader/src/main/java/com/github/why168/multifiledownloader/db/DBHelper.kt: -------------------------------------------------------------------------------- 1 | package com.github.why168.multifiledownloader.db 2 | 3 | import android.database.sqlite.SQLiteOpenHelper 4 | import android.database.sqlite.SQLiteDatabase 5 | import kotlin.jvm.Volatile 6 | import com.github.why168.multifiledownloader.db.DBHelper 7 | import android.content.ContentValues 8 | import android.content.Context 9 | import android.database.Cursor 10 | import android.util.Log 11 | import com.github.why168.multifiledownloader.Constants 12 | 13 | /** 14 | * 数据库 15 | * 16 | * @author Edwin.Wu 17 | * @version 2016/12/28 14:09 18 | * @since JDK1.8 19 | */ 20 | class DBHelper private constructor(context: Context) : 21 | SQLiteOpenHelper(context, Constants.DATA_BASE_DOWN, null, Constants.DATA_BASE_VERSION) { 22 | 23 | companion object { 24 | @Volatile 25 | private var instance: DBHelper? = null 26 | 27 | @JvmStatic 28 | private fun getInstance(context: Context): SQLiteDatabase { 29 | if (instance == null) synchronized(DBHelper::class.java) { 30 | if (instance == null) instance = DBHelper(context) 31 | } 32 | return instance!!.readableDatabase 33 | } 34 | 35 | @JvmStatic 36 | fun clearTable(context: Context) { 37 | val instance = getInstance(context) 38 | instance.execSQL("delete from " + Constants.TABLE_DOWN) // 清空数据 39 | instance.execSQL("update sqlite_sequence SET seq = 0 where name = " + Constants.TABLE_DOWN); //自增长ID为0 40 | } 41 | 42 | /** 43 | * 查询数据库的指定表中的指定数据. 44 | * 45 | * @param table 表名. 46 | * @param columns 查询字段. 47 | * @param selection 条件字段. 48 | * @param selectionArgs 条件值. 49 | * @param groupBy 分组名称. 50 | * @param having 分组条件.与groupBy配合使用. 51 | * @param orderBy 排序字段. 52 | * @param limit 分页. 53 | * @return 查询结果游标 54 | */ 55 | @JvmStatic 56 | fun selectInfo( 57 | context: Context, 58 | table: String?, 59 | columns: Array?, 60 | selection: String?, 61 | selectionArgs: Array?, 62 | groupBy: String?, 63 | having: String?, 64 | orderBy: String?, 65 | limit: String? 66 | ): Cursor { 67 | // 执行查询操作 68 | return getInstance(context).query( 69 | table, 70 | columns, 71 | selection, 72 | selectionArgs, 73 | groupBy, 74 | having, 75 | orderBy, 76 | limit 77 | ) 78 | } 79 | 80 | /** 81 | * 修改数据库的指定表中的指定数据. 82 | * 83 | * @param needClose 是否需要关闭数据库连接.true为关闭,否则不关闭. 84 | * @param table 表名. 85 | * @param titles 字段名. 86 | * @param values 数据值. 87 | * @param conditions 条件字段. 88 | * @param whereValues 条件值. 89 | * @return 若传入的字段名与插入值的长度不等则返回false, 否则执行成功则返回true. 90 | */ 91 | @JvmStatic 92 | fun updateInfo( 93 | context: Context, 94 | needClose: Boolean, 95 | table: String?, 96 | titles: Array, 97 | values: Array, 98 | conditions: String?, 99 | whereValues: Array? 100 | ): Boolean { 101 | return if (titles.size != values.size) { 102 | false 103 | } else { 104 | if (getInstance(context).isOpen) { 105 | // 将插入值与对应字段放入ContentValues实例中 106 | val contentValues = ContentValues() 107 | for (i in titles.indices) { 108 | contentValues.put(titles[i], values[i]) 109 | } 110 | getInstance(context).update( 111 | table, 112 | contentValues, 113 | conditions, 114 | whereValues 115 | ) // 执行修改操作 116 | true 117 | } else { 118 | false 119 | } 120 | } 121 | } 122 | 123 | /** 124 | * 删除数据库的指定表中的指定数据. 125 | * 126 | * @param needClose 是否需要关闭数据库连接.true为关闭,否则不关闭. 127 | * @param table 表名. 128 | * @param conditions 条件字段. 129 | * @param whereValues 条件值. 130 | */ 131 | @JvmStatic 132 | fun deleteInfo( 133 | context: Context, 134 | needClose: Boolean, 135 | table: String?, 136 | conditions: String?, 137 | whereValues: Array? 138 | ) { 139 | getInstance(context).delete(table, conditions, whereValues) // 执行删除操作 140 | } 141 | 142 | /** 143 | * 向数据库的指定表中插入数据. 144 | * 145 | * @param needClose 是否需要关闭数据库连接.true为关闭,否则不关闭. 146 | * @param table 表名. 147 | * @param titles 字段名. 148 | * @param values 数据值. 149 | * @return 若传入的字段名与插入值的长度不等则返回false, 否则执行成功则返回true. 150 | */ 151 | @JvmStatic 152 | fun insertInfo( 153 | context: Context, 154 | needClose: Boolean, 155 | table: String?, 156 | titles: Array, 157 | values: Array 158 | ): Boolean { 159 | // 判断传入的字段名数量与插入数据的数量是否相等 160 | return if (titles.size != values.size) { 161 | false 162 | } else { 163 | // 将插入值与对应字段放入ContentValues实例中 164 | val contentValues = ContentValues() 165 | for (i in titles.indices) { 166 | contentValues.put(titles[i], values[i]) 167 | } 168 | getInstance(context).insert(table, null, contentValues) // 执行插入操作 169 | true 170 | } 171 | } 172 | } 173 | 174 | override fun onCreate(db: SQLiteDatabase) { 175 | createTable(db) 176 | } 177 | 178 | override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { 179 | Log.d( 180 | this@DBHelper.javaClass.toString(), 181 | "onUpgrade ----> oldVersion = $oldVersion,newVersion = $newVersion" 182 | ) 183 | } 184 | 185 | /** 186 | * 创建数据库 187 | * 188 | * @param db 189 | */ 190 | private fun createTable(db: SQLiteDatabase) { 191 | val sql = ("create table if not exists " 192 | + Constants.TABLE_DOWN + "(" // 创建下载表 193 | + Constants.ID + " integer PRIMARY KEY autoincrement, " // 自增长id. 194 | + Constants.DOWN_ID + " varchar, " // 下载id 195 | + Constants.DOWN_NAME + " varchar, " // app的名字 196 | + Constants.DOWN_ICON + " varchar, " // app的图片 197 | + Constants.DOWN_URL + " varchar, " // 广告下载的url 198 | + Constants.DOWN_FILE_PATH + " varchar, " // 文件存放路径 199 | + Constants.DOWN_STATE + " int, " // 下载状态 200 | + Constants.DOWN_FILE_SIZE + " int, " // 文件总大小 201 | + Constants.DOWN_FILE_SIZE_ING + " int, " // 下载进度 202 | + Constants.DOWN_SUPPORT_RANGE + " smallint" // 是否断点下载 0 (false) and 1 (true). 203 | + ");") 204 | db.execSQL(sql) 205 | } 206 | } -------------------------------------------------------------------------------- /multifiledownloader/src/main/java/com/github/why168/multifiledownloader/db/DataBaseUtil.kt: -------------------------------------------------------------------------------- 1 | package com.github.why168.multifiledownloader.db 2 | 3 | import android.content.Context 4 | import com.github.why168.multifiledownloader.db.DBHelper.Companion.insertInfo 5 | import com.github.why168.multifiledownloader.db.DBHelper.Companion.selectInfo 6 | import com.github.why168.multifiledownloader.db.DBHelper.Companion.updateInfo 7 | import com.github.why168.multifiledownloader.db.DBHelper.Companion.deleteInfo 8 | import kotlin.jvm.Synchronized 9 | import com.github.why168.multifiledownloader.DownLoadBean 10 | import android.text.TextUtils 11 | import com.github.why168.multifiledownloader.Constants 12 | import java.util.ArrayList 13 | 14 | /** 15 | * 数据操作工具类 16 | * 17 | * @author Edwin.Wu 18 | * @version 2016/12/28 14:47 19 | * @since JDK1.8 20 | */ 21 | object DataBaseUtil { 22 | 23 | 24 | @JvmStatic 25 | @Synchronized 26 | fun insertDown(context: Context, bean: DownLoadBean): Boolean { 27 | val table = Constants.TABLE_DOWN // 表名 28 | 29 | /** 字段名对应字段值 */ 30 | val titles = arrayOf( 31 | Constants.DOWN_ID, 32 | Constants.DOWN_NAME, 33 | Constants.DOWN_ICON, 34 | Constants.DOWN_URL, 35 | Constants.DOWN_FILE_PATH, 36 | Constants.DOWN_STATE, 37 | Constants.DOWN_FILE_SIZE, 38 | Constants.DOWN_FILE_SIZE_ING, 39 | Constants.DOWN_SUPPORT_RANGE 40 | ) 41 | 42 | /** 字段值对应字段名 */ 43 | val values = arrayOf( 44 | bean.id + "", 45 | bean.appName + "", 46 | bean.appIcon + "", 47 | bean.url + "", 48 | bean.path + "", 49 | bean.downloadState.toString() + "", 50 | bean.totalSize.toString() + "", 51 | bean.currentSize.toString() + "", 52 | if (bean.isSupportRange) 1.toString() + "" else 0.toString() + "" 53 | ) 54 | return insertInfo(context, true, table, titles, values) 55 | } 56 | 57 | /** 58 | * 根据id获取数据 59 | */ 60 | @JvmStatic 61 | @Synchronized 62 | fun getDownLoadById(context: Context, DownloadID: String?): DownLoadBean? { 63 | var bean: DownLoadBean? = null 64 | val cursor = selectInfo( 65 | context, 66 | Constants.TABLE_DOWN, 67 | arrayOf("*"), 68 | Constants.DOWN_ID + " = ? ", 69 | arrayOf(DownloadID), 70 | null, 71 | null, 72 | null, 73 | null 74 | ) 75 | if (cursor.moveToNext()) { 76 | bean = DownLoadBean() 77 | bean.id = cursor.getString(cursor.getColumnIndex(Constants.DOWN_ID)) 78 | bean.appName = cursor.getString(cursor.getColumnIndex(Constants.DOWN_NAME)) 79 | bean.appIcon = cursor.getString(cursor.getColumnIndex(Constants.DOWN_ICON)) 80 | bean.url = cursor.getString(cursor.getColumnIndex(Constants.DOWN_URL)) 81 | bean.path = cursor.getString(cursor.getColumnIndex(Constants.DOWN_FILE_PATH)) 82 | bean.downloadState = cursor.getInt(cursor.getColumnIndex(Constants.DOWN_STATE)) 83 | bean.totalSize = cursor.getLong(cursor.getColumnIndex(Constants.DOWN_FILE_SIZE)) 84 | bean.currentSize = cursor.getLong(cursor.getColumnIndex(Constants.DOWN_FILE_SIZE_ING)) 85 | bean.isSupportRange = 86 | cursor.getLong(cursor.getColumnIndex(Constants.DOWN_SUPPORT_RANGE)) == 1L 87 | } 88 | cursor.close() 89 | return bean 90 | } 91 | 92 | /** 93 | * 获取所有数据 94 | */ 95 | @JvmStatic 96 | @Synchronized 97 | fun getDownLoad(context: Context): ArrayList { 98 | val list = ArrayList() 99 | val cursor = selectInfo( 100 | context, 101 | Constants.TABLE_DOWN, 102 | arrayOf("*"), 103 | null, 104 | null, 105 | null, 106 | null, 107 | null, 108 | null 109 | ) 110 | while (cursor.moveToNext()) { 111 | val bean = DownLoadBean() 112 | bean.id = cursor.getString(cursor.getColumnIndex(Constants.DOWN_ID)) 113 | bean.appName = cursor.getString(cursor.getColumnIndex(Constants.DOWN_NAME)) 114 | bean.appIcon = cursor.getString(cursor.getColumnIndex(Constants.DOWN_ICON)) 115 | bean.totalSize = cursor.getLong(cursor.getColumnIndex(Constants.DOWN_FILE_SIZE)) 116 | bean.currentSize = cursor.getLong(cursor.getColumnIndex(Constants.DOWN_FILE_SIZE_ING)) 117 | bean.downloadState = cursor.getInt(cursor.getColumnIndex(Constants.DOWN_STATE)) 118 | bean.url = cursor.getString(cursor.getColumnIndex(Constants.DOWN_URL)) 119 | bean.path = cursor.getString(cursor.getColumnIndex(Constants.DOWN_FILE_PATH)) 120 | bean.isSupportRange = 121 | cursor.getLong(cursor.getColumnIndex(Constants.DOWN_SUPPORT_RANGE)) == 1L 122 | list.add(bean) 123 | } 124 | cursor.close() 125 | return list 126 | } 127 | 128 | /** 129 | * 修改下载数据库 130 | */ 131 | @JvmStatic 132 | @Synchronized 133 | fun updateDownLoadById(context: Context, bean: DownLoadBean) { 134 | /** 字段名对应字段值 */ 135 | val titles = arrayOf( 136 | Constants.DOWN_ID, 137 | Constants.DOWN_NAME, 138 | Constants.DOWN_ICON, 139 | Constants.DOWN_URL, 140 | Constants.DOWN_FILE_PATH, 141 | Constants.DOWN_STATE, 142 | Constants.DOWN_FILE_SIZE, 143 | Constants.DOWN_FILE_SIZE_ING, 144 | Constants.DOWN_SUPPORT_RANGE 145 | ) 146 | 147 | /** 字段值对应字段名 */ 148 | val values = arrayOf( 149 | bean.id + "", 150 | bean.appName + "", 151 | bean.appIcon + "", 152 | bean.url + "", 153 | bean.path + "", 154 | bean.downloadState.toString() + "", 155 | bean.totalSize.toString() + "", 156 | bean.currentSize.toString() + "", 157 | if (bean.isSupportRange) 1.toString() + "" else 0.toString() + "" 158 | ) 159 | updateInfo( 160 | context, 161 | true, 162 | Constants.TABLE_DOWN, 163 | titles, 164 | values, 165 | Constants.DOWN_ID + " =? ", 166 | arrayOf(bean.id) 167 | ) 168 | } 169 | 170 | /** 171 | * 删除下载数据库数据 172 | */ 173 | @JvmStatic 174 | @Synchronized 175 | fun deleteDownLoadById(context: Context, id: String?) { 176 | if (!TextUtils.isEmpty(id)) { 177 | deleteInfo( 178 | context, 179 | true, 180 | Constants.TABLE_DOWN, 181 | Constants.DOWN_ID + " =? ", 182 | arrayOf(id) 183 | ) 184 | } 185 | } 186 | } -------------------------------------------------------------------------------- /multifiledownloader/src/main/java/com/github/why168/multifiledownloader/notify/DownFileObserver.kt: -------------------------------------------------------------------------------- 1 | package com.github.why168.multifiledownloader.notify 2 | 3 | import com.github.why168.multifiledownloader.DownLoadBean 4 | import com.github.why168.multifiledownloader.DownLoadState 5 | import java.util.* 6 | 7 | /** 8 | * 观察者 9 | * 10 | * @author Edwin.Wu 11 | * @version 2016/12/28 11:39 12 | * @since JDK1.8 13 | */ 14 | class DownFileObserver : Observer { 15 | 16 | override fun update(o: Observable?, arg: Any?) { 17 | if (arg !is DownLoadBean) { 18 | return 19 | } 20 | when (arg.downloadState) { 21 | DownLoadState.STATE_NONE.index -> {} 22 | DownLoadState.STATE_WAITING.index -> onPrepare(arg) 23 | DownLoadState.STATE_DOWNLOADING.index -> onProgress(arg) 24 | DownLoadState.STATE_PAUSED.index -> onStop(arg) 25 | DownLoadState.STATE_DOWNLOADED.index -> onFinish(arg) 26 | DownLoadState.STATE_ERROR.index -> onError(arg) 27 | } 28 | o?.notifyObservers() 29 | } 30 | 31 | /** 32 | * 准备下载 33 | */ 34 | fun onPrepare(bean: DownLoadBean?) {} 35 | 36 | /** 37 | * 开始下载 38 | */ 39 | fun onStart(bean: DownLoadBean?) {} 40 | 41 | /** 42 | * 下载中 43 | */ 44 | fun onProgress(bean: DownLoadBean?) {} 45 | 46 | /** 47 | * 暂停 48 | */ 49 | fun onStop(bean: DownLoadBean?) {} 50 | 51 | /** 52 | * 下载完成 53 | */ 54 | fun onFinish(bean: DownLoadBean?) {} 55 | 56 | /** 57 | * 下载失败 58 | */ 59 | fun onError(bean: DownLoadBean?) {} 60 | } -------------------------------------------------------------------------------- /multifiledownloader/src/main/java/com/github/why168/multifiledownloader/notify/DownLoadObservable.kt: -------------------------------------------------------------------------------- 1 | package com.github.why168.multifiledownloader.notify 2 | 3 | import android.util.Log 4 | import com.github.why168.multifiledownloader.notify.DownLoadObservable 5 | import com.github.why168.multifiledownloader.DownLoadBean 6 | import java.util.* 7 | 8 | /** 9 | * 下载被观察者 10 | * 11 | * @author Edwin.Wu 12 | * @version 2016/12/28 11:25 13 | * @since JDK1.8 14 | */ 15 | object DownLoadObservable : Observable() { 16 | 17 | @JvmStatic 18 | fun dataChange(data: DownLoadBean) { 19 | Log.d( 20 | "Edwin", 21 | "DownLoadObservable dataChange " + data.downloadState + " , currentSize = " + data.currentSize 22 | ) 23 | setChanged() 24 | this.notifyObservers(data) 25 | } 26 | } -------------------------------------------------------------------------------- /multifiledownloader/src/main/java/com/github/why168/multifiledownloader/utlis/DownLoadConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github.why168.multifiledownloader.utlis 2 | 3 | /** 4 | * 下载配置 5 | * 6 | * @author Edwin.Wu 7 | * @version 2017/1/10 11:43 8 | * @since JDK1.8 9 | */ 10 | object DownLoadConfig { 11 | 12 | /** 13 | * 下载的任务数 14 | */ 15 | private var maxTasks = 5 16 | 17 | @JvmStatic 18 | fun getMaxTasks(): Int { 19 | return maxTasks 20 | } 21 | 22 | @JvmStatic 23 | fun setMaxTasks(maxTasks: Int) { 24 | if (maxTasks <= 0) { 25 | this.maxTasks = 5 26 | } else { 27 | this.maxTasks = maxTasks 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /multifiledownloader/src/main/java/com/github/why168/multifiledownloader/utlis/FileUtilities.kt: -------------------------------------------------------------------------------- 1 | package com.github.why168.multifiledownloader.utlis 2 | 3 | import android.os.Environment 4 | 5 | import com.github.why168.multifiledownloader.Constants 6 | 7 | import java.io.File 8 | import java.math.BigInteger 9 | import java.security.MessageDigest 10 | import java.security.NoSuchAlgorithmException 11 | import java.text.DecimalFormat 12 | 13 | /** 14 | * @author Edwin.Wu 15 | * @version 2017/1/4 18:09 16 | * @since JDK1.8 17 | */ 18 | object FileUtilities { 19 | 20 | private const val HASH_ALGORITHM = "MD5" 21 | private const val RADIX = 10 + 26 // 10 digits + 26 letters 22 | 23 | fun getMd5FileName(url: String): String { 24 | val md5 = getMD5(url.toByteArray()) 25 | val bi = BigInteger(md5).abs() 26 | return bi.toString(RADIX) + url.substring(url.lastIndexOf("/") + 1) 27 | } 28 | 29 | private fun getMD5(data: ByteArray): ByteArray? { 30 | var hash: ByteArray? = null 31 | try { 32 | val digest = MessageDigest.getInstance(HASH_ALGORITHM) 33 | digest.update(data) 34 | hash = digest.digest() 35 | } catch (e: Exception) { 36 | e.printStackTrace() 37 | } 38 | 39 | return hash 40 | } 41 | 42 | 43 | @Synchronized 44 | fun getDownloadFile(url: String): File { 45 | val file = File(Constants.PATH_BASE) 46 | if (!file.exists()) { 47 | file.mkdirs() 48 | } 49 | 50 | return File(file, getMd5FileName(url)) 51 | } 52 | 53 | 54 | fun delFile(file: File): Boolean { 55 | if (!file.exists()) { 56 | return false 57 | } 58 | if (file.isDirectory) { 59 | val files = file.listFiles() 60 | for (f in files) { 61 | delFile(f) 62 | } 63 | } 64 | return file.delete() 65 | } 66 | 67 | 68 | fun clearFileDownloader(): Boolean { 69 | return delFile(File(Constants.PATH_BASE)) 70 | } 71 | 72 | /** 73 | * 转换文件大小 74 | * 75 | * @param fileSize 文件大小 76 | * @return 格式化 77 | */ 78 | fun convertFileSize(fileSize: Long): String { 79 | if (fileSize <= 0) { 80 | return "0M" 81 | } 82 | val df = DecimalFormat("#.00") 83 | val fileSizeString: String 84 | fileSizeString = when { 85 | fileSize < 1024 -> df.format(fileSize.toDouble()) + "B" 86 | fileSize < 1024 * 1024 -> df.format(fileSize.toDouble() / 1024) + "K" 87 | fileSize < 1024 * 1024 * 1024 -> df.format(fileSize.toDouble() / (1024 * 1024)) + "M" 88 | else -> df.format(fileSize.toDouble() / (1024 * 1024 * 1024)) + "G" 89 | } 90 | return fileSizeString 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /multifiledownloader/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | multifiledownloader-library 3 | 4 | -------------------------------------------------------------------------------- /multifiledownloader/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /multifiledownloader/src/test/java/com/github/why168/multifiledownloader/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.github.why168.multifiledownloader; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = "FileDownloader" 2 | include ':app', ':multifiledownloader' 3 | --------------------------------------------------------------------------------