Full Code of ssseasonnn/DownloadX for AI

main 2a307fa435d7 cached
71 files
125.7 KB
37.5k tokens
1 requests
Download .txt
Repository: ssseasonnn/DownloadX
Branch: main
Commit: 2a307fa435d7
Files: 71
Total size: 125.7 KB

Directory structure:
gitextract_xtdo3wj0/

├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.ch.md
├── README.md
├── app/
│   ├── .gitignore
│   ├── build.gradle
│   ├── proguard-rules.pro
│   └── src/
│       ├── androidTest/
│       │   └── java/
│       │       └── zlc/
│       │           └── season/
│       │               └── downloadxdemo/
│       │                   └── ExampleInstrumentedTest.kt
│       ├── main/
│       │   ├── AndroidManifest.xml
│       │   ├── java/
│       │   │   └── zlc/
│       │   │       └── season/
│       │   │           └── downloadxdemo/
│       │   │               ├── ApkFileProvider.kt
│       │   │               ├── AppInfoManager.kt
│       │   │               ├── AppListResp.kt
│       │   │               ├── DetailActivity.kt
│       │   │               ├── HistoryActivity.kt
│       │   │               ├── MainActivity.kt
│       │   │               ├── ProgressButton.kt
│       │   │               ├── TestActivity.kt
│       │   │               └── Utils.kt
│       │   └── res/
│       │       ├── drawable/
│       │       │   ├── ic_launcher_background.xml
│       │       │   └── progress.xml
│       │       ├── drawable-v24/
│       │       │   └── ic_launcher_foreground.xml
│       │       ├── layout/
│       │       │   ├── activity_detail.xml
│       │       │   ├── activity_history.xml
│       │       │   ├── activity_main.xml
│       │       │   ├── activity_test.xml
│       │       │   ├── app_info_item.xml
│       │       │   ├── history_item.xml
│       │       │   └── layout_progress_button.xml
│       │       ├── mipmap-anydpi-v26/
│       │       │   ├── ic_launcher.xml
│       │       │   └── ic_launcher_round.xml
│       │       ├── values/
│       │       │   ├── colors.xml
│       │       │   ├── strings.xml
│       │       │   └── styles.xml
│       │       └── xml/
│       │           ├── apk_file_provider.xml
│       │           └── network_config.xml
│       └── test/
│           └── java/
│               └── zlc/
│                   └── season/
│                       └── downloadxdemo/
│                           └── ExampleUnitTest.kt
├── build.gradle
├── downloadx/
│   ├── .gitignore
│   ├── build.gradle
│   ├── consumer-rules.pro
│   ├── proguard-rules.pro
│   └── src/
│       ├── androidTest/
│       │   └── java/
│       │       └── zlc/
│       │           └── season/
│       │               └── downloadx/
│       │                   └── ExampleInstrumentedTest.kt
│       ├── main/
│       │   ├── AndroidManifest.xml
│       │   └── java/
│       │       └── zlc/
│       │           └── season/
│       │               └── downloadx/
│       │                   ├── DownloadX.kt
│       │                   ├── Progress.kt
│       │                   ├── State.kt
│       │                   ├── core/
│       │                   │   ├── DownloadConfig.kt
│       │                   │   ├── DownloadParam.kt
│       │                   │   ├── DownloadQueue.kt
│       │                   │   ├── DownloadTask.kt
│       │                   │   ├── Downloader.kt
│       │                   │   ├── Extensions.kt
│       │                   │   ├── NormalDownloader.kt
│       │                   │   ├── RangeDownloader.kt
│       │                   │   ├── RangeTmpFile.kt
│       │                   │   └── TaskManager.kt
│       │                   ├── helper/
│       │                   │   ├── Default.kt
│       │                   │   └── Request.kt
│       │                   └── utils/
│       │                       ├── FileUtils.kt
│       │                       ├── HttpUtil.kt
│       │                       ├── LogUtil.kt
│       │                       └── Util.kt
│       └── test/
│           └── java/
│               └── zlc/
│                   └── season/
│                       └── downloadx/
│                           └── ExampleUnitTest.kt
├── gradle/
│   └── wrapper/
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── jitpack.yml
└── settings.gradle

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
*.iml
.gradle
.idea
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx


================================================
FILE: CHANGELOG.md
================================================
# Changelog
All notable changes to this project will be documented in this file.

