Repository: ssseasonnn/DownloadX Branch: main Commit: 2a307fa435d7 Files: 71 Total size: 125.7 KB Directory structure: gitextract_xtdo3wj0/ ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.ch.md ├── README.md ├── app/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── zlc/ │ │ └── season/ │ │ └── downloadxdemo/ │ │ └── ExampleInstrumentedTest.kt │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── zlc/ │ │ │ └── season/ │ │ │ └── downloadxdemo/ │ │ │ ├── ApkFileProvider.kt │ │ │ ├── AppInfoManager.kt │ │ │ ├── AppListResp.kt │ │ │ ├── DetailActivity.kt │ │ │ ├── HistoryActivity.kt │ │ │ ├── MainActivity.kt │ │ │ ├── ProgressButton.kt │ │ │ ├── TestActivity.kt │ │ │ └── Utils.kt │ │ └── res/ │ │ ├── drawable/ │ │ │ ├── ic_launcher_background.xml │ │ │ └── progress.xml │ │ ├── drawable-v24/ │ │ │ └── ic_launcher_foreground.xml │ │ ├── layout/ │ │ │ ├── activity_detail.xml │ │ │ ├── activity_history.xml │ │ │ ├── activity_main.xml │ │ │ ├── activity_test.xml │ │ │ ├── app_info_item.xml │ │ │ ├── history_item.xml │ │ │ └── layout_progress_button.xml │ │ ├── mipmap-anydpi-v26/ │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ ├── values/ │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ └── xml/ │ │ ├── apk_file_provider.xml │ │ └── network_config.xml │ └── test/ │ └── java/ │ └── zlc/ │ └── season/ │ └── downloadxdemo/ │ └── ExampleUnitTest.kt ├── build.gradle ├── downloadx/ │ ├── .gitignore │ ├── build.gradle │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── zlc/ │ │ └── season/ │ │ └── downloadx/ │ │ └── ExampleInstrumentedTest.kt │ ├── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── zlc/ │ │ └── season/ │ │ └── downloadx/ │ │ ├── DownloadX.kt │ │ ├── Progress.kt │ │ ├── State.kt │ │ ├── core/ │ │ │ ├── DownloadConfig.kt │ │ │ ├── DownloadParam.kt │ │ │ ├── DownloadQueue.kt │ │ │ ├── DownloadTask.kt │ │ │ ├── Downloader.kt │ │ │ ├── Extensions.kt │ │ │ ├── NormalDownloader.kt │ │ │ ├── RangeDownloader.kt │ │ │ ├── RangeTmpFile.kt │ │ │ └── TaskManager.kt │ │ ├── helper/ │ │ │ ├── Default.kt │ │ │ └── Request.kt │ │ └── utils/ │ │ ├── FileUtils.kt │ │ ├── HttpUtil.kt │ │ ├── LogUtil.kt │ │ └── Util.kt │ └── test/ │ └── java/ │ └── zlc/ │ └── season/ │ └── downloadx/ │ └── ExampleUnitTest.kt ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── jitpack.yml └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.iml .gradle .idea /local.properties /.idea/caches /.idea/libraries /.idea/modules.xml /.idea/workspace.xml /.idea/navEditor.xml /.idea/assetWizardSettings.xml .DS_Store /build /captures .externalNativeBuild .cxx ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. ## [DownloadX v1.0.2] - 2021-04-07 - Fix error crash - Add http client factory ## [DownloadX v1.0.1] - 2021-03-11 - Fix stop bug - Add remove method ## [DownloadX v1.0.0] - 2021-03-11 - DownloadX basic ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.ch.md ================================================ ![](usage.png) # DownloadX [![](https://jitpack.io/v/ssseasonnn/DownloadX.svg)](https://jitpack.io/#ssseasonnn/DownloadX) 基于协程打造的下载工具, 支持多线程下载和断点续传 *Read this in other languages: [中文](README.ch.md), [English](README.md), [Changelog](CHANGELOG.md)*  ## Prepare - 添加仓库: ```gradle maven { url 'https://jitpack.io' } ``` - 添加依赖: ```gradle implementation "com.github.ssseasonnn:DownloadX:1.0.5" ``` ## Basic Usage ```kotlin // 创建下载任务 val downloadTask = coroutineScope.download("url") // 监听下载进度 downloadTask.progress() .onEach { binding.button.setProgress(it) } .launchIn(lifecycleScope) // 或者监听下载状态 downloadTask.state() .onEach { binding.button.setState(it) } .launchIn(lifecycleScope) // 开始下载 downloadTask.start() ``` ## 创建任务 - 指定CoroutineScope 如果下载任务仅限于Activity或Fragment的生命周期内,那么可以直接使用Activity或Fragment的**lifecycleScope**,即可保证在Activity或Fragment销毁的时候自动结束下载任务 > lifecycleScope是androidX中的扩展,需要添加以下依赖: > implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0' ```kotlin class DemoActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //activity销毁时,该下载任务自动停止 val downloadTask = lifecycleScope.download("url") downloadTask.start() } } ``` 如果下载任务需要在多个Activity之间共享,或者进行后台下载,那么直接使用**GlobalScope**即可 ```kotlin class DemoActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //activity销毁时,该下载任务仍然继续下载 val downloadTask = GlobalScope.download("url") downloadTask.start() } } ``` - 设置保存文件名和保存路径 直接传给download方法: ```kotlin val downloadTask = GlobalScope.download("url", "saveName", "savePath") ``` 创建自定义DownloadParam: ```kotlin val downloadParam = DownloadParam("url", "saveName", "savePath") val downloadTask = lifecycleScope.download(downloadParam) ``` 默认情况下,我们使用**url**作为**DownloadTask**的唯一标示,当需要改变这一默认行为时,可以自定义自己的**DownloadParam**: ```kotlin class CustomDownloadParam(url: String, saveName: String, savePath: String) : DownloadParam(url, saveName, savePath) { override fun tag(): String { // 使用文件路径作为唯一标示 return savePath + saveName } } val customDownloadParam = CustomDownloadParam("url", "saveName", "savePath") val downloadTask = lifecycleScope.download(customDownloadParam) ``` 在多个页面使用同样的标识(例如相同的url)创建下载任务时,将会返回同一个DownloadTask,例如: ```kotlin // 同一个url val url = "xxxx" class DemoActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //创建下载任务 val downloadTask = GlobalScope.download(url) downloadTask.progress() .onEach { progress -> /* 更新进度 */ } .launchIn(lifecycleScope) downloadTask.start() } } class OtherActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //以相同的url创建下载任务,即可获取上一个页面创建的下载任务 val downloadTask = GlobalScope.download(url) downloadTask.progress() .onEach { progress -> /* 更新进度 */ } .launchIn(lifecycleScope) downloadTask.start() } } ``` 基于此,可以在任意多个页面中共享同一个下载进度和下载状态 ## 进度和状态 - 只监听进度 在某些场景只需要下载的进度时,可使用这种方式 ```kotlin // 创建任务 val downloadTask = lifecycleScope.download("url") downloadTask.progress() .onEach { progress -> /* 更新进度 */ } .launchIn(lifecycleScope) // 使用lifecycleScope //开始下载 downloadTask.start() ``` > 利用**lifecycleScope**可确保在Activity或Fragment销毁的时候自动解除监听 可以为progress()方法设置更新间隔,默认是200ms更新一次,如: ```kotlin downloadTask.progress(500) // 设置为500ms更新一次进度 .onEach { progress -> // 更新进度 setProgress(progress) } .launchIn(lifecycleScope) ``` - 监听下载状态和进度 当需要下载状态和下载进度的时候,使用这种方式获取 ```kotlin // 创建任务 val downloadTask = lifecycleScope.download("url") downloadTask.state() .onEach { state -> // 更新状态 setState(state) // 更新进度 setProgress(state.progress) } .launchIn(lifecycleScope) //开始下载 downloadTask.start() ``` > state有以下值:**None,Waiting,Downloading,Stopped,Failed,Succeed** 同样的,可以为state()方法设置进度更新间隔 ## 启动和停止 - 开始下载 ```kotlin downloadTask.start() ``` - 停止下载 ```kotlin downloadTask.stop() ``` - 删除下载 ```kotlin downloadTask.remove() ``` ## License > ``` > Copyright 2021 Season.Zlc > > Licensed under the Apache License, Version 2.0 (the "License"); > you may not use this file except in compliance with the License. > You may obtain a copy of the License at > > http://www.apache.org/licenses/LICENSE-2.0 > > Unless required by applicable law or agreed to in writing, software > distributed under the License is distributed on an "AS IS" BASIS, > WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. > See the License for the specific language governing permissions and > limitations under the License. > ``` ================================================ FILE: README.md ================================================ ![](usage.png) # DownloadX [![](https://jitpack.io/v/ssseasonnn/DownloadX.svg)](https://jitpack.io/#ssseasonnn/DownloadX) A multi-threaded download tool written with Coroutine and Kotlin *Read this in other languages: [中文](README.ch.md), [English](README.md), [Changelog](CHANGELOG.md)*  ## Prepare - Add jitpack repo: ```gradle maven { url 'https://jitpack.io' } ``` - Add dependency: ```gradle implementation "com.github.ssseasonnn:DownloadX:1.0.5" ``` ## Basic Usage ```kotlin // create download task val downloadTask = coroutineScope.download("url") // listen download progress downloadTask.progress() .onEach { binding.button.setProgress(it) } .launchIn(lifecycleScope) // or listen download state downloadTask.state() .onEach { binding.button.setState(it) } .launchIn(lifecycleScope) // start download downloadTask.start() ``` ## Create task - Specify CoroutineScope If the download task is limited to the lifecycle of the Activity or Fragment, you can directly use the **lifecycleScope** of the Activity or Fragment to ensure that the download task ends automatically when the Activity or Fragment is destroyed > lifecycleScope is an extension in androidX, you need to add the following dependencies: > implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0' ```kotlin class DemoActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //When the activity is destroyed, the download task automatically stops val downloadTask = lifecycleScope.download("url") downloadTask.start() } } ``` If the download task needs to be shared between multiple activities, or download in the background, then directly use **GlobalScope** ```kotlin class DemoActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //When the activity is destroyed, the download task still continues to download val downloadTask = GlobalScope.download("url") downloadTask.start() } } ``` - Set the file name and save path Pass directly to the download method: ```kotlin val downloadTask = GlobalScope.download("url", "saveName", "savePath") ``` Custom DownloadParam: ```kotlin val downloadParam = DownloadParam("url", "saveName", "savePath") val downloadTask = lifecycleScope.download(downloadParam) ``` By default, we use **url** as the only indicator of **DownloadTask**. When you need to change this default behavior, you can customize your own **DownloadParam**: ```kotlin class CustomDownloadParam(url: String, saveName: String, savePath: String) : DownloadParam(url, saveName, savePath) { override fun tag(): String { // Use the file path as a unique identifier return savePath + saveName } } val customDownloadParam = CustomDownloadParam("url", "saveName", "savePath") val downloadTask = lifecycleScope.download(customDownloadParam) ``` When multiple pages use the same identifier (for example, the same url) to create a download task, the same DownloadTask will be returned, for example: ```kotlin // same url val url = "xxxx" class DemoActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //Create download task val downloadTask = GlobalScope.download(url) downloadTask.progress() .onEach { progress -> /* update progress */ } .launchIn(lifecycleScope) downloadTask.start() } } class OtherActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //Create a download task with the same url to get the download task created on the previous page val downloadTask = GlobalScope.download(url) downloadTask.progress() .onEach { progress -> /* update progress */ } .launchIn(lifecycleScope) downloadTask.start() } } ``` Based on this, the same download progress and download status can be shared on any number of pages ## Progress and State - Listen progress only This method can be used in certain scenarios when only the download progress is needed ```kotlin val downloadTask = lifecycleScope.download("url") downloadTask.progress() .onEach { progress -> /* update progress */ } .launchIn(lifecycleScope) // using lifecycleScope downloadTask.start() ``` > Use **lifecycleScope** to ensure that the monitoring is automatically released when the Activity or Fragment is destroyed You can set the update interval for the progress() method. The default is to update every 200ms, such as: ```kotlin downloadTask.progress(500) // Set to update the progress every 500ms .onEach { progress -> // update progress setProgress(progress) } .launchIn(lifecycleScope) ``` - Listen progress and state When you need download status and download progress, use this method to get ```kotlin val downloadTask = lifecycleScope.download("url") downloadTask.state() .onEach { state -> // update state setState(state) // update progress setProgress(state.progress) } .launchIn(lifecycleScope) downloadTask.start() ``` > state has the following values:**None,Waiting,Downloading,Stopped,Failed,Succeed** Similarly, you can set the progress update interval for the state() method ## Start and Stop - Start download ```kotlin downloadTask.start() ``` - Stop download ```kotlin downloadTask.stop() ``` - Remove download ```kotlin downloadTask.remove() ``` ## License > ``` > Copyright 2021 Season.Zlc > > Licensed under the Apache License, Version 2.0 (the "License"); > you may not use this file except in compliance with the License. > You may obtain a copy of the License at > > http://www.apache.org/licenses/LICENSE-2.0 > > Unless required by applicable law or agreed to in writing, software > distributed under the License is distributed on an "AS IS" BASIS, > WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. > See the License for the specific language governing permissions and > limitations under the License. > ``` ================================================ FILE: app/.gitignore ================================================ /build ================================================ FILE: app/build.gradle ================================================ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' android { compileSdkVersion 28 defaultConfig { applicationId "zlc.season.downloadxdemo" minSdkVersion 21 //noinspection ExpiredTargetSdkVersion targetSdkVersion 28 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } lintOptions { abortOnError false } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = "1.8" } buildFeatures { viewBinding true } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'com.google.android.material:material:1.4.0' implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.fragment:fragment-ktx:1.2.5' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation "io.coil-kt:coil:1.1.1" implementation 'com.github.ssseasonnn:Yasha:1.1.4' implementation 'com.github.ssseasonnn:Bracer:1.0.7' implementation project(path: ':downloadx') testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' } ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: app/src/androidTest/java/zlc/season/downloadxdemo/ExampleInstrumentedTest.kt ================================================ package zlc.season.downloadxdemo import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Test import org.junit.runner.RunWith import org.junit.Assert.* /** * Instrumented test, which will execute on an Android device. * * See [testing documentation](http://d.android.com/tools/testing). */ @RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { @Test fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getInstrumentation().targetContext assertEquals("zlc.season.downloadxdemo", appContext.packageName) } } ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/java/zlc/season/downloadxdemo/ApkFileProvider.kt ================================================ package zlc.season.downloadxdemo import androidx.core.content.FileProvider class ApkFileProvider : FileProvider() ================================================ FILE: app/src/main/java/zlc/season/downloadxdemo/AppInfoManager.kt ================================================ package zlc.season.downloadxdemo import com.google.gson.Gson import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext object AppInfoManager { suspend fun getAppInfoList(): List { return withContext(Dispatchers.IO) { val list = Gson().fromJson(appListJson, AppListResp::class.java) list.appList } } val appListJson = """ { "appList": [ { "pkgName": "com.tencent.ggame", "channelId": "", "source": 52513621, "appId": 52513588, "apkId": 102432290, "appName": "腾讯广东麻将", "fileSize": 52489311, "versionCode": 174, "versionName": "1.7.4", "apkUrl": "http://imtt.dd.qq.com/sjy.40001/sjy.00001/16891/apk/69E7E4E87B798032925A2CA9D99F4F22.apk?fsname=com.tencent.ggame_1.7.4_174.apk&csr=81e7", "totalDownloadTimes": 1892987, "shortDesc": "正宗广东麻将手游", "apkMd5": "69E7E4E87B798032925A2CA9D99F4F22", "minSdkVersion": 16, "parentCategoryID": -2, "signatureMd5": "A8DF121F79960593B23A558E2154FFBA", "categoryId": 121, "categoryName": "网络游戏", "averageRating": 3, "publishTime": 0, "realTimeReportFlag": 0, "extData": "", "logoUrl": "https://pp.myapp.com/ma_icon/0/icon_52513588_1658376667/256", "iconUrl": "https://pp.myapp.com/ma_icon/0/icon_52513588_1658376667/96", "recommendId": "BwYCGQIAAw+ZEBUsRgxHTlhuZ3RpcGdYR3RRASxgGXYAiAyWAKyyYwNTsckACAoAATIAAxD5QAILCgACMgADEPxABwsKAAMyAAMQ/UAICwoABDIAAxD+QAgLCgAFMgADEP9ABgsKAAYyAAMRB0ACCwoABzIAAxEIQAILCgAIMgADEQlAAgvVQDlLKgNxdY7s/A/wEAP2EQ8GCeaWl+WcsOS4uyABugv2EjE1MjUxMzU4OF8xNjYwNzM0MzQ1ODE3NTI0NzU5M180MjA1NDk3Nzk1NjE4MjU2OTc2+BMADQYCMTAWEDQ3OjAuMDQxOTA4MjMxOjAGAjEzFm10dz0wOnRndz0wOnRhZ3c9MDpxaXM9MDpxaXI9MDpsZGFzPTAuMTU0MDc3NzQ1OmN2cj0wOnFtMnNpbT0wOm1hdGNoX3R5cGU9MDpzZWFyY2hfbHZsPTA6cnFfbWF0Y2g6NTpxY19tYXRjaDo1BgQyMDE4FgswLjAwMzEzMTA1NAYEMjAxORYJ5paX5Zyw5Li7BghhY2N1cmF0ZRYBMQYEcGN2chYLMC4wODQzMTEzNjYGBXF1ZXJ5FgnmlpflnLDkuLsGCnF1ZXJ5X2ZsYWcWATEGDHJld3JpdGVxdWVyeRYABgN0Z3cWBjAuMDAwMQYCdHcWBjAuMDAwMQYJdXNlcl90eXBlFgEwBgZ6cHJpY2UWATD1FAAAAAAAAAAA/f8ADH8ABBDg6x0=" }, { "pkgName": "com.tencent.qqgame.qqhlupwvga", "channelId": "", "source": 10103134, "appId": 10103101, "apkId": 104735997, "appName": "欢乐升级(腾讯)", "fileSize": 130472533, "versionCode": 43020, "versionName": "4.3.2", "apkUrl": "http://imtt.dd.qq.com/sjy.40001/sjy.00001/16891/apk/C2F17F4006AB308BAF0B29D102DD6566.apk?fsname=com.tencent.qqgame.qqhlupwvga_4.3.2_43020.apk&csr=81e7", "totalDownloadTimes": 26475921, "shortDesc": "腾讯官方出品的欢乐升级", "apkMd5": "C2F17F4006AB308BAF0B29D102DD6566", "minSdkVersion": 16, "parentCategoryID": -2, "signatureMd5": "F6A0BB7245074B9F080D03796F8919DB", "categoryId": 148, "categoryName": "棋牌中心", "averageRating": 4, "publishTime": 0, "realTimeReportFlag": 0, "extData": "", "logoUrl": "https://pp.myapp.com/ma_icon/0/icon_10103101_1657014288/256", "iconUrl": "https://pp.myapp.com/ma_icon/0/icon_10103101_1657014288/96", "recommendId": "BwYCGQIAAw+ZEBUsRgxHTlhuZ3RpcGdYR3RRASxgGnYAiAyWAKyyYwNTsckACAoAATIAAxD5QAILCgACMgADEPxABwsKAAMyAAMQ/UAICwoABDIAAxD+QAgLCgAFMgADEP9ABgsKAAYyAAMRB0ACCwoABzIAAxEIQAILCgAIMgADEQlAAgvVQDkAbDlxdY7s/A/wEAP2EQ8GCeaWl+WcsOS4uyABugv2EjExMDEwMzEwMV8xNjYwNzM0MzQ1ODE3NTI0NzU5M180MjA1NDk3Nzk1NjE4MjU2OTc2+BMADQYCMTAWEDQ3OjAuMTAyMDg3Mjc5OjAGAjEzFm10dz0wOnRndz0wOnRhZ3c9MDpxaXM9MDpxaXI9MDpsZGFzPTAuODk3NjI3NTY0OmN2cj0wOnFtMnNpbT0wOm1hdGNoX3R5cGU9MDpzZWFyY2hfbHZsPTA6cnFfbWF0Y2g6NTpxY19tYXRjaDo1BgQyMDE4FgswLjAwOTE3ODkyNgYEMjAxORYJ5paX5Zyw5Li7BghhY2N1cmF0ZRYBMQYEcGN2chYLMC4wODMzMzgxNzEGBXF1ZXJ5FgnmlpflnLDkuLsGCnF1ZXJ5X2ZsYWcWATEGDHJld3JpdGVxdWVyeRYABgN0Z3cWBjAuMDAwMQYCdHcWBjAuMDAwMQYJdXNlcl90eXBlFgEwBgZ6cHJpY2UWATD1FAAAAAAAAAAA/f8ADH8ABBDg6x0=" }, { "pkgName": "com.ourgame.mahjong.danji", "channelId": "", "source": 273687, "appId": 273654, "apkId": 105063525, "appName": "单机麻将-开心版", "fileSize": 60238101, "versionCode": 31927, "versionName": "7.3.19.27", "apkUrl": "http://imtt.dd.qq.com/sjy.40001/sjy.00002/16891/apk/49B44FE0E45A7FF1AB02AF9B348022DC.apk?fsname=com.ourgame.mahjong.danji_7.3.19.27_31927.apk&csr=81e7", "totalDownloadTimes": 13671289, "shortDesc": "上桌论英雄,争先当雀神!", "apkMd5": "49B44FE0E45A7FF1AB02AF9B348022DC", "minSdkVersion": 17, "parentCategoryID": -2, "signatureMd5": "7F49616F29A5888427DA005028176EEE", "categoryId": 148, "categoryName": "棋牌中心", "averageRating": 4, "publishTime": 0, "realTimeReportFlag": 0, "extData": "", "logoUrl": "https://pp.myapp.com/ma_icon/0/icon_273654_1658307138/256", "iconUrl": "https://pp.myapp.com/ma_icon/0/icon_273654_1658307138/96", "recommendId": "BwYCFgIAAw+ZEBUsRgxHTlhuZ3RpcGdYR3RRASxgG3YAiAyWAKyyYwNTsckACAoAATIAAxD5QAILCgACMgADEPxABwsKAAMyAAMQ/UAICwoABDIAAxD+QAgLCgAFMgADEP9ABgsKAAYyAAMRB0ACCwoABzIAAxEIQAILCgAIMgADEQlAAgvVQDhXbPFxdY7s/A/wEAP2EQ8GCeaWl+WcsOS4uyABugv2Ei8yNzM2NTRfMTY2MDczNDM0NTgxNzUyNDc1OTNfNDIwNTQ5Nzc5NTYxODI1Njk3NvgTAA0GAjEwFhA0NzowLjA0NTU4OTUwOTowBgIxMxZsdHc9MDp0Z3c9MDp0YWd3PTA6cWlzPTA6cWlyPTA6bGRhcz0wLjE1Nzk0NTYyOmN2cj0wOnFtMnNpbT0wOm1hdGNoX3R5cGU9MDpzZWFyY2hfbHZsPTA6cnFfbWF0Y2g6NTpxY19tYXRjaDo1BgQyMDE4FgswLjAwMzkzMTA2NgYEMjAxORYJ5paX5Zyw5Li7BghhY2N1cmF0ZRYBMQYEcGN2chYLMC4wODExMzc2ODcGBXF1ZXJ5FgnmlpflnLDkuLsGCnF1ZXJ5X2ZsYWcWATEGDHJld3JpdGVxdWVyeRYABgN0Z3cWBjAuMDAwMQYCdHcWBjAuMDAwMQYJdXNlcl90eXBlFgEwBgZ6cHJpY2UWATD1FAAAAAAAAAAA/f8ADH8ABBDg6x0=" }, { "pkgName": "com.gdy.yyb", "channelId": "", "source": 52637269, "appId": 52637236, "apkId": 104990247, "appName": "干瞪眼", "fileSize": 45351985, "versionCode": 70309, "versionName": "7.3.9", "apkUrl": "http://imtt.dd.qq.com/sjy.40001/sjy.00002/16891/apk/74043F69111880A873832E75B1535AC3.apk?fsname=com.gdy.yyb_7.3.9_70309.apk&csr=81e7", "totalDownloadTimes": 92229, "shortDesc": "一轮出完让他干瞪眼", "apkMd5": "74043F69111880A873832E75B1535AC3", "minSdkVersion": 14, "parentCategoryID": -2, "signatureMd5": "172C769C70E4586A9359A2ECEA2649B4", "categoryId": 148, "categoryName": "棋牌中心", "averageRating": 3, "publishTime": 0, "realTimeReportFlag": 0, "extData": "", "logoUrl": "https://pp.myapp.com/ma_icon/0/icon_52637236_1658109485/256", "iconUrl": "https://pp.myapp.com/ma_icon/0/icon_52637236_1658109485/96", "recommendId": "BwYCGQIAAw+ZEBUsRgxHTlhuZ3RpcGdYR3RRASxgHHYAiAyWAKyyYwNTsckACAoAATIAAxD5QAILCgACMgADEPxABwsKAAMyAAMQ/UAICwoABDIAAxD+QAgLCgAFMgADEP9ABgsKAAYyAAMRB0ACCwoABzIAAxEIQAILCgAIMgADEQlAAgvVQDVkkGlxdY7s/A/wEAP2EQ8GCeaWl+WcsOS4uyABugv2EjE1MjYzNzIzNl8xNjYwNzM0MzQ1ODE3NTI0NzU5M180MjA1NDk3Nzk1NjE4MjU2OTc2+BMADQYCMTAWEDQ3OjAuMDM4OTE1MDU4OjAGAjEzFm10dz0wOnRndz0wOnRhZ3c9MDpxaXM9MDpxaXI9MDpsZGFzPTAuOTkwNzQ3OTU2OmN2cj0wOnFtMnNpbT0wOm1hdGNoX3R5cGU9MDpzZWFyY2hfbHZsPTA6cnFfbWF0Y2g6NTpxY19tYXRjaDo1BgQyMDE4FgswLjAwMjIxNzkzNQYEMjAxORYJ5paX5Zyw5Li7BghhY2N1cmF0ZRYBMQYEcGN2chYLMC4wNzEzMDg3NjIGBXF1ZXJ5FgnmlpflnLDkuLsGCnF1ZXJ5X2ZsYWcWATEGDHJld3JpdGVxdWVyeRYABgN0Z3cWBjAuMDAwMQYCdHcWBjAuMDAwMQYJdXNlcl90eXBlFgEwBgZ6cHJpY2UWATD1FAAAAAAAAAAA/f8ADH8ABBDg6x0=" }, { "pkgName": "com.tencent.tmgp.jinxiuddz", "channelId": "", "source": 54172738, "appId": 54172705, "apkId": 98945691, "appName": "英雄斗地主", "fileSize": 84962197, "versionCode": 1, "versionName": "1.27", "apkUrl": "http://imtt.dd.qq.com/sjy.40001/sjy.00002/16891/apk/E9C56679E0F72140578F8B7AC5E3C536.apk?fsname=com.tencent.tmgp.jinxiuddz_1.27_1.apk&csr=81e7", "totalDownloadTimes": 4370, "shortDesc": "", "apkMd5": "E9C56679E0F72140578F8B7AC5E3C536", "minSdkVersion": 16, "parentCategoryID": -2, "signatureMd5": "B08E94AB2AE6AD9E8696B32F1AD823F7", "categoryId": 148, "categoryName": "棋牌中心", "averageRating": 3, "publishTime": 0, "realTimeReportFlag": 0, "extData": "", "logoUrl": "https://pp.myapp.com/ma_icon/0/icon_54172705_1635823131/256", "iconUrl": "https://pp.myapp.com/ma_icon/0/icon_54172705_1635823131/96", "recommendId": "BwYCHAIAAw+ZEBUsRgxHTlhuZ3RpcGdYR3RRASxgHXYAiAyWAKyyYwNTsckACAoAATIAAxD5QAILCgACMgADEPxABwsKAAMyAAMQ/UAICwoABDIAAxD+QAgLCgAFMgADEP9ABgsKAAYyAAMRB0ACCwoABzIAAxEIQAILCgAIMgADEQlAAgvVQDKlCMAfIS3s/A/wEAP2EQ8GCeaWl+WcsOS4uyABugv2EjE1NDE3MjcwNV8xNjYwNzM0MzQ1ODE3NTI0NzU5M180MjA1NDk3Nzk1NjE4MjU2OTc2+BMADQYCMTAWGjc5OjAuMjE2MzM1MjU0OjAuODQzMjY2MTc0BgIxMxZodHc9MC42NTp0Z3c9MDp0YWd3PTA6cWlzPTM6cWlyPTAuNjpsZGFzPTA6Y3ZyPTA6cW0yc2ltPTA6bWF0Y2hfdHlwZT0yOnNlYXJjaF9sdmw9MjpycV9tYXRjaDo1OnFjX21hdGNoOjUGBDIwMTgWCzAuMDMwMTA1NDcyBgQyMDE5FgnmlpflnLDkuLsGCGFjY3VyYXRlFgExBgRwY3ZyFgswLjA1OTk4MTg4MwYFcXVlcnkWCeaWl+WcsOS4uwYKcXVlcnlfZmxhZxYBMQYMcmV3cml0ZXF1ZXJ5FgAGA3RndxYGMC4wMDAxBgJ0dxYEMC42NQYJdXNlcl90eXBlFgEwBgZ6cHJpY2UWATD1FAAAAAAAAAAA/f8ADH8ABBDg6x0=" }, { "pkgName": "com.tencent.tmgp.lljjyyb2", "channelId": "", "source": 54205905, "appId": 54205872, "apkId": 105434988, "appName": "乐乐竞技斗地主", "fileSize": 60127573, "versionCode": 2200, "versionName": "2.2.0", "apkUrl": "http://imtt.dd.qq.com/sjy.40001/sjy.00002/16891/apk/22EF8602EB5EB244D9B60F872FDCD91C.apk?fsname=com.tencent.tmgp.lljjyyb2_2.2.0_2200.apk&csr=81e7", "totalDownloadTimes": 1735, "shortDesc": "", "apkMd5": "22EF8602EB5EB244D9B60F872FDCD91C", "minSdkVersion": 19, "parentCategoryID": -2, "signatureMd5": "D6AE8A16AB9B034460663473405385E1", "categoryId": 148, "categoryName": "棋牌中心", "averageRating": 5, "publishTime": 0, "realTimeReportFlag": 0, "extData": "", "logoUrl": "https://pp.myapp.com/ma_icon/0/icon_54205872_1658914399/256", "iconUrl": "https://pp.myapp.com/ma_icon/0/icon_54205872_1658914399/96", "recommendId": "BwYCGwIAAw+ZEBUsRgxHTlhuZ3RpcGdYR3RRASxgHnYAiAyWAKyyYwNTsckACAoAATIAAxD5QAILCgACMgADEPxABwsKAAMyAAMQ/UAICwoABDIAAxD+QAgLCgAFMgADEP9ABgsKAAYyAAMRB0ACCwoABzIAAxEIQAILCgAIMgADEQlAAgvVQDIx1z+fIS3s/A/wEAP2EQ8GCeaWl+WcsOS4uyABugv2EjE1NDIwNTg3Ml8xNjYwNzM0MzQ1ODE3NTI0NzU5M180MjA1NDk3Nzk1NjE4MjU2OTc2+BMADQYCMTAWGTc5OjAuMjIxNzQzMTM6MC44NjY1MjQ1NTIGAjEzFmh0dz0wLjY1OnRndz0wOnRhZ3c9MDpxaXM9MzpxaXI9MC41OmxkYXM9MDpjdnI9MDpxbTJzaW09MDptYXRjaF90eXBlPTI6c2VhcmNoX2x2bD0yOnJxX21hdGNoOjU6cWNfbWF0Y2g6NQYEMjAxOBYLMC4wMzIwMTc1MDEGBDIwMTkWCeaWl+WcsOS4uwYIYWNjdXJhdGUWATEGBHBjdnIWCzAuMDU4NDgxOTY5BgVxdWVyeRYJ5paX5Zyw5Li7BgpxdWVyeV9mbGFnFgExBgxyZXdyaXRlcXVlcnkWAAYDdGd3FgYwLjAwMDEGAnR3FgQwLjY1Bgl1c2VyX3R5cGUWATAGBnpwcmljZRYBMPUUAAAAAAAAAAD9/wAMfwAEEODrHQ==" }, { "pkgName": "com.cbfq.srddz", "channelId": "", "source": 52515798, "appId": 52515765, "apkId": 105830041, "appName": "乐享四人斗地主", "fileSize": 74401217, "versionCode": 929, "versionName": "9.2.9", "apkUrl": "http://imtt.dd.qq.com/sjy.40001/sjy.00002/16891/apk/DEC4815412BFEDFDD02308835FF74999.apk?fsname=com.cbfq.srddz_9.2.9_929.apk&csr=81e7", "totalDownloadTimes": 356940, "shortDesc": "新用户登录即领可领取5元话费哦", "apkMd5": "DEC4815412BFEDFDD02308835FF74999", "minSdkVersion": 21, "parentCategoryID": -2, "signatureMd5": "1DAF2C9DECC930C11D4ADB45C6B69F82", "categoryId": 148, "categoryName": "棋牌中心", "averageRating": 4, "publishTime": 0, "realTimeReportFlag": 0, "extData": "", "logoUrl": "https://pp.myapp.com/ma_icon/0/icon_52515765_1660287617/256", "iconUrl": "https://pp.myapp.com/ma_icon/0/icon_52515765_1660287617/96", "recommendId": "BwYCGAIAAw+ZEBUsRgxHTlhuZ3RpcGdYR3RRASxgH3YAiAyWAKyyYwNTsckACAoAATIAAxD5QAILCgACMgADEPxABwsKAAMyAAMQ/UAICwoABDIAAxD+QAgLCgAFMgADEP9ABgsKAAYyAAMRB0ACCwoABzIAAxEIQAILCgAIMgADEQlAAgvVQDF5pP/xdY7s/A/wEAP2EQ8GCeaWl+WcsOS4uyABugv2EjE1MjUxNTc2NV8xNjYwNzM0MzQ1ODE3NTI0NzU5M180MjA1NDk3Nzk1NjE4MjU2OTc2+BMADQYCMTAWEDQ3OjAuMDUyMDQyODQxOjAGAjEzFm10dz0wOnRndz0wOnRhZ3c9MDpxaXM9MDpxaXI9MDpsZGFzPTAuOTg1Nzc1NjE5OmN2cj0wOnFtMnNpbT0wOm1hdGNoX3R5cGU9MDpzZWFyY2hfbHZsPTA6cnFfbWF0Y2g6NTpxY19tYXRjaDo1BgQyMDE4FgowLjAwMzQ3MzEyBgQyMDE5FgnmlpflnLDkuLsGCGFjY3VyYXRlFgExBgRwY3ZyFgswLjA1ODI0OTkxMwYFcXVlcnkWCeaWl+WcsOS4uwYKcXVlcnlfZmxhZxYBMQYMcmV3cml0ZXF1ZXJ5FgAGA3RndxYGMC4wMDAxBgJ0dxYGMC4wMDAxBgl1c2VyX3R5cGUWATAGBnpwcmljZRYBMPUUAAAAAAAAAAD9/wAMfwAEEODrHQ==" }, { "pkgName": "com.tencent.tmgp.speedmobile", "channelId": "", "source": 52488608, "appId": 52488575, "apkId": 104549788, "appName": "QQ飞车手游", "fileSize": 2096029603, "versionCode": 1320002188, "versionName": "1.32.0.2188", "apkUrl": "http://imtt.dd.qq.com/sjy.40001/sjy.00001/16891/apk/D09201BF06A2EAFA68833DDAEEB38A5F.apk?fsname=com.tencent.tmgp.speedmobile_1.32.0.2188_1320002188.apk&csr=81e7", "totalDownloadTimes": 113372898, "shortDesc": "经典国民竞速手游", "apkMd5": "D09201BF06A2EAFA68833DDAEEB38A5F", "minSdkVersion": 21, "parentCategoryID": -2, "signatureMd5": "9BCBAFE32AE8382CC224F5AAB0EE8383", "categoryId": 151, "categoryName": "体育竞速", "averageRating": 4, "publishTime": 0, "realTimeReportFlag": 0, "extData": "", "logoUrl": "https://pp.myapp.com/ma_icon/0/icon_52488575_1658377825/256", "iconUrl": "https://pp.myapp.com/ma_icon/0/icon_52488575_1658377825/96", "recommendId": "BwYCGQIAAw+ZEBUsRgxHTlhuZ3RpcGdYR3RRASxgIHYAiAyWAKyyYwNTsckACAoAATIAAxD5QAILCgACMgADEPxABwsKAAMyAAMQ/UAICwoABDIAAxD+QAgLCgAFMgADEP9ABgsKAAYyAAMRB0ACCwoABzIAAxEIQAILCgAIMgADEQlAAgvVQCxOKZ7i6xzs/A/wEAP2EQ8GCeaWl+WcsOS4uyABugv2EjE1MjQ4ODU3NV8xNjYwNzM0MzQ1ODE3NTI0NzU5M180MjA1NDk3Nzk1NjE4MjU2OTc2+BMADQYCMTAWEDQ3OjAuMDY5MTU0MjczOjAGAjEzFm10dz0wOnRndz0wOnRhZ3c9MDpxaXM9MDpxaXI9MDpsZGFzPTAuMDQxNzU1ODYxOmN2cj0wOnFtMnNpbT0wOm1hdGNoX3R5cGU9MDpzZWFyY2hfbHZsPTA6cnFfbWF0Y2g6NTpxY19tYXRjaDo1BgQyMDE4FgswLjAwNjc4MzA4OQYEMjAxORYJ5paX5Zyw5Li7BghhY2N1cmF0ZRYBMQYEcGN2chYLMC4wNDcxNzQ4NzEGBXF1ZXJ5FgnmlpflnLDkuLsGCnF1ZXJ5X2ZsYWcWATEGDHJld3JpdGVxdWVyeRYABgN0Z3cWBjAuMDAwMQYCdHcWBjAuMDAwMQYJdXNlcl90eXBlFgEwBgZ6cHJpY2UWATD1FAAAAAAAAAAA/f8ADH8ABBDg6x0=" }, { "pkgName": "com.qqgame.happymj", "channelId": "", "source": 10125401, "appId": 10125368, "apkId": 105120675, "appName": "腾讯欢乐麻将全集", "fileSize": 270064595, "versionCode": 77630, "versionName": "7.7.63", "apkUrl": "http://imtt.dd.qq.com/sjy.40001/sjy.00001/16891/apk/D830417642DF843242A0BD78601D1B14.apk?fsname=com.qqgame.happymj_7.7.63_77630.apk&csr=81e7", "totalDownloadTimes": 327695044, "shortDesc": "全国各地麻将玩法合集", "apkMd5": "D830417642DF843242A0BD78601D1B14", "minSdkVersion": 16, "parentCategoryID": -2, "signatureMd5": "F6A0BB7245074B9F080D03796F8919DB", "categoryId": 148, "categoryName": "棋牌中心", "averageRating": 4, "publishTime": 0, "realTimeReportFlag": 0, "extData": "", "logoUrl": "https://pp.myapp.com/ma_icon/0/icon_10125368_1658475371/256", "iconUrl": "https://pp.myapp.com/ma_icon/0/icon_10125368_1658475371/96", "recommendId": "BwYCGQIAAw+ZEBUsRgxHTlhuZ3RpcGdYR3RRASxgIXYAiAyWAKyyYwNTsckACAoAATIAAxD5QAILCgACMgADEPxABwsKAAMyAAMQ/UAICwoABDIAAxD+QAgLCgAFMgADEP9ABgsKAAYyAAMRB0ACCwoABzIAAxEIQAILCgAIMgADEQlAAgvVQCaIMK7i6xzs/A/wEAP2EQ8GCeaWl+WcsOS4uyABugv2EjExMDEyNTM2OF8xNjYwNzM0MzQ1ODE3NTI0NzU5M180MjA1NDk3Nzk1NjE4MjU2OTc2+BMADQYCMTAWEDQ3OjAuMTczNTE3MjM1OjAGAjEzFm10dz0wOnRndz0wOnRhZ3c9MDpxaXM9MDpxaXI9MDpsZGFzPTAuMTc3NTYxMjgzOmN2cj0wOnFtMnNpbT0wOm1hdGNoX3R5cGU9MDpzZWFyY2hfbHZsPTA6cnFfbWF0Y2g6NTpxY19tYXRjaDo1BgQyMDE4FgswLjAxODIyNDE0MQYEMjAxORYJ5paX5Zyw5Li7BghhY2N1cmF0ZRYBMQYEcGN2chYLMC4wMzc1NTI2NTUGBXF1ZXJ5FgnmlpflnLDkuLsGCnF1ZXJ5X2ZsYWcWATEGDHJld3JpdGVxdWVyeRYABgN0Z3cWBjAuMDAwMQYCdHcWBjAuMDAwMQYJdXNlcl90eXBlFgEwBgZ6cHJpY2UWATD1FAAAAAAAAAAA/f8ADH8ABBDg6x0=" }, { "pkgName": "com.tencent.tmgp.ibirdgame.doudizhu", "channelId": "", "source": 54211549, "appId": 54211516, "apkId": 100771090, "appName": "笨鸟斗地主", "fileSize": 111990879, "versionCode": 3, "versionName": "1.3", "apkUrl": "http://imtt.dd.qq.com/sjy.40001/sjy.00002/16891/apk/A7120AEB0DE5C59E3415C33149F5F6BA.apk?fsname=com.tencent.tmgp.ibirdgame.doudizhu_1.3_3.apk&csr=81e7", "totalDownloadTimes": 1030, "shortDesc": "", "apkMd5": "A7120AEB0DE5C59E3415C33149F5F6BA", "minSdkVersion": 21, "parentCategoryID": -2, "signatureMd5": "B86815FCC30CFAE5EFCCAA07FD9F6C52", "categoryId": 148, "categoryName": "棋牌中心", "averageRating": 0, "publishTime": 0, "realTimeReportFlag": 0, "extData": "", "logoUrl": "https://pp.myapp.com/ma_icon/0/icon_54211516_1640055072/256", "iconUrl": "https://pp.myapp.com/ma_icon/0/icon_54211516_1640055072/96", "recommendId": "BwYCGwIAAw+ZEBUsRgxHTlhuZ3RpcGdYR3RRASxgInYAiAyWAKyyYwNTsckACAoAATIAAxD5QAILCgACMgADEPxABwsKAAMyAAMQ/UAICwoABDIAAxD+QAgLCgAFMgADEP9ABgsKAAYyAAMRB0ACCwoABzIAAxEIQAILCgAIMgADEQlAAgvVQCH2J+A+Qlvs/A/wEAP2EQ8GCeaWl+WcsOS4uyABugv2EjE1NDIxMTUxNl8xNjYwNzM0MzQ1ODE3NTI0NzU5M180MjA1NDk3Nzk1NjE4MjU2OTc2+BMADQYCMTAWGjc5OjAuMjIyNzk5MzU5OjAuOTAzOTI4ODg1BgIxMxZodHc9MC42NTp0Z3c9MDp0YWd3PTA6cWlzPTM6cWlyPTAuNjpsZGFzPTA6Y3ZyPTA6cW0yc2ltPTA6bWF0Y2hfdHlwZT0yOnNlYXJjaF9sdmw9MjpycV9tYXRjaDo1OnFjX21hdGNoOjUGBDIwMTgWCzAuMDI1MjEzNDI1BgQyMDE5FgnmlpflnLDkuLsGCGFjY3VyYXRlFgExBgRwY3ZyFgowLjAyNzc2ODkxBgVxdWVyeRYJ5paX5Zyw5Li7BgpxdWVyeV9mbGFnFgExBgxyZXdyaXRlcXVlcnkWAAYDdGd3FgYwLjAwMDEGAnR3FgQwLjY1Bgl1c2VyX3R5cGUWATAGBnpwcmljZRYBMPUUAAAAAAAAAAD9/wAMfwAEEODrHQ==" } ] } """.trimIndent() } ================================================ FILE: app/src/main/java/zlc/season/downloadxdemo/AppListResp.kt ================================================ package zlc.season.downloadxdemo import com.google.gson.annotations.SerializedName import kotlinx.coroutines.Job import zlc.season.yasha.YashaItem data class AppListResp( @SerializedName("appList") val appList: List = listOf(), ) { data class AppInfo( @SerializedName("appId") val appId: Int = 0, @SerializedName("apkMd5") val apkMd5: String = "", @SerializedName("apkUrl") val apkUrl: String = "", @SerializedName("appDownCount") val appDownCount: Long = 0, @SerializedName("appName") val appName: String = "", @SerializedName("averageRating") val averageRating: Double = 0.0, @SerializedName("categoryId") val categoryId: Int = 0, @SerializedName("categoryName") val categoryName: String = "", @SerializedName("editorIntro") val editorIntro: String = "", @SerializedName("fileSize") val fileSize: Int = 0, @SerializedName("iconUrl") val iconUrl: String = "", @SerializedName("images") val images: List = listOf(), @SerializedName("pkgName") val pkgName: String = "", @SerializedName("versionCode") val versionCode: Int = 0, @SerializedName("versionName") val versionName: String = "" ) : YashaItem { @Transient var progressJob: Job? = null } } ================================================ FILE: app/src/main/java/zlc/season/downloadxdemo/DetailActivity.kt ================================================ package zlc.season.downloadxdemo import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import coil.load import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.* import zlc.season.bracer.mutableParams import zlc.season.bracer.params import zlc.season.downloadx.State import zlc.season.downloadx.download import zlc.season.downloadxdemo.databinding.ActivityDetailBinding class DetailActivity : AppCompatActivity() { val binding by lazy { ActivityDetailBinding.inflate(layoutInflater) } var appInfo by mutableParams() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) binding.icon.load(appInfo.iconUrl) binding.title.text = appInfo.appName binding.desc.text = appInfo.editorIntro val downloadTask = GlobalScope.download(appInfo.apkUrl) downloadTask.state() .onEach { binding.button.setState(it) } .launchIn(lifecycleScope) binding.button.setOnClickListener { when { downloadTask.isSucceed() -> { installApk(downloadTask.file()!!) } downloadTask.isStarted() -> { downloadTask.stop() } else -> { downloadTask.start() } } } } } ================================================ FILE: app/src/main/java/zlc/season/downloadxdemo/HistoryActivity.kt ================================================ package zlc.season.downloadxdemo import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import zlc.season.downloadxdemo.databinding.ActivityHistoryBinding class HistoryActivity : AppCompatActivity() { val binding by lazy { ActivityHistoryBinding.inflate(layoutInflater) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) } } ================================================ FILE: app/src/main/java/zlc/season/downloadxdemo/MainActivity.kt ================================================ package zlc.season.downloadxdemo import android.os.Bundle import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import coil.load import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import zlc.season.bracer.startActivity import zlc.season.downloadx.download import zlc.season.downloadxdemo.databinding.ActivityMainBinding import zlc.season.downloadxdemo.databinding.AppInfoItemBinding import zlc.season.yasha.YashaDataSource import zlc.season.yasha.YashaItem import zlc.season.yasha.linear class MainActivity : AppCompatActivity() { private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) } private val dataSource by lazy { AppListDataSource() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) dataSource.loading.onEach { binding.progress.visibility = if (it) View.VISIBLE else View.GONE }.launchIn(lifecycleScope) dataSource.retry.onEach { binding.btnRetry.visibility = if (it) View.VISIBLE else View.GONE }.launchIn(lifecycleScope) binding.btnRetry.setOnClickListener { dataSource.invalidate() } binding.recyclerView.linear(dataSource) { renderBindingItem { onAttach { val downloadTask = GlobalScope.download(data.apkUrl) data.progressJob?.cancel() data.progressJob = downloadTask.state() .onEach { itemBinding.button.setState(it) } .launchIn(lifecycleScope) } onBind { itemBinding.title.text = data.appName itemBinding.desc.text = data.editorIntro itemBinding.icon.load(data.iconUrl) itemBinding.root.setOnClickListener { startActivity { appInfo = data } } itemBinding.button.setOnClickListener { val downloadTask = GlobalScope.download(data.apkUrl) when { downloadTask.isSucceed() -> { installApk(downloadTask.file()!!) } downloadTask.isStarted() -> { downloadTask.stop() } else -> { downloadTask.start() } } } } onDetach { data.progressJob?.cancel() } } } } class AppListDataSource : YashaDataSource() { val retry = MutableStateFlow(false) val loading = MutableStateFlow(false) override suspend fun loadInitial(): List { return try { loading.value = true retry.value = false AppInfoManager.getAppInfoList() } catch (e: Exception) { e.printStackTrace() retry.value = true emptyList() } finally { loading.value = false } } } } ================================================ FILE: app/src/main/java/zlc/season/downloadxdemo/ProgressButton.kt ================================================ package zlc.season.downloadxdemo import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import android.widget.FrameLayout import zlc.season.downloadx.Progress import zlc.season.downloadx.State import zlc.season.downloadxdemo.databinding.LayoutProgressButtonBinding class ProgressButton @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : FrameLayout(context, attrs, defStyleAttr) { val binding = LayoutProgressButtonBinding.inflate(LayoutInflater.from(context), this, true) fun setState(state: State) { binding.progress.max = state.progress.totalSize.toInt() binding.progress.progress = state.progress.downloadSize.toInt() when (state) { is State.None -> { binding.button.text = "下载" } is State.Waiting -> { binding.button.text = "等待中" } is State.Downloading -> { binding.button.text = state.progress.percentStr() } is State.Failed -> { binding.button.text = "重试" } is State.Stopped -> { binding.button.text = "继续" } is State.Succeed -> { binding.button.text = "安装" } } } } ================================================ FILE: app/src/main/java/zlc/season/downloadxdemo/TestActivity.kt ================================================ package zlc.season.downloadxdemo import android.os.Bundle import android.util.Log import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import zlc.season.downloadx.download import zlc.season.downloadxdemo.databinding.ActivityTestBinding class TestActivity : AppCompatActivity() { var stateJob: Job? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = ActivityTestBinding.inflate(layoutInflater) setContentView(binding.root) val downloadTask = GlobalScope.download("http://imtt.dd.qq.com/sjy.40001/sjy.00002/16891/apk/A7120AEB0DE5C59E3415C33149F5F6BA.apk?fsname=com.tencent.tmgp.ibirdgame.doudizhu_1.3_3.apk&csr=81e7") binding.btnTest.setOnClickListener { downloadTask.start() stateJob?.cancel() stateJob = downloadTask.state(1000) .onEach { Log.e("Download", "state -> $it, progress -> ${it.progress.percentStr()}") } .launchIn(lifecycleScope) } } } ================================================ FILE: app/src/main/java/zlc/season/downloadxdemo/Utils.kt ================================================ package zlc.season.downloadxdemo import android.content.Context import android.content.Intent import android.content.Intent.* import android.net.Uri.fromFile import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION_CODES.N import androidx.core.content.FileProvider.getUriForFile import java.io.File fun Context.installApk(file: File) { val intent = Intent(ACTION_VIEW) val authority = "$packageName.provider" val uri = if (SDK_INT >= N) { getUriForFile(this, authority, file) } else { fromFile(file) } intent.setDataAndType(uri, "application/vnd.android.package-archive") intent.addFlags(FLAG_ACTIVITY_NEW_TASK) intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION) startActivity(intent) } ================================================ FILE: app/src/main/res/drawable/ic_launcher_background.xml ================================================ ================================================ FILE: app/src/main/res/drawable/progress.xml ================================================ ================================================ FILE: app/src/main/res/drawable-v24/ic_launcher_foreground.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_detail.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_history.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_main.xml ================================================