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
================================================

# DownloadX
[](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
================================================

# DownloadX
[](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
================================================
================================================
FILE: app/src/main/res/layout/activity_test.xml
================================================
================================================
FILE: app/src/main/res/layout/app_info_item.xml
================================================
================================================
FILE: app/src/main/res/layout/history_item.xml
================================================
================================================
FILE: app/src/main/res/layout/layout_progress_button.xml
================================================
================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
================================================
================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
================================================
================================================
FILE: app/src/main/res/values/colors.xml
================================================
#6200EE#3700B3#03DAC5
================================================
FILE: app/src/main/res/values/strings.xml
================================================
DownloadXDemo
================================================
FILE: app/src/main/res/values/styles.xml
================================================
================================================
FILE: app/src/main/res/xml/apk_file_provider.xml
================================================
================================================
FILE: app/src/main/res/xml/network_config.xml
================================================
================================================
FILE: app/src/test/java/zlc/season/downloadxdemo/ExampleUnitTest.kt
================================================
package zlc.season.downloadxdemo
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
================================================
FILE: build.gradle
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.7.0'
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
}
dependencies {
classpath 'com.android.tools.build:gradle:7.2.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
================================================
FILE: downloadx/.gitignore
================================================
/build
================================================
FILE: downloadx/build.gradle
================================================
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'maven-publish'
android {
compileSdkVersion 28
defaultConfig {
minSdkVersion 16
//noinspection ExpiredTargetSdkVersion
targetSdkVersion 28
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles 'consumer-rules.pro'
}
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'
}
}
task androidSourcesJar(type: Jar) {
classifier 'sources'
from android.sourceSets.main.java.srcDirs
}
project.afterEvaluate {
publishing {
publications {
release(MavenPublication) {
from components.release
artifact androidSourcesJar // optional sources
}
}
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
api 'androidx.appcompat:appcompat:1.2.0'
api 'androidx.core:core-ktx:1.3.2'
api 'com.squareup.retrofit2:retrofit:2.9.0'
api 'com.squareup.retrofit2:converter-gson:2.9.0'
api "com.squareup.okhttp3:okhttp:4.9.0"
api "com.squareup.okio:okio:2.9.0"
api 'com.github.ssseasonnn:ClarityPotion:1.0.4'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}
================================================
FILE: downloadx/consumer-rules.pro
================================================
================================================
FILE: downloadx/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: downloadx/src/androidTest/java/zlc/season/downloadx/ExampleInstrumentedTest.kt
================================================
package zlc.season.downloadx
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.downloadx.test", appContext.packageName)
}
}
================================================
FILE: downloadx/src/main/AndroidManifest.xml
================================================
================================================
FILE: downloadx/src/main/java/zlc/season/downloadx/DownloadX.kt
================================================
package zlc.season.downloadx
import kotlinx.coroutines.CoroutineScope
import zlc.season.downloadx.helper.Default
import zlc.season.downloadx.core.DownloadTask
import zlc.season.downloadx.core.DownloadParam
import zlc.season.downloadx.core.DownloadConfig
fun CoroutineScope.download(
url: String,
saveName: String = "",
savePath: String = Default.DEFAULT_SAVE_PATH,
downloadConfig: DownloadConfig = DownloadConfig()
): DownloadTask {
val downloadParam = DownloadParam(url, saveName, savePath)
val task = DownloadTask(this, downloadParam, downloadConfig)
return downloadConfig.taskManager.add(task)
}
fun CoroutineScope.download(
downloadParam: DownloadParam,
downloadConfig: DownloadConfig = DownloadConfig()
): DownloadTask {
val task = DownloadTask(this, downloadParam, downloadConfig)
return downloadConfig.taskManager.add(task)
}
================================================
FILE: downloadx/src/main/java/zlc/season/downloadx/Progress.kt
================================================
package zlc.season.downloadx
import zlc.season.downloadx.utils.formatSize
import zlc.season.downloadx.utils.ratio
class Progress(
var downloadSize: Long = 0,
var totalSize: Long = 0,
/**
* 用于标识一个链接是否是分块下载, 如果该值为true, 那么totalSize为-1
*/
var isChunked: Boolean = false
) {
/**
* Return total size str. eg: 10M
*/
fun totalSizeStr(): String {
return totalSize.formatSize()
}
/**
* Return download size str. eg: 3M
*/
fun downloadSizeStr(): String {
return downloadSize.formatSize()
}
/**
* Return percent number.
*/
fun percent(): Double {
if (isChunked) return 0.0
return downloadSize ratio totalSize
}
/**
* Return percent string.
*/
fun percentStr(): String {
return "${percent()}%"
}
}
================================================
FILE: downloadx/src/main/java/zlc/season/downloadx/State.kt
================================================
package zlc.season.downloadx
sealed class State {
var progress: Progress = Progress()
internal set
class None : State()
class Waiting : State()
class Downloading : State()
class Stopped : State()
class Failed : State()
class Succeed : State()
}
================================================
FILE: downloadx/src/main/java/zlc/season/downloadx/core/DownloadConfig.kt
================================================
package zlc.season.downloadx.core
import okhttp3.ResponseBody
import retrofit2.Response
import zlc.season.downloadx.helper.Default.DEFAULT_RANGE_CURRENCY
import zlc.season.downloadx.helper.Default.DEFAULT_RANGE_SIZE
import zlc.season.downloadx.helper.apiCreator
class DownloadConfig(
/**
* 禁用断点续传
*/
val disableRangeDownload: Boolean = false,
/**
* 下载管理
*/
val taskManager: TaskManager = DefaultTaskManager,
/**
* 下载队列
*/
val queue: DownloadQueue = DefaultDownloadQueue.get(),
/**
* 自定义header
*/
val customHeader: Map = emptyMap(),
/**
* 分片下载每片的大小
*/
val rangeSize: Long = DEFAULT_RANGE_SIZE,
/**
* 分片下载并行数量
*/
val rangeCurrency: Int = DEFAULT_RANGE_CURRENCY,
/**
* 下载器分发
*/
val dispatcher: DownloadDispatcher = DefaultDownloadDispatcher,
/**
* 文件校验
*/
val validator: FileValidator = DefaultFileValidator,
/**
* http client
*/
httpClientFactory: HttpClientFactory = DefaultHttpClientFactory
) {
private val api = apiCreator(httpClientFactory.create())
suspend fun request(url: String, header: Map): Response {
val tempHeader = mutableMapOf().also {
it.putAll(customHeader)
it.putAll(header)
}
return api.get(url, tempHeader)
}
}
================================================
FILE: downloadx/src/main/java/zlc/season/downloadx/core/DownloadParam.kt
================================================
package zlc.season.downloadx.core
open class DownloadParam(
var url: String,
var saveName: String = "",
var savePath: String = "",
var extra: String = ""
) {
/**
* Each task with unique tag.
*/
open fun tag() = url
override fun equals(other: Any?): Boolean {
if (other == null) return false
if (this === other) return true
return if (other is DownloadParam) {
tag() == other.tag()
} else {
false
}
}
override fun hashCode(): Int {
return tag().hashCode()
}
}
================================================
FILE: downloadx/src/main/java/zlc/season/downloadx/core/DownloadQueue.kt
================================================
package zlc.season.downloadx.core
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.consumeEach
import zlc.season.downloadx.helper.Default.MAX_TASK_NUMBER
import java.util.concurrent.*
interface DownloadQueue {
suspend fun enqueue(task: DownloadTask)
suspend fun dequeue(task: DownloadTask)
}
@OptIn(DelicateCoroutinesApi::class)
class DefaultDownloadQueue private constructor(private val maxTask: Int) : DownloadQueue {
companion object {
private val lock = Any()
private var instance: DefaultDownloadQueue? = null
fun get(maxTask: Int = MAX_TASK_NUMBER): DefaultDownloadQueue {
if (instance == null) {
synchronized(lock) {
if (instance == null) {
instance = DefaultDownloadQueue(maxTask)
}
}
}
return instance!!
}
}
private val channel = Channel()
private val tempMap = ConcurrentHashMap()
init {
GlobalScope.launch {
repeat(maxTask) {
launch {
channel.consumeEach {
if (contain(it)) {
it.suspendStart()
dequeue(it)
}
}
}
}
}
}
override suspend fun enqueue(task: DownloadTask) {
tempMap[task.param.tag()] = task
channel.send(task)
}
override suspend fun dequeue(task: DownloadTask) {
tempMap.remove(task.param.tag())
}
private fun contain(task: DownloadTask): Boolean {
return tempMap[task.param.tag()] != null
}
}
================================================
FILE: downloadx/src/main/java/zlc/season/downloadx/core/DownloadTask.kt
================================================
package zlc.season.downloadx.core
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import zlc.season.downloadx.Progress
import zlc.season.downloadx.State
import zlc.season.downloadx.helper.Default
import zlc.season.downloadx.utils.clear
import zlc.season.downloadx.utils.closeQuietly
import zlc.season.downloadx.utils.fileName
import zlc.season.downloadx.utils.log
import java.io.File
@OptIn(ExperimentalCoroutinesApi::class)
open class DownloadTask(
val coroutineScope: CoroutineScope,
val param: DownloadParam,
val config: DownloadConfig
) {
private val stateHolder by lazy { StateHolder() }
private var downloadJob: Job? = null
private var downloader: Downloader? = null
private val downloadProgressFlow = MutableStateFlow(0)
private val downloadStateFlow = MutableStateFlow(stateHolder.none)
fun isStarted(): Boolean {
return stateHolder.isStarted()
}
fun isFailed(): Boolean {
return stateHolder.isFailed()
}
fun isSucceed(): Boolean {
return stateHolder.isSucceed()
}
fun canStart(): Boolean {
return stateHolder.canStart()
}
private fun checkJob() = downloadJob?.isActive == true
/**
* 获取下载文件
*/
fun file(): File? {
return if (param.saveName.isNotEmpty() && param.savePath.isNotEmpty()) {
File(param.savePath, param.saveName)
} else {
null
}
}
/**
* 开始下载,添加到下载队列
*/
fun start() {
coroutineScope.launch {
if (checkJob()) return@launch
notifyWaiting()
try {
config.queue.enqueue(this@DownloadTask)
} catch (e: Exception) {
if (e !is CancellationException) {
notifyFailed()
}
e.log()
}
}
}
/**
* 开始下载并等待下载完成,直接开始下载,不添加到下载队列
*/
suspend fun suspendStart() {
if (checkJob()) return
downloadJob?.cancel()
val errorHandler = CoroutineExceptionHandler { _, throwable ->
throwable.log()
if (throwable !is CancellationException) {
coroutineScope.launch {
notifyFailed()
}
}
}
downloadJob = coroutineScope.launch(errorHandler + Dispatchers.IO) {
val response = config.request(param.url, Default.RANGE_CHECK_HEADER)
try {
if (!response.isSuccessful || response.body() == null) {
throw RuntimeException("request failed")
}
if (param.saveName.isEmpty()) {
param.saveName = response.fileName()
}
if (param.savePath.isEmpty()) {
param.savePath = Default.DEFAULT_SAVE_PATH
}
if (downloader == null) {
downloader = config.dispatcher.dispatch(this@DownloadTask, response)
}
notifyStarted()
val deferred = async(Dispatchers.IO) { downloader?.download(param, config, response) }
deferred.await()
notifySucceed()
} catch (e: Exception) {
if (e !is CancellationException) {
notifyFailed()
}
e.log()
} finally {
response.closeQuietly()
}
}
downloadJob?.join()
}
/**
* 停止下载
*/
fun stop() {
coroutineScope.launch {
if (isStarted()) {
config.queue.dequeue(this@DownloadTask)
downloadJob?.cancel()
notifyStopped()
}
}
}
/**
* 移除任务
*/
fun remove(deleteFile: Boolean = true) {
stop()
config.taskManager.remove(this)
if (deleteFile) {
file()?.clear()
}
}
/**
* @param interval 更新进度间隔时间,单位ms
* @param ensureLast 能否收到最后一个进度
*/
fun progress(interval: Long = 200, ensureLast: Boolean = true): Flow