## [DownloadX v1.0.2] - 2021-04-07
- Fix error crash
- Add http client factory

## [DownloadX v1.0.1] - 2021-03-11
- Fix stop bug
- Add remove method

## [DownloadX v1.0.0] - 2021-03-11
- DownloadX basic


================================================
FILE: LICENSE
================================================
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: README.ch.md
================================================
![](usage.png)

# DownloadX

[![](https://jitpack.io/v/ssseasonnn/DownloadX.svg)](https://jitpack.io/#ssseasonnn/DownloadX)

基于协程打造的下载工具, 支持多线程下载和断点续传

*Read this in other languages: [中文](README.ch.md), [English](README.md), [Changelog](CHANGELOG.md)* 

## Prepare

- 添加仓库:

```gradle
maven { url 'https://jitpack.io' }
```

- 添加依赖:

```gradle
implementation "com.github.ssseasonnn:DownloadX:1.0.5"
```

## Basic Usage

```kotlin
// 创建下载任务
val downloadTask = coroutineScope.download("url")

// 监听下载进度
downloadTask.progress()
    .onEach { binding.button.setProgress(it)  }
    .launchIn(lifecycleScope)

// 或者监听下载状态
downloadTask.state()
    .onEach { binding.button.setState(it)  }
    .launchIn(lifecycleScope)

// 开始下载
downloadTask.start()
```

## 创建任务

- 指定CoroutineScope

如果下载任务仅限于Activity或Fragment的生命周期内,那么可以直接使用Activity或Fragment的**lifecycleScope**,即可保证在Activity或Fragment销毁的时候自动结束下载任务

> lifecycleScope是androidX中的扩展,需要添加以下依赖:
> implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'

```kotlin
class DemoActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        //activity销毁时,该下载任务自动停止
        val downloadTask = lifecycleScope.download("url")
        downloadTask.start()
    }
}
```

如果下载任务需要在多个Activity之间共享,或者进行后台下载,那么直接使用**GlobalScope**即可

```kotlin
class DemoActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        //activity销毁时,该下载任务仍然继续下载
        val downloadTask = GlobalScope.download("url")
        downloadTask.start()
    }
}
```

- 设置保存文件名和保存路径

直接传给download方法:

```kotlin
val downloadTask = GlobalScope.download("url", "saveName", "savePath")
```

创建自定义DownloadParam:

```kotlin
val downloadParam = DownloadParam("url", "saveName", "savePath")
val downloadTask = lifecycleScope.download(downloadParam)
```

默认情况下,我们使用**url**作为**DownloadTask**的唯一标示,当需要改变这一默认行为时,可以自定义自己的**DownloadParam**:

```kotlin
class CustomDownloadParam(url: String, saveName: String, savePath: String) : DownloadParam(url, saveName, savePath) {
    override fun tag(): String {
        // 使用文件路径作为唯一标示
        return savePath + saveName
    }
}

val customDownloadParam = CustomDownloadParam("url", "saveName", "savePath")
val downloadTask = lifecycleScope.download(customDownloadParam)
```

在多个页面使用同样的标识(例如相同的url)创建下载任务时,将会返回同一个DownloadTask,例如:

```kotlin
// 同一个url
val url = "xxxx"

class DemoActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        //创建下载任务
        val downloadTask = GlobalScope.download(url)

        downloadTask.progress()
            .onEach { progress ->  /* 更新进度 */ }
            .launchIn(lifecycleScope)

        downloadTask.start()
    }
}

class OtherActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        //以相同的url创建下载任务,即可获取上一个页面创建的下载任务
        val downloadTask = GlobalScope.download(url)

        downloadTask.progress()
            .onEach { progress ->  /* 更新进度 */ }
            .launchIn(lifecycleScope)

        downloadTask.start()
    }
}
```

基于此,可以在任意多个页面中共享同一个下载进度和下载状态

## 进度和状态

- 只监听进度

在某些场景只需要下载的进度时,可使用这种方式

```kotlin
// 创建任务
val downloadTask = lifecycleScope.download("url")

downloadTask.progress()
    .onEach { progress ->  /* 更新进度 */ }
    .launchIn(lifecycleScope) // 使用lifecycleScope

//开始下载
downloadTask.start()
```

> 利用**lifecycleScope**可确保在Activity或Fragment销毁的时候自动解除监听


可以为progress()方法设置更新间隔,默认是200ms更新一次,如:

```kotlin
downloadTask.progress(500) // 设置为500ms更新一次进度
    .onEach { progress ->  
        // 更新进度
        setProgress(progress)
    }
    .launchIn(lifecycleScope)
```

- 监听下载状态和进度

当需要下载状态和下载进度的时候,使用这种方式获取

```kotlin
// 创建任务
val downloadTask = lifecycleScope.download("url")

downloadTask.state()
    .onEach { state ->  
        // 更新状态
        setState(state)
        // 更新进度
        setProgress(state.progress)
    }
    .launchIn(lifecycleScope)

//开始下载
downloadTask.start()
```

> state有以下值:**None,Waiting,Downloading,Stopped,Failed,Succeed**

同样的,可以为state()方法设置进度更新间隔


## 启动和停止

- 开始下载

```kotlin
downloadTask.start()
```

- 停止下载

```kotlin
downloadTask.stop()
```

- 删除下载

```kotlin
downloadTask.remove()
```

## License

> ```
> Copyright 2021 Season.Zlc
>
> Licensed under the Apache License, Version 2.0 (the "License");
> you may not use this file except in compliance with the License.
> You may obtain a copy of the License at
>
>    http://www.apache.org/licenses/LICENSE-2.0
>
> Unless required by applicable law or agreed to in writing, software
> distributed under the License is distributed on an "AS IS" BASIS,
> WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
> See the License for the specific language governing permissions and
> limitations under the License.
> ```


================================================
FILE: README.md
================================================
![](usage.png)

# DownloadX

[![](https://jitpack.io/v/ssseasonnn/DownloadX.svg)](https://jitpack.io/#ssseasonnn/DownloadX)

A multi-threaded download tool written with Coroutine and Kotlin

*Read this in other languages: [中文](README.ch.md), [English](README.md), [Changelog](CHANGELOG.md)* 

## Prepare

- Add jitpack repo:

```gradle
maven { url 'https://jitpack.io' }
```
    
- Add dependency:

```gradle
implementation "com.github.ssseasonnn:DownloadX:1.0.5"
```

## Basic Usage

```kotlin
// create download task
val downloadTask = coroutineScope.download("url")

// listen download progress
downloadTask.progress()
    .onEach { binding.button.setProgress(it)  }
    .launchIn(lifecycleScope)

// or listen download state
downloadTask.state()
    .onEach { binding.button.setState(it)  }
    .launchIn(lifecycleScope)

// start download
downloadTask.start()
```

## Create task

- Specify CoroutineScope

If the download task is limited to the lifecycle of the Activity or Fragment, you can directly use the **lifecycleScope** of the Activity or Fragment to ensure that the download task ends automatically when the Activity or Fragment is destroyed

> lifecycleScope is an extension in androidX, you need to add the following dependencies:
> implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'

```kotlin
class DemoActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        //When the activity is destroyed, the download task automatically stops
        val downloadTask = lifecycleScope.download("url")
        downloadTask.start()
    }
}
```

If the download task needs to be shared between multiple activities, or download in the background, then directly use **GlobalScope**

```kotlin
class DemoActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        //When the activity is destroyed, the download task still continues to download
        val downloadTask = GlobalScope.download("url")
        downloadTask.start()
    }
}
```

- Set the file name and save path

Pass directly to the download method:

```kotlin
val downloadTask = GlobalScope.download("url", "saveName", "savePath")
```

Custom DownloadParam:

```kotlin
val downloadParam = DownloadParam("url", "saveName", "savePath")
val downloadTask = lifecycleScope.download(downloadParam)
```

By default, we use **url** as the only indicator of **DownloadTask**. When you need to change this default behavior, you can customize your own **DownloadParam**:

```kotlin
class CustomDownloadParam(url: String, saveName: String, savePath: String) : DownloadParam(url, saveName, savePath) {
    override fun tag(): String {
        // Use the file path as a unique identifier
        return savePath + saveName
    }
}

val customDownloadParam = CustomDownloadParam("url", "saveName", "savePath")
val downloadTask = lifecycleScope.download(customDownloadParam)
```

When multiple pages use the same identifier (for example, the same url) to create a download task, the same DownloadTask will be returned, for example:

```kotlin
// same url
val url = "xxxx"

class DemoActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        //Create download task
        val downloadTask = GlobalScope.download(url)

        downloadTask.progress()
            .onEach { progress ->  /* update progress */ }
            .launchIn(lifecycleScope)

        downloadTask.start()
    }
}

class OtherActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        //Create a download task with the same url to get the download task created on the previous page
        val downloadTask = GlobalScope.download(url)

        downloadTask.progress()
            .onEach { progress ->  /* update progress */ }
            .launchIn(lifecycleScope)

        downloadTask.start()
    }
}
```

Based on this, the same download progress and download status can be shared on any number of pages

## Progress and State

- Listen progress only

This method can be used in certain scenarios when only the download progress is needed

```kotlin
val downloadTask = lifecycleScope.download("url")

downloadTask.progress()
    .onEach { progress ->  /* update progress */ }
    .launchIn(lifecycleScope) // using lifecycleScope

downloadTask.start()
```

> Use **lifecycleScope** to ensure that the monitoring is automatically released when the Activity or Fragment is destroyed


You can set the update interval for the progress() method. The default is to update every 200ms, such as:

```kotlin
downloadTask.progress(500) // Set to update the progress every 500ms
    .onEach { progress ->  
        // update progress
        setProgress(progress)
    }
    .launchIn(lifecycleScope)
```

- Listen progress and state

When you need download status and download progress, use this method to get

```kotlin
val downloadTask = lifecycleScope.download("url")

downloadTask.state()
    .onEach { state ->  
        // update state
        setState(state)
        // update progress
        setProgress(state.progress)
    }
    .launchIn(lifecycleScope)

downloadTask.start()
```

> state has the following values:**None,Waiting,Downloading,Stopped,Failed,Succeed**

Similarly, you can set the progress update interval for the state() method


## Start and Stop

- Start download

```kotlin
downloadTask.start()
```

- Stop download

```kotlin
downloadTask.stop()
```

- Remove download

```kotlin
downloadTask.remove()
```


## License

> ```
> Copyright 2021 Season.Zlc
>
> Licensed under the Apache License, Version 2.0 (the "License");
> you may not use this file except in compliance with the License.
> You may obtain a copy of the License at
>
>    http://www.apache.org/licenses/LICENSE-2.0
>
> Unless required by applicable law or agreed to in writing, software
> distributed under the License is distributed on an "AS IS" BASIS,
> WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
> See the License for the specific language governing permissions and
> limitations under the License.
> ```


================================================
FILE: app/.gitignore
================================================
/build


================================================
FILE: app/build.gradle
================================================
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'

android {
    compileSdkVersion 28

    defaultConfig {
        applicationId "zlc.season.downloadxdemo"
        minSdkVersion 21
        //noinspection ExpiredTargetSdkVersion
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    lintOptions {
        abortOnError false
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }

    buildFeatures {
        viewBinding true
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'com.google.android.material:material:1.4.0'
    implementation 'androidx.core:core-ktx:1.3.2'
    implementation 'androidx.fragment:fragment-ktx:1.2.5'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    implementation "io.coil-kt:coil:1.1.1"
    implementation 'com.github.ssseasonnn:Yasha:1.1.4'
    implementation 'com.github.ssseasonnn:Bracer:1.0.7'

    implementation project(path: ':downloadx')

    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}


================================================
FILE: app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
#   http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
#   public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile


================================================
FILE: app/src/androidTest/java/zlc/season/downloadxdemo/ExampleInstrumentedTest.kt
================================================
package zlc.season.downloadxdemo

import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4

import org.junit.Test
import org.junit.runner.RunWith

import org.junit.Assert.*

/**
 * Instrumented test, which will execute on an Android device.
 *
 * See [testing documentation](http://d.android.com/tools/testing).
 */
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
    @Test
    fun useAppContext() {
        // Context of the app under test.
        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
        assertEquals("zlc.season.downloadxdemo", appContext.packageName)
    }
}


================================================
FILE: app/src/main/AndroidManifest.xml
================================================
<?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'
Download .txt
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": "![](usage.png)\n\n# DownloadX\n\n[![](https://jitpack.io/v/ssseasonnn/DownloadX.svg)](https://jitpack.io/#ssseasonnn/Downloa"
  },
  {
    "path": "README.md",
    "chars": 6312,
    "preview": "![](usage.png)\n\n# DownloadX\n\n[![](https://jitpack.io/v/ssseasonnn/DownloadX.svg)](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.

Copied to clipboard!