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
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="zlc.season.downloadxdemo">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".TestActivity"
android:exported="true">
</activity>
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".DetailActivity" />
<activity android:name=".HistoryActivity" />
<provider
android:name=".ApkFileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/apk_file_provider" />
</provider>
</application>
</manifest>
================================================
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<AppListResp.AppInfo> {
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<AppInfo> = 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<String> = 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<AppListResp.AppInfo>()
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<AppListResp.AppInfo, AppInfoItemBinding> {
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<DetailActivity> {
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<YashaItem> {
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
================================================
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
================================================
FILE: app/src/main/res/drawable/progress.xml
================================================
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape>
<corners android:radius="20dp" />
<solid android:color="#EFB33B" />
<stroke
android:width="0.5dp"
android:color="@android:color/holo_orange_dark" />
</shape>
</item>
<item android:id="@android:id/progress">
<clip>
<shape>
<corners android:radius="20dp" />
<solid android:color="@android:color/holo_orange_dark" />
</shape>
</clip>
</item>
</layer-list>
================================================
FILE: app/src/main/res/drawable-v24/ic_launcher_foreground.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
================================================
FILE: app/src/main/res/layout/activity_detail.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:background="@android:color/darker_gray" />
<ImageView
android:id="@+id/icon"
android:layout_width="90dp"
android:layout_height="90dp"
android:layout_marginTop="120dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:background="@color/colorAccent" />
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textColor="@android:color/black"
android:textSize="20sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/icon"
tools:text="title" />
<TextView
android:id="@+id/desc"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="10dp"
android:layout_marginLeft="10dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="10dp"
android:layout_marginBottom="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title"
tools:text="desc" />
<zlc.season.downloadxdemo.ProgressButton
android:id="@+id/button"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:layout_marginBottom="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
================================================
FILE: app/src/main/res/layout/activity_history.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
================================================
FILE: app/src/main/res/layout/activity_main.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btn_retry"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="重试"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ProgressBar
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
================================================
FILE: app/src/main/res/layout/activity_test.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".TestActivity">
<Button
android:id="@+id/btn_test"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="test" />
</FrameLayout>
================================================
FILE: app/src/main/res/layout/app_info_item.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp">
<ImageView
android:id="@+id/icon"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginLeft="20dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@color/colorAccent" />
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginLeft="10dp"
android:layout_marginEnd="10dp"
android:layout_marginRight="10dp"
android:maxLines="1"
android:textColor="@android:color/black"
android:textSize="15sp"
app:layout_constraintEnd_toStartOf="@+id/button"
app:layout_constraintStart_toEndOf="@+id/icon"
app:layout_constraintTop_toTopOf="@+id/icon"
tools:text="title" />
<TextView
android:id="@+id/desc"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="10dp"
android:layout_marginLeft="10dp"
android:layout_marginTop="2dp"
android:layout_marginEnd="10dp"
android:layout_marginRight="10dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="1"
app:layout_constraintBottom_toBottomOf="@+id/icon"
app:layout_constraintEnd_toStartOf="@+id/button"
app:layout_constraintStart_toEndOf="@+id/icon"
app:layout_constraintTop_toBottomOf="@+id/title"
tools:text="desc" />
<zlc.season.downloadxdemo.ProgressButton
android:id="@+id/button"
android:layout_width="70dp"
android:layout_height="40dp"
android:layout_marginRight="20dp"
app:layout_constraintBottom_toBottomOf="@+id/icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/icon" />
</androidx.constraintlayout.widget.ConstraintLayout>
================================================
FILE: app/src/main/res/layout/history_item.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp">
<ImageView
android:id="@+id/icon"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_marginLeft="20dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@color/colorAccent" />
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginLeft="10dp"
android:layout_marginEnd="10dp"
android:layout_marginRight="10dp"
android:textColor="@android:color/black"
android:textSize="20sp"
app:layout_constraintEnd_toStartOf="@+id/button"
app:layout_constraintStart_toEndOf="@+id/icon"
app:layout_constraintTop_toTopOf="@+id/icon"
tools:text="title" />
<ProgressBar
android:id="@+id/progress"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="10dp"
android:layout_marginLeft="10dp"
android:layout_marginTop="2dp"
android:layout_marginEnd="10dp"
android:layout_marginRight="10dp"
app:layout_constraintBottom_toBottomOf="@+id/icon"
app:layout_constraintEnd_toStartOf="@+id/button"
app:layout_constraintStart_toEndOf="@+id/icon"
app:layout_constraintTop_toBottomOf="@+id/title"
tools:progress="50" />
<Button
android:id="@+id/button"
android:layout_width="70dp"
android:layout_height="wrap_content"
android:layout_marginRight="20dp"
app:layout_constraintBottom_toBottomOf="@+id/icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/icon"
tools:text="下载" />
</androidx.constraintlayout.widget.ConstraintLayout>
================================================
FILE: app/src/main/res/layout/layout_progress_button.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="40dp">
<ProgressBar
android:id="@+id/progress"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="0dp"
android:layout_height="0dp"
android:progressDrawable="@drawable/progress"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:max="100"
tools:progress="90" />
<TextView
android:id="@+id/button"
android:layout_width="0dp"
android:layout_height="0dp"
android:gravity="center"
android:text="下载"
android:textColor="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
================================================
FILE: app/src/main/res/values/colors.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#6200EE</color>
<color name="colorPrimaryDark">#3700B3</color>
<color name="colorAccent">#03DAC5</color>
</resources>
================================================
FILE: app/src/main/res/values/strings.xml
================================================
<resources>
<string name="app_name">DownloadXDemo</string>
</resources>
================================================
FILE: app/src/main/res/values/styles.xml
================================================
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>
================================================
FILE: app/src/main/res/xml/apk_file_provider.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
<paths>
<external-path
name="external_files"
path="." />
<root-path
name="files"
path="" />
</paths>
</resources>
================================================
FILE: app/src/main/res/xml/network_config.xml
================================================
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
<debug-overrides>
<trust-anchors>
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</debug-overrides>
</network-security-config>
================================================
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
================================================
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="zlc.season.downloadx" />
================================================
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<String, String> = 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<String, String>): Response<ResponseBody> {
val tempHeader = mutableMapOf<String, String>().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<DownloadTask>()
private val tempMap = ConcurrentHashMap<String, DownloadTask>()
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<State>(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<Progress> {
return downloadProgressFlow.flatMapLatest {
// make sure send once
var hasSend = false
channelFlow {
while (currentCoroutineContext().isActive) {
val progress = getProgress()
if (hasSend && stateHolder.isEnd()) {
if (!ensureLast) {
break
}
}
send(progress)
"url ${param.url} progress ${progress.percentStr()}".log()
hasSend = true
if (progress.isComplete()) break
delay(interval)
}
}
}
}
/**
* @param interval 更新进度间隔时间,单位ms
*/
fun state(interval: Long = 200): Flow<State> {
return downloadStateFlow.combine(progress(interval, ensureLast = false)) { l, r -> l.apply { progress = r } }
}
suspend fun getProgress(): Progress {
return downloader?.queryProgress() ?: Progress()
}
fun getState() = stateHolder.currentState
private suspend fun notifyWaiting() {
stateHolder.updateState(stateHolder.waiting, getProgress())
downloadStateFlow.value = stateHolder.currentState
"url ${param.url} download task waiting.".log()
}
private suspend fun notifyStarted() {
stateHolder.updateState(stateHolder.downloading, getProgress())
downloadStateFlow.value = stateHolder.currentState
downloadProgressFlow.value = downloadProgressFlow.value + 1
"url ${param.url} download task start.".log()
}
private suspend fun notifyStopped() {
stateHolder.updateState(stateHolder.stopped, getProgress())
downloadStateFlow.value = stateHolder.currentState
"url ${param.url} download task stopped.".log()
}
private suspend fun notifyFailed() {
stateHolder.updateState(stateHolder.failed, getProgress())
downloadStateFlow.value = stateHolder.currentState
"url ${param.url} download task failed.".log()
}
private suspend fun notifySucceed() {
stateHolder.updateState(stateHolder.succeed, getProgress())
downloadStateFlow.value = stateHolder.currentState
"url ${param.url} download task succeed.".log()
}
private fun Progress.isComplete(): Boolean {
return totalSize > 0 && totalSize == downloadSize
}
class StateHolder {
val none by lazy { State.None() }
val waiting by lazy { State.Waiting() }
val downloading by lazy { State.Downloading() }
val stopped by lazy { State.Stopped() }
val failed by lazy { State.Failed() }
val succeed by lazy { State.Succeed() }
var currentState: State = none
fun isStarted(): Boolean {
return currentState is State.Waiting || currentState is State.Downloading
}
fun isFailed(): Boolean {
return currentState is State.Failed
}
fun isSucceed(): Boolean {
return currentState is State.Succeed
}
fun canStart(): Boolean {
return currentState is State.None || currentState is State.Failed || currentState is State.Stopped
}
fun isEnd(): Boolean {
return currentState is State.None || currentState is State.Waiting || currentState is State.Stopped || currentState is State.Failed || currentState is State.Succeed
}
fun updateState(new: State, progress: Progress): State {
currentState = new.apply { this.progress = progress }
return currentState
}
}
}
================================================
FILE: downloadx/src/main/java/zlc/season/downloadx/core/Downloader.kt
================================================
package zlc.season.downloadx.core
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.ObsoleteCoroutinesApi
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.channels.actor
import okhttp3.ResponseBody
import retrofit2.Response
import zlc.season.downloadx.Progress
import java.io.File
class QueryProgress(val completableDeferred: CompletableDeferred<Progress>)
interface Downloader {
var actor: SendChannel<QueryProgress>
suspend fun queryProgress(): Progress
suspend fun download(
downloadParam: DownloadParam,
downloadConfig: DownloadConfig,
response: Response<ResponseBody>
)
}
@OptIn(ObsoleteCoroutinesApi::class, DelicateCoroutinesApi::class)
abstract class BaseDownloader(protected val coroutineScope: CoroutineScope) : Downloader {
protected var totalSize: Long = 0L
protected var downloadSize: Long = 0L
protected var isChunked: Boolean = false
private val progress = Progress()
override var actor = GlobalScope.actor<QueryProgress>(Dispatchers.IO) {
for (each in channel) {
each.completableDeferred.complete(progress.also {
it.downloadSize = downloadSize
it.totalSize = totalSize
it.isChunked = isChunked
})
}
}
override suspend fun queryProgress(): Progress {
val ack = CompletableDeferred<Progress>()
val queryProgress = QueryProgress(ack)
actor.send(queryProgress)
return ack.await()
}
fun DownloadParam.dir(): File {
return File(savePath)
}
fun DownloadParam.file(): File {
return File(savePath, saveName)
}
}
================================================
FILE: downloadx/src/main/java/zlc/season/downloadx/core/Extensions.kt
================================================
package zlc.season.downloadx.core
import okhttp3.OkHttpClient
import okhttp3.Protocol
import okhttp3.ResponseBody
import retrofit2.Response
import zlc.season.downloadx.utils.contentLength
import zlc.season.downloadx.utils.isSupportRange
import java.io.File
import java.util.concurrent.TimeUnit
interface HttpClientFactory {
fun create(): OkHttpClient
}
object DefaultHttpClientFactory : HttpClientFactory {
override fun create(): OkHttpClient {
return OkHttpClient().newBuilder()
.protocols(listOf(Protocol.HTTP_2, Protocol.HTTP_1_1))
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
.writeTimeout(120, TimeUnit.SECONDS)
.build()
}
}
interface DownloadDispatcher {
fun dispatch(downloadTask: DownloadTask, resp: Response<ResponseBody>): Downloader
}
object DefaultDownloadDispatcher : DownloadDispatcher {
override fun dispatch(downloadTask: DownloadTask, resp: Response<ResponseBody>): Downloader {
return if (downloadTask.config.disableRangeDownload || !resp.isSupportRange()) {
NormalDownloader(downloadTask.coroutineScope)
} else {
RangeDownloader(downloadTask.coroutineScope)
}
}
}
interface FileValidator {
fun validate(
file: File,
param: DownloadParam,
resp: Response<ResponseBody>
): Boolean
}
object DefaultFileValidator : FileValidator {
override fun validate(
file: File,
param: DownloadParam,
resp: Response<ResponseBody>
): Boolean {
return file.length() == resp.contentLength()
}
}
================================================
FILE: downloadx/src/main/java/zlc/season/downloadx/core/NormalDownloader.kt
================================================
package zlc.season.downloadx.core
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.isActive
import okhttp3.ResponseBody
import okio.buffer
import okio.sink
import retrofit2.Response
import zlc.season.downloadx.utils.closeQuietly
import zlc.season.downloadx.utils.contentLength
import zlc.season.downloadx.utils.isChunked
import zlc.season.downloadx.utils.recreate
import zlc.season.downloadx.utils.shadow
import java.io.File
class NormalDownloader(coroutineScope: CoroutineScope) : BaseDownloader(coroutineScope) {
companion object {
private const val BUFFER_SIZE = 8192L
}
private var alreadyDownloaded = false
private lateinit var file: File
private lateinit var shadowFile: File
override suspend fun download(
downloadParam: DownloadParam,
downloadConfig: DownloadConfig,
response: Response<ResponseBody>
) {
try {
file = downloadParam.file()
shadowFile = file.shadow()
val contentLength = response.contentLength()
val isChunked = response.isChunked()
downloadPrepare(downloadParam, contentLength)
if (alreadyDownloaded) {
this.downloadSize = contentLength
this.totalSize = contentLength
this.isChunked = isChunked
} else {
this.totalSize = contentLength
this.downloadSize = 0
this.isChunked = isChunked
startDownload(response.body()!!)
}
} finally {
response.closeQuietly()
}
}
private fun downloadPrepare(downloadParam: DownloadParam, contentLength: Long) {
//make sure dir is exists
val fileDir = downloadParam.dir()
if (!fileDir.exists() || !fileDir.isDirectory) {
fileDir.mkdirs()
}
if (file.exists()) {
if (file.length() == contentLength) {
alreadyDownloaded = true
} else {
file.delete()
shadowFile.recreate()
}
} else {
shadowFile.recreate()
}
}
private suspend fun startDownload(body: ResponseBody) = coroutineScope {
val deferred = async(Dispatchers.IO) {
val source = body.source()
val sink = shadowFile.sink().buffer()
val buffer = sink.buffer
var readLen = source.read(buffer, BUFFER_SIZE)
while (isActive && readLen != -1L) {
downloadSize += readLen
readLen = source.read(buffer, BUFFER_SIZE)
sink.flush()
}
sink.flush()
}
deferred.await()
if (isActive) {
shadowFile.renameTo(file)
}
}
}
================================================
FILE: downloadx/src/main/java/zlc/season/downloadx/core/RangeDownloader.kt
================================================
package zlc.season.downloadx.core
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.channels.actor
import kotlinx.coroutines.channels.consumeEach
import okhttp3.ResponseBody
import retrofit2.Response
import zlc.season.downloadx.core.Range.Companion.RANGE_SIZE
import zlc.season.downloadx.utils.*
import java.io.File
@OptIn(ObsoleteCoroutinesApi::class)
class RangeDownloader(coroutineScope: CoroutineScope) : BaseDownloader(coroutineScope) {
private lateinit var file: File
private lateinit var shadowFile: File
private lateinit var tmpFile: File
private lateinit var rangeTmpFile: RangeTmpFile
override suspend fun download(
downloadParam: DownloadParam,
downloadConfig: DownloadConfig,
response: Response<ResponseBody>
) {
try {
file = downloadParam.file()
shadowFile = file.shadow()
tmpFile = file.tmp()
val alreadyDownloaded = checkFiles(downloadParam, downloadConfig, response)
if (alreadyDownloaded) {
downloadSize = response.contentLength()
totalSize = response.contentLength()
} else {
val last = rangeTmpFile.lastProgress()
downloadSize = last.downloadSize
totalSize = last.totalSize
startDownload(downloadParam, downloadConfig)
}
} finally {
response.closeQuietly()
}
}
private fun checkFiles(
param: DownloadParam,
config: DownloadConfig,
response: Response<ResponseBody>
): Boolean {
var alreadyDownloaded = false
//make sure dir is exists
val fileDir = param.dir()
if (!fileDir.exists() || !fileDir.isDirectory) {
fileDir.mkdirs()
}
val contentLength = response.contentLength()
val rangeSize = config.rangeSize
val totalRanges = response.calcRanges(rangeSize)
if (file.exists()) {
if (config.validator.validate(file, param, response)) {
alreadyDownloaded = true
} else {
file.delete()
recreateFiles(contentLength, totalRanges, rangeSize)
}
} else {
if (shadowFile.exists() && tmpFile.exists()) {
rangeTmpFile = RangeTmpFile(tmpFile)
rangeTmpFile.read()
if (!rangeTmpFile.isValid(contentLength, totalRanges)) {
recreateFiles(contentLength, totalRanges, rangeSize)
}
} else {
recreateFiles(contentLength, totalRanges, rangeSize)
}
}
return alreadyDownloaded
}
private fun recreateFiles(contentLength: Long, totalRanges: Long, rangeSize: Long) {
tmpFile.recreate()
shadowFile.recreate(contentLength)
rangeTmpFile = RangeTmpFile(tmpFile)
rangeTmpFile.write(contentLength, totalRanges, rangeSize)
}
private suspend fun startDownload(param: DownloadParam, config: DownloadConfig) {
val progressChannel = coroutineScope.actor<Int> {
channel.consumeEach { downloadSize += it }
}
rangeTmpFile.undoneRanges().parallel(max = config.rangeCurrency) {
it.download(param, config, progressChannel)
}
progressChannel.close()
shadowFile.renameTo(file)
tmpFile.delete()
}
private suspend fun Range.download(
param: DownloadParam,
config: DownloadConfig,
sendChannel: SendChannel<Int>
) = coroutineScope {
val deferred = async(Dispatchers.IO) {
val url = param.url
val rangeHeader = mapOf("Range" to "bytes=${current}-${end}")
val response = config.request(url, rangeHeader)
if (!response.isSuccessful || response.body() == null) {
throw RuntimeException("Request failed!")
}
response.body()?.use {
it.byteStream().use { source ->
val tmpFileBuffer = tmpFile.mappedByteBuffer(startByte(), RANGE_SIZE)
val shadowFileBuffer = shadowFile.mappedByteBuffer(current, remainSize())
val buffer = ByteArray(8192)
var readLen = source.read(buffer)
while (isActive && readLen != -1) {
shadowFileBuffer.put(buffer, 0, readLen)
current += readLen
tmpFileBuffer.putLong(16, current)
sendChannel.send(readLen)
readLen = source.read(buffer)
}
}
}
}
deferred.await()
}
}
================================================
FILE: downloadx/src/main/java/zlc/season/downloadx/core/RangeTmpFile.kt
================================================
package zlc.season.downloadx.core
import okio.*
import okio.ByteString.Companion.decodeHex
import zlc.season.downloadx.Progress
import java.io.File
class RangeTmpFile(private val tmpFile: File) {
private val fileHeader = FileHeader()
private val fileContent = FileContent()
fun write(totalSize: Long, totalRanges: Long, rangeSize: Long) {
tmpFile.sink().buffer().use {
fileHeader.write(it, totalSize, totalRanges)
fileContent.write(it, totalSize, totalRanges, rangeSize)
}
}
fun read() {
tmpFile.source().buffer().use {
fileHeader.read(it)
fileContent.read(it, fileHeader.totalRanges)
}
}
fun isValid(totalSize: Long, totalRanges: Long): Boolean {
return fileHeader.check(totalSize, totalRanges)
}
fun undoneRanges(): List<Range> {
return fileContent.ranges.filter { !it.isComplete() }
}
fun lastProgress(): Progress {
val totalSize = fileHeader.totalSize
val downloadSize = fileContent.downloadSize()
return Progress(downloadSize, totalSize)
}
}
/**
* Save tmp file base info
*/
private class FileHeader(
var totalSize: Long = 0L,
var totalRanges: Long = 0L
) {
companion object {
const val FILE_HEADER_MAGIC_NUMBER = "a1b2c3d4e5f6"
//How to calc: ByteString.decodeHex(FILE_HEADER_MAGIC_NUMBER).size() = 6
const val FILE_HEADER_MAGIC_NUMBER_SIZE = 6L
//total header size
const val FILE_HEADER_SIZE = FILE_HEADER_MAGIC_NUMBER_SIZE + 16L
}
fun write(sink: BufferedSink, totalSize: Long, totalRanges: Long) {
this.totalSize = totalSize
this.totalRanges = totalRanges
sink.apply {
write(FILE_HEADER_MAGIC_NUMBER.decodeHex())
writeLong(totalSize)
writeLong(totalRanges)
}
}
fun read(source: BufferedSource) {
val header = source.readByteString(FILE_HEADER_MAGIC_NUMBER_SIZE).hex()
if (header != FILE_HEADER_MAGIC_NUMBER) {
throw IllegalStateException("not a tmp file")
}
totalSize = source.readLong()
totalRanges = source.readLong()
}
fun check(totalSize: Long, totalRanges: Long): Boolean {
return this.totalSize == totalSize &&
this.totalRanges == totalRanges
}
}
/**
* Save file range info
*/
private class FileContent {
val ranges = mutableListOf<Range>()
fun write(
sink: BufferedSink,
totalSize: Long,
totalRanges: Long,
rangeSize: Long
) {
ranges.clear()
slice(totalSize, totalRanges, rangeSize)
ranges.forEach {
it.write(sink)
}
}
fun read(source: BufferedSource, totalRanges: Long) {
ranges.clear()
for (i in 0 until totalRanges) {
ranges.add(Range().read(source))
}
}
fun downloadSize(): Long {
var downloadSize = 0L
ranges.forEach {
downloadSize += it.completeSize()
}
return downloadSize
}
private fun slice(totalSize: Long, totalRanges: Long, rangeSize: Long) {
var start = 0L
for (i in 0 until totalRanges) {
val end = if (i == totalRanges - 1) {
totalSize - 1
} else {
start + rangeSize - 1
}
ranges.add(Range(i, start, start, end))
start += rangeSize
}
}
}
class Range(
var index: Long = 0L,
var start: Long = 0L,
var current: Long = 0L,
var end: Long = 0L
) {
companion object {
const val RANGE_SIZE = 32L //each Long is 8 bytes
}
fun write(sink: BufferedSink): Range {
sink.apply {
writeLong(index)
writeLong(start)
writeLong(current)
writeLong(end)
}
return this
}
fun read(source: BufferedSource): Range {
val buffer = Buffer()
source.readFully(buffer, RANGE_SIZE)
buffer.apply {
index = readLong()
start = readLong()
current = readLong()
end = readLong()
}
return this
}
fun isComplete() = (current - end) == 1L
fun remainSize() = end - current + 1
fun completeSize() = current - start
/**
* Return the starting position of the range
*/
fun startByte() = FileHeader.FILE_HEADER_SIZE + RANGE_SIZE * index
}
================================================
FILE: downloadx/src/main/java/zlc/season/downloadx/core/TaskManager.kt
================================================
package zlc.season.downloadx.core
import java.util.concurrent.ConcurrentHashMap
interface TaskManager {
fun add(task: DownloadTask): DownloadTask
fun remove(task: DownloadTask)
}
object DefaultTaskManager : TaskManager {
private val taskMap = ConcurrentHashMap<String, DownloadTask>()
override fun add(task: DownloadTask): DownloadTask {
if (taskMap[task.param.tag()] == null) {
taskMap[task.param.tag()] = task
}
return taskMap[task.param.tag()]!!
}
override fun remove(task: DownloadTask) {
taskMap.remove(task.param.tag())
}
}
================================================
FILE: downloadx/src/main/java/zlc/season/downloadx/helper/Default.kt
================================================
package zlc.season.downloadx.helper
import zlc.season.claritypotion.ClarityPotion
object Default {
/**
* 默认的保存路径
*/
val DEFAULT_SAVE_PATH = ClarityPotion.context.filesDir.path
/**
* 默认的分片大小
*/
const val DEFAULT_RANGE_SIZE = 5L * 1024 * 1024
/**
* 单个任务同时下载的分片数量
*/
const val DEFAULT_RANGE_CURRENCY = 5
/**
* 同时下载的任务数量
*/
const val MAX_TASK_NUMBER = 3
/**
* 默认的Header
*/
val RANGE_CHECK_HEADER = mapOf("Range" to "bytes=0-")
}
================================================
FILE: downloadx/src/main/java/zlc/season/downloadx/helper/Request.kt
================================================
package zlc.season.downloadx.helper
import okhttp3.OkHttpClient
import okhttp3.ResponseBody
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.HeaderMap
import retrofit2.http.Streaming
import retrofit2.http.Url
internal const val FAKE_BASE_URL = "http://www.example.com"
internal fun apiCreator(client: OkHttpClient): Api {
val retrofit = Retrofit.Builder()
.baseUrl(FAKE_BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
return retrofit.create(Api::class.java)
}
internal interface Api {
@GET
@Streaming
suspend fun get(
@Url url: String,
@HeaderMap headers: Map<String, String>
): Response<ResponseBody>
}
================================================
FILE: downloadx/src/main/java/zlc/season/downloadx/utils/FileUtils.kt
================================================
package zlc.season.downloadx.utils
import java.io.File
import java.io.RandomAccessFile
import java.nio.MappedByteBuffer
import java.nio.channels.FileChannel
fun File.shadow(): File {
val shadowPath = "$canonicalPath.download"
return File(shadowPath)
}
fun File.tmp(): File {
val tmpPath = "$canonicalPath.tmp"
return File(tmpPath)
}
fun File.recreate(length: Long = 0L) {
delete()
val created = createNewFile()
if (created) {
setLength(length)
} else {
throw IllegalStateException("File create failed!")
}
}
fun File.setLength(length: Long = 0L) {
RandomAccessFile(this, "rw").setLength(length)
}
fun File.channel(): FileChannel {
return RandomAccessFile(this, "rw").channel
}
fun File.mappedByteBuffer(position: Long, size: Long): MappedByteBuffer {
val channel = channel()
val map = channel.map(FileChannel.MapMode.READ_WRITE, position, size)
channel.closeQuietly()
return map
}
fun File.clear() {
val shadow = shadow()
val tmp = tmp()
shadow.delete()
tmp.delete()
delete()
}
================================================
FILE: downloadx/src/main/java/zlc/season/downloadx/utils/HttpUtil.kt
================================================
package zlc.season.downloadx.utils
import okhttp3.ResponseBody
import retrofit2.Response
import java.io.Closeable
import java.util.*
import java.util.regex.Pattern
/** Closes this, ignoring any checked exceptions. */
fun Closeable.closeQuietly() {
try {
close()
} catch (rethrown: RuntimeException) {
throw rethrown
} catch (_: Exception) {
}
}
fun Response<ResponseBody>.closeQuietly() {
body()?.closeQuietly()
errorBody()?.closeQuietly()
}
fun Response<*>.url(): String {
return raw().request.url.toString()
}
fun Response<*>.contentLength(): Long {
return header("Content-Length").toLongOrDefault(-1)
}
fun Response<*>.isChunked(): Boolean {
return header("Transfer-Encoding") == "chunked"
}
fun Response<*>.isSupportRange(): Boolean {
if (code() == 206
|| header("Content-Range").isNotEmpty()
|| header("Accept-Ranges") == "bytes"
) {
return true
}
return false
}
fun Response<*>.fileName(): String {
val url = url()
var fileName = contentDisposition()
if (fileName.isEmpty()) {
fileName = getFileNameFromUrl(url)
}
return fileName
}
fun Response<*>.calcRanges(rangeSize: Long): Long {
val totalSize = contentLength()
val remainder = totalSize % rangeSize
val result = totalSize / rangeSize
return if (remainder == 0L) {
result
} else {
result + 1
}
}
private fun Response<*>.contentDisposition(): String {
val contentDisposition = header("Content-Disposition").toLowerCase(Locale.getDefault())
if (contentDisposition.isEmpty()) {
return ""
}
val matcher = Pattern.compile(".*filename=(.*)").matcher(contentDisposition)
if (!matcher.find()) {
return ""
}
var result = matcher.group(1)
if (result.startsWith("\"")) {
result = result.substring(1)
}
if (result.endsWith("\"")) {
result = result.substring(0, result.length - 1)
}
result = result.replace("/", "_", false)
return result
}
fun getFileNameFromUrl(url: String): String {
var temp = url
if (temp.isNotEmpty()) {
val fragment = temp.lastIndexOf('#')
if (fragment > 0) {
temp = temp.substring(0, fragment)
}
val query = temp.lastIndexOf('?')
if (query > 0) {
temp = temp.substring(0, query)
}
val filenamePos = temp.lastIndexOf('/')
val filename = if (0 <= filenamePos) temp.substring(filenamePos + 1) else temp
if (filename.isNotEmpty() && Pattern.matches("[a-zA-Z_0-9.\\-()%]+", filename)) {
return filename
}
}
return ""
}
private fun Response<*>.header(key: String): String {
val header = headers()[key]
return header ?: ""
}
================================================
FILE: downloadx/src/main/java/zlc/season/downloadx/utils/LogUtil.kt
================================================
package zlc.season.downloadx.utils
import android.util.Log
var LOG_ENABLE = false
const val LOG_TAG = "DownloadX"
fun <T> T.log(prefix: String = ""): T {
val prefixStr = if (prefix.isEmpty()) "" else "[$prefix] "
if (LOG_ENABLE) {
if (this is Throwable) {
Log.w(LOG_TAG, prefixStr + this.message, this)
} else {
Log.d(LOG_TAG, prefixStr + toString())
}
}
return this
}
================================================
FILE: downloadx/src/main/java/zlc/season/downloadx/utils/Util.kt
================================================
package zlc.season.downloadx.utils
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.consumeEach
import java.math.BigDecimal
import java.util.concurrent.atomic.AtomicInteger
fun String.toLongOrDefault(defaultValue: Long): Long {
return try {
toLong()
} catch (_: NumberFormatException) {
defaultValue
}
}
fun Long.formatSize(): String {
require(this >= 0) { "Size must larger than 0." }
val byte = this.toDouble()
val kb = byte / 1024.0
val mb = byte / 1024.0 / 1024.0
val gb = byte / 1024.0 / 1024.0 / 1024.0
val tb = byte / 1024.0 / 1024.0 / 1024.0 / 1024.0
return when {
tb >= 1 -> "${tb.decimal(2)} TB"
gb >= 1 -> "${gb.decimal(2)} GB"
mb >= 1 -> "${mb.decimal(2)} MB"
kb >= 1 -> "${kb.decimal(2)} KB"
else -> "${byte.decimal(2)} B"
}
}
fun Double.decimal(digits: Int): Double {
return this.toBigDecimal()
.setScale(digits, BigDecimal.ROUND_HALF_UP)
.toDouble()
}
infix fun Long.ratio(bottom: Long): Double {
if (bottom <= 0) {
return 0.0
}
val result = (this * 100.0).toBigDecimal()
.divide((bottom * 1.0).toBigDecimal(), 2, BigDecimal.ROUND_FLOOR)
return result.toDouble()
}
suspend fun <T, R> (Collection<T>).parallel(
dispatcher: CoroutineDispatcher = Dispatchers.Default,
max: Int = 2,
action: suspend CoroutineScope.(T) -> R
): Iterable<R> = coroutineScope {
val list = this@parallel
if (list.isEmpty()) return@coroutineScope listOf<R>()
val channel = Channel<T>()
val output = Channel<R>()
val counter = AtomicInteger(0)
launch {
list.forEach { channel.send(it) }
channel.close()
}
repeat(max) {
launch(dispatcher) {
channel.consumeEach {
output.send(action(it))
val completed = counter.incrementAndGet()
if (completed == list.size) {
output.close()
}
}
}
}
val results = mutableListOf<R>()
for (item in output) {
results.add(item)
}
return@coroutineScope results
}
================================================
FILE: downloadx/src/test/java/zlc/season/downloadx/ExampleUnitTest.kt
================================================
package zlc.season.downloadx
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: gradle/wrapper/gradle-wrapper.properties
================================================
#Wed Dec 09 15:04:10 CST 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
================================================
FILE: gradle.properties
================================================
## For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
#
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx1024m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
#
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
#Fri Sep 30 10:30:25 CST 2022
kotlin.code.style=official
org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M"
android.useAndroidX=true
android.enableJetifier=true
================================================
FILE: gradlew
================================================
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ]; do
ls=$(ls -ld "$PRG")
link=$(expr "$ls" : '.*-> \(.*\)$')
if expr "$link" : '/.*' >/dev/null; then
PRG="$link"
else
PRG=$(dirname "$PRG")"/$link"
fi
done
SAVED="$(pwd)"
cd "$(dirname \"$PRG\")/" >/dev/null
APP_HOME="$(pwd -P)"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=$(basename "$0")
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn() {
echo "$*"
}
die() {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$(uname)" in
CYGWIN*)
cygwin=true
;;
Darwin*)
darwin=true
;;
MINGW*)
msys=true
;;
NONSTOP*)
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ]; then
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ]; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ]; then
MAX_FD_LIMIT=$(ulimit -H -n)
if [ $? -eq 0 ]; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ]; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ]; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin; then
APP_HOME=$(cygpath --path --mixed "$APP_HOME")
CLASSPATH=$(cygpath --path --mixed "$CLASSPATH")
JAVACMD=$(cygpath --unix "$JAVACMD")
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=$(find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null)
SEP=""
for dir in $ROOTDIRSRAW; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ]; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@"; do
CHECK=$(echo "$arg" | egrep -c "$OURCYGPATTERN" -)
CHECK2=$(echo "$arg" | egrep -c "^-") ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ]; then ### Added a condition
eval $(echo args$i)=$(cygpath --path --ignore --mixed "$arg")
else
eval $(echo args$i)="\"$arg\""
fi
i=$((i + 1))
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save() {
for i; do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/"; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"
================================================
FILE: gradlew.bat
================================================
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: jitpack.yml
================================================
jdk: openjdk11
install:
- ./gradlew build :downloadx:publishToMavenLocal
================================================
FILE: settings.gradle
================================================
rootProject.name='DownloadXDemo'
include ':app'
include ':downloadx'
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
Condensed preview — 71 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (142K chars).
[
{
"path": ".gitignore",
"chars": 214,
"preview": "*.iml\n.gradle\n.idea\n/local.properties\n/.idea/caches\n/.idea/libraries\n/.idea/modules.xml\n/.idea/workspace.xml\n/.idea/navE"
},
{
"path": "CHANGELOG.md",
"chars": 286,
"preview": "# Changelog\nAll notable changes to this project will be documented in this file.\n\n## [DownloadX v1.0.2] - 2021-04-07\n- F"
},
{
"path": "LICENSE",
"chars": 11357,
"preview": " Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.ch.md",
"chars": 4947,
"preview": "\n\n# DownloadX\n\n[](https://jitpack.io/#ssseasonnn/Downloa"
},
{
"path": "README.md",
"chars": 6312,
"preview": "\n\n# DownloadX\n\n[](https://jitpack.io/#ssseasonnn/Downloa"
},
{
"path": "app/.gitignore",
"chars": 7,
"preview": "/build\n"
},
{
"path": "app/build.gradle",
"chars": 1723,
"preview": "apply plugin: 'com.android.application'\napply plugin: 'kotlin-android'\n\nandroid {\n compileSdkVersion 28\n\n defaultC"
},
{
"path": "app/proguard-rules.pro",
"chars": 751,
"preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
},
{
"path": "app/src/androidTest/java/zlc/season/downloadxdemo/ExampleInstrumentedTest.kt",
"chars": 676,
"preview": "package zlc.season.downloadxdemo\n\nimport androidx.test.platform.app.InstrumentationRegistry\nimport androidx.test.ext.jun"
},
{
"path": "app/src/main/AndroidManifest.xml",
"chars": 1659,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n package="
},
{
"path": "app/src/main/java/zlc/season/downloadxdemo/ApkFileProvider.kt",
"chars": 115,
"preview": "package zlc.season.downloadxdemo\n\nimport androidx.core.content.FileProvider\n\nclass ApkFileProvider : FileProvider()"
},
{
"path": "app/src/main/java/zlc/season/downloadxdemo/AppInfoManager.kt",
"chars": 17867,
"preview": "package zlc.season.downloadxdemo\n\nimport com.google.gson.Gson\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.corou"
},
{
"path": "app/src/main/java/zlc/season/downloadxdemo/AppListResp.kt",
"chars": 1456,
"preview": "package zlc.season.downloadxdemo\n\n\nimport com.google.gson.annotations.SerializedName\nimport kotlinx.coroutines.Job\nimpor"
},
{
"path": "app/src/main/java/zlc/season/downloadxdemo/DetailActivity.kt",
"chars": 1482,
"preview": "package zlc.season.downloadxdemo\n\nimport android.os.Bundle\nimport androidx.appcompat.app.AppCompatActivity\nimport androi"
},
{
"path": "app/src/main/java/zlc/season/downloadxdemo/HistoryActivity.kt",
"chars": 441,
"preview": "package zlc.season.downloadxdemo\n\nimport android.os.Bundle\nimport androidx.appcompat.app.AppCompatActivity\nimport zlc.se"
},
{
"path": "app/src/main/java/zlc/season/downloadxdemo/MainActivity.kt",
"chars": 3654,
"preview": "package zlc.season.downloadxdemo\n\nimport android.os.Bundle\nimport android.view.View\nimport androidx.appcompat.app.AppCom"
},
{
"path": "app/src/main/java/zlc/season/downloadxdemo/ProgressButton.kt",
"chars": 1357,
"preview": "package zlc.season.downloadxdemo\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport android.view.La"
},
{
"path": "app/src/main/java/zlc/season/downloadxdemo/TestActivity.kt",
"chars": 1268,
"preview": "package zlc.season.downloadxdemo\n\nimport android.os.Bundle\nimport android.util.Log\nimport androidx.appcompat.app.AppComp"
},
{
"path": "app/src/main/java/zlc/season/downloadxdemo/Utils.kt",
"chars": 752,
"preview": "package zlc.season.downloadxdemo\n\nimport android.content.Context\nimport android.content.Intent\nimport android.content.In"
},
{
"path": "app/src/main/res/drawable/ic_launcher_background.xml",
"chars": 5606,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:wi"
},
{
"path": "app/src/main/res/drawable/progress.xml",
"chars": 647,
"preview": "<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n <item android:id=\"@android:id/background\">\n "
},
{
"path": "app/src/main/res/drawable-v24/ic_launcher_foreground.xml",
"chars": 1702,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:aapt=\"http://schemas.android.com/aapt\"\n "
},
{
"path": "app/src/main/res/layout/activity_detail.xml",
"chars": 2580,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas."
},
{
"path": "app/src/main/res/layout/activity_history.xml",
"chars": 729,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas."
},
{
"path": "app/src/main/res/layout/activity_main.xml",
"chars": 1580,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas."
},
{
"path": "app/src/main/res/layout/activity_test.xml",
"chars": 500,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns"
},
{
"path": "app/src/main/res/layout/app_info_item.xml",
"chars": 2465,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas."
},
{
"path": "app/src/main/res/layout/history_item.xml",
"chars": 2402,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas."
},
{
"path": "app/src/main/res/layout/layout_progress_button.xml",
"chars": 1328,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas."
},
{
"path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
"chars": 272,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n <b"
},
{
"path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
"chars": 272,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n <b"
},
{
"path": "app/src/main/res/values/colors.xml",
"chars": 208,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <color name=\"colorPrimary\">#6200EE</color>\n <color name=\"color"
},
{
"path": "app/src/main/res/values/strings.xml",
"chars": 76,
"preview": "<resources>\n <string name=\"app_name\">DownloadXDemo</string>\n</resources>\n"
},
{
"path": "app/src/main/res/values/styles.xml",
"chars": 383,
"preview": "<resources>\n\n <!-- Base application theme. -->\n <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.DarkActionBar"
},
{
"path": "app/src/main/res/xml/apk_file_provider.xml",
"chars": 237,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <paths>\n <external-path\n name=\"external_files\"\n"
},
{
"path": "app/src/main/res/xml/network_config.xml",
"chars": 281,
"preview": "<network-security-config>\n <base-config cleartextTrafficPermitted=\"true\" />\n <debug-overrides>\n <trust-anch"
},
{
"path": "app/src/test/java/zlc/season/downloadxdemo/ExampleUnitTest.kt",
"chars": 349,
"preview": "package zlc.season.downloadxdemo\n\nimport org.junit.Test\n\nimport org.junit.Assert.*\n\n/**\n * Example local unit test, whic"
},
{
"path": "build.gradle",
"chars": 739,
"preview": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\n\nbuildscript {\n e"
},
{
"path": "downloadx/.gitignore",
"chars": 7,
"preview": "/build\n"
},
{
"path": "downloadx/build.gradle",
"chars": 1826,
"preview": "apply plugin: 'com.android.library'\napply plugin: 'kotlin-android'\napply plugin: 'maven-publish'\n\nandroid {\n compileS"
},
{
"path": "downloadx/consumer-rules.pro",
"chars": 0,
"preview": ""
},
{
"path": "downloadx/proguard-rules.pro",
"chars": 751,
"preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
},
{
"path": "downloadx/src/androidTest/java/zlc/season/downloadx/ExampleInstrumentedTest.kt",
"chars": 673,
"preview": "package zlc.season.downloadx\n\nimport androidx.test.platform.app.InstrumentationRegistry\nimport androidx.test.ext.junit.r"
},
{
"path": "downloadx/src/main/AndroidManifest.xml",
"chars": 107,
"preview": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n package=\"zlc.season.downloadx\" />\n"
},
{
"path": "downloadx/src/main/java/zlc/season/downloadx/DownloadX.kt",
"chars": 879,
"preview": "package zlc.season.downloadx\n\nimport kotlinx.coroutines.CoroutineScope\nimport zlc.season.downloadx.helper.Default\nimport"
},
{
"path": "downloadx/src/main/java/zlc/season/downloadx/Progress.kt",
"chars": 845,
"preview": "package zlc.season.downloadx\n\nimport zlc.season.downloadx.utils.formatSize\nimport zlc.season.downloadx.utils.ratio\n\n\ncla"
},
{
"path": "downloadx/src/main/java/zlc/season/downloadx/State.kt",
"chars": 282,
"preview": "package zlc.season.downloadx\n\nsealed class State {\n var progress: Progress = Progress()\n internal set\n\n cla"
},
{
"path": "downloadx/src/main/java/zlc/season/downloadx/core/DownloadConfig.kt",
"chars": 1420,
"preview": "package zlc.season.downloadx.core\n\nimport okhttp3.ResponseBody\nimport retrofit2.Response\nimport zlc.season.downloadx.hel"
},
{
"path": "downloadx/src/main/java/zlc/season/downloadx/core/DownloadParam.kt",
"chars": 587,
"preview": "package zlc.season.downloadx.core\n\n\nopen class DownloadParam(\n var url: String,\n var saveName: String = \"\",\n va"
},
{
"path": "downloadx/src/main/java/zlc/season/downloadx/core/DownloadQueue.kt",
"chars": 1786,
"preview": "package zlc.season.downloadx.core\n\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.channels.Channel\nimport kotlinx"
},
{
"path": "downloadx/src/main/java/zlc/season/downloadx/core/DownloadTask.kt",
"chars": 7835,
"preview": "package zlc.season.downloadx.core\n\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.flow.*\nimport zlc.season.downlo"
},
{
"path": "downloadx/src/main/java/zlc/season/downloadx/core/Downloader.kt",
"chars": 1864,
"preview": "package zlc.season.downloadx.core\n\nimport kotlinx.coroutines.CompletableDeferred\nimport kotlinx.coroutines.CoroutineScop"
},
{
"path": "downloadx/src/main/java/zlc/season/downloadx/core/Extensions.kt",
"chars": 1644,
"preview": "package zlc.season.downloadx.core\n\nimport okhttp3.OkHttpClient\nimport okhttp3.Protocol\nimport okhttp3.ResponseBody\nimpor"
},
{
"path": "downloadx/src/main/java/zlc/season/downloadx/core/NormalDownloader.kt",
"chars": 2918,
"preview": "package zlc.season.downloadx.core\n\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport"
},
{
"path": "downloadx/src/main/java/zlc/season/downloadx/core/RangeDownloader.kt",
"chars": 4813,
"preview": "package zlc.season.downloadx.core\n\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.channels.SendChannel\nimport kot"
},
{
"path": "downloadx/src/main/java/zlc/season/downloadx/core/RangeTmpFile.kt",
"chars": 4504,
"preview": "package zlc.season.downloadx.core\n\nimport okio.*\nimport okio.ByteString.Companion.decodeHex\nimport zlc.season.downloadx."
},
{
"path": "downloadx/src/main/java/zlc/season/downloadx/core/TaskManager.kt",
"chars": 607,
"preview": "package zlc.season.downloadx.core\n\nimport java.util.concurrent.ConcurrentHashMap\n\ninterface TaskManager {\n fun add(ta"
},
{
"path": "downloadx/src/main/java/zlc/season/downloadx/helper/Default.kt",
"chars": 520,
"preview": "package zlc.season.downloadx.helper\n\nimport zlc.season.claritypotion.ClarityPotion\n\nobject Default {\n /**\n * 默认的保"
},
{
"path": "downloadx/src/main/java/zlc/season/downloadx/helper/Request.kt",
"chars": 823,
"preview": "package zlc.season.downloadx.helper\n\nimport okhttp3.OkHttpClient\nimport okhttp3.ResponseBody\nimport retrofit2.Response\ni"
},
{
"path": "downloadx/src/main/java/zlc/season/downloadx/utils/FileUtils.kt",
"chars": 1082,
"preview": "package zlc.season.downloadx.utils\n\nimport java.io.File\nimport java.io.RandomAccessFile\nimport java.nio.MappedByteBuffer"
},
{
"path": "downloadx/src/main/java/zlc/season/downloadx/utils/HttpUtil.kt",
"chars": 2796,
"preview": "package zlc.season.downloadx.utils\n\nimport okhttp3.ResponseBody\nimport retrofit2.Response\nimport java.io.Closeable\nimpor"
},
{
"path": "downloadx/src/main/java/zlc/season/downloadx/utils/LogUtil.kt",
"chars": 436,
"preview": "package zlc.season.downloadx.utils\n\nimport android.util.Log\n\nvar LOG_ENABLE = false\n\nconst val LOG_TAG = \"DownloadX\"\n\nfu"
},
{
"path": "downloadx/src/main/java/zlc/season/downloadx/utils/Util.kt",
"chars": 2210,
"preview": "package zlc.season.downloadx.utils\n\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.channels.Channel\nimport kotlin"
},
{
"path": "downloadx/src/test/java/zlc/season/downloadx/ExampleUnitTest.kt",
"chars": 345,
"preview": "package zlc.season.downloadx\n\nimport org.junit.Test\n\nimport org.junit.Assert.*\n\n/**\n * Example local unit test, which wi"
},
{
"path": "gradle/wrapper/gradle-wrapper.properties",
"chars": 232,
"preview": "#Wed Dec 09 15:04:10 CST 2020\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_"
},
{
"path": "gradle.properties",
"chars": 869,
"preview": "## For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/buil"
},
{
"path": "gradlew",
"chars": 4962,
"preview": "#!/usr/bin/env sh\n\n##############################################################################\n##\n## Gradle start up"
},
{
"path": "gradlew.bat",
"chars": 2260,
"preview": "@if \"%DEBUG%\" == \"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@r"
},
{
"path": "jitpack.yml",
"chars": 74,
"preview": "jdk: openjdk11\ninstall:\n - ./gradlew build :downloadx:publishToMavenLocal"
},
{
"path": "settings.gradle",
"chars": 69,
"preview": "rootProject.name='DownloadXDemo'\ninclude ':app'\ninclude ':downloadx'\n"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the ssseasonnn/DownloadX GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 71 files (125.7 KB), approximately 37.5k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.