[
  {
    "path": ".gitignore",
    "content": "*.iml\n.gradle\n.idea\n/local.properties\n/.idea/caches\n/.idea/libraries\n/.idea/modules.xml\n/.idea/workspace.xml\n/.idea/navEditor.xml\n/.idea/assetWizardSettings.xml\n.DS_Store\n/build\n/captures\n.externalNativeBuild\n.cxx\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\nAll notable changes to this project will be documented in this file.\n\n## [DownloadX v1.0.2] - 2021-04-07\n- Fix error crash\n- Add http client factory\n\n## [DownloadX v1.0.1] - 2021-03-11\n- Fix stop bug\n- Add remove method\n\n## [DownloadX v1.0.0] - 2021-03-11\n- DownloadX basic\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.ch.md",
    "content": "![](usage.png)\n\n# DownloadX\n\n[![](https://jitpack.io/v/ssseasonnn/DownloadX.svg)](https://jitpack.io/#ssseasonnn/DownloadX)\n\n基于协程打造的下载工具, 支持多线程下载和断点续传\n\n*Read this in other languages: [中文](README.ch.md), [English](README.md), [Changelog](CHANGELOG.md)* \n\n## Prepare\n\n- 添加仓库:\n\n```gradle\nmaven { url 'https://jitpack.io' }\n```\n\n- 添加依赖:\n\n```gradle\nimplementation \"com.github.ssseasonnn:DownloadX:1.0.5\"\n```\n\n## Basic Usage\n\n```kotlin\n// 创建下载任务\nval downloadTask = coroutineScope.download(\"url\")\n\n// 监听下载进度\ndownloadTask.progress()\n    .onEach { binding.button.setProgress(it)  }\n    .launchIn(lifecycleScope)\n\n// 或者监听下载状态\ndownloadTask.state()\n    .onEach { binding.button.setState(it)  }\n    .launchIn(lifecycleScope)\n\n// 开始下载\ndownloadTask.start()\n```\n\n## 创建任务\n\n- 指定CoroutineScope\n\n如果下载任务仅限于Activity或Fragment的生命周期内，那么可以直接使用Activity或Fragment的**lifecycleScope**，即可保证在Activity或Fragment销毁的时候自动结束下载任务\n\n> lifecycleScope是androidX中的扩展，需要添加以下依赖：\n> implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'\n\n```kotlin\nclass DemoActivity : AppCompatActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        \n        //activity销毁时，该下载任务自动停止\n        val downloadTask = lifecycleScope.download(\"url\")\n        downloadTask.start()\n    }\n}\n```\n\n如果下载任务需要在多个Activity之间共享，或者进行后台下载，那么直接使用**GlobalScope**即可\n\n```kotlin\nclass DemoActivity : AppCompatActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        \n        //activity销毁时，该下载任务仍然继续下载\n        val downloadTask = GlobalScope.download(\"url\")\n        downloadTask.start()\n    }\n}\n```\n\n- 设置保存文件名和保存路径\n\n直接传给download方法:\n\n```kotlin\nval downloadTask = GlobalScope.download(\"url\", \"saveName\", \"savePath\")\n```\n\n创建自定义DownloadParam:\n\n```kotlin\nval downloadParam = DownloadParam(\"url\", \"saveName\", \"savePath\")\nval downloadTask = lifecycleScope.download(downloadParam)\n```\n\n默认情况下，我们使用**url**作为**DownloadTask**的唯一标示，当需要改变这一默认行为时，可以自定义自己的**DownloadParam**：\n\n```kotlin\nclass CustomDownloadParam(url: String, saveName: String, savePath: String) : DownloadParam(url, saveName, savePath) {\n    override fun tag(): String {\n        // 使用文件路径作为唯一标示\n        return savePath + saveName\n    }\n}\n\nval customDownloadParam = CustomDownloadParam(\"url\", \"saveName\", \"savePath\")\nval downloadTask = lifecycleScope.download(customDownloadParam)\n```\n\n在多个页面使用同样的标识（例如相同的url）创建下载任务时，将会返回同一个DownloadTask，例如：\n\n```kotlin\n// 同一个url\nval url = \"xxxx\"\n\nclass DemoActivity : AppCompatActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        //创建下载任务\n        val downloadTask = GlobalScope.download(url)\n\n        downloadTask.progress()\n            .onEach { progress ->  /* 更新进度 */ }\n            .launchIn(lifecycleScope)\n\n        downloadTask.start()\n    }\n}\n\nclass OtherActivity : AppCompatActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        //以相同的url创建下载任务，即可获取上一个页面创建的下载任务\n        val downloadTask = GlobalScope.download(url)\n\n        downloadTask.progress()\n            .onEach { progress ->  /* 更新进度 */ }\n            .launchIn(lifecycleScope)\n\n        downloadTask.start()\n    }\n}\n```\n\n基于此，可以在任意多个页面中共享同一个下载进度和下载状态\n\n## 进度和状态\n\n- 只监听进度\n\n在某些场景只需要下载的进度时，可使用这种方式\n\n```kotlin\n// 创建任务\nval downloadTask = lifecycleScope.download(\"url\")\n\ndownloadTask.progress()\n    .onEach { progress ->  /* 更新进度 */ }\n    .launchIn(lifecycleScope) // 使用lifecycleScope\n\n//开始下载\ndownloadTask.start()\n```\n\n> 利用**lifecycleScope**可确保在Activity或Fragment销毁的时候自动解除监听\n\n\n可以为progress()方法设置更新间隔，默认是200ms更新一次，如：\n\n```kotlin\ndownloadTask.progress(500) // 设置为500ms更新一次进度\n    .onEach { progress ->  \n        // 更新进度\n        setProgress(progress)\n    }\n    .launchIn(lifecycleScope)\n```\n\n- 监听下载状态和进度\n\n当需要下载状态和下载进度的时候，使用这种方式获取\n\n```kotlin\n// 创建任务\nval downloadTask = lifecycleScope.download(\"url\")\n\ndownloadTask.state()\n    .onEach { state ->  \n        // 更新状态\n        setState(state)\n        // 更新进度\n        setProgress(state.progress)\n    }\n    .launchIn(lifecycleScope)\n\n//开始下载\ndownloadTask.start()\n```\n\n> state有以下值：**None,Waiting,Downloading,Stopped,Failed,Succeed**\n\n同样的，可以为state()方法设置进度更新间隔\n\n\n## 启动和停止\n\n- 开始下载\n\n```kotlin\ndownloadTask.start()\n```\n\n- 停止下载\n\n```kotlin\ndownloadTask.stop()\n```\n\n- 删除下载\n\n```kotlin\ndownloadTask.remove()\n```\n\n## License\n\n> ```\n> Copyright 2021 Season.Zlc\n>\n> Licensed under the Apache License, Version 2.0 (the \"License\");\n> you may not use this file except in compliance with the License.\n> You may obtain a copy of the License at\n>\n>    http://www.apache.org/licenses/LICENSE-2.0\n>\n> Unless required by applicable law or agreed to in writing, software\n> distributed under the License is distributed on an \"AS IS\" BASIS,\n> WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n> See the License for the specific language governing permissions and\n> limitations under the License.\n> ```\n"
  },
  {
    "path": "README.md",
    "content": "![](usage.png)\n\n# DownloadX\n\n[![](https://jitpack.io/v/ssseasonnn/DownloadX.svg)](https://jitpack.io/#ssseasonnn/DownloadX)\n\nA multi-threaded download tool written with Coroutine and Kotlin\n\n*Read this in other languages: [中文](README.ch.md), [English](README.md), [Changelog](CHANGELOG.md)* \n\n## Prepare\n\n- Add jitpack repo:\n\n```gradle\nmaven { url 'https://jitpack.io' }\n```\n    \n- Add dependency:\n\n```gradle\nimplementation \"com.github.ssseasonnn:DownloadX:1.0.5\"\n```\n\n## Basic Usage\n\n```kotlin\n// create download task\nval downloadTask = coroutineScope.download(\"url\")\n\n// listen download progress\ndownloadTask.progress()\n    .onEach { binding.button.setProgress(it)  }\n    .launchIn(lifecycleScope)\n\n// or listen download state\ndownloadTask.state()\n    .onEach { binding.button.setState(it)  }\n    .launchIn(lifecycleScope)\n\n// start download\ndownloadTask.start()\n```\n\n## Create task\n\n- Specify CoroutineScope\n\nIf 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\n\n> lifecycleScope is an extension in androidX, you need to add the following dependencies:\n> implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'\n\n```kotlin\nclass DemoActivity : AppCompatActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        \n        //When the activity is destroyed, the download task automatically stops\n        val downloadTask = lifecycleScope.download(\"url\")\n        downloadTask.start()\n    }\n}\n```\n\nIf the download task needs to be shared between multiple activities, or download in the background, then directly use **GlobalScope**\n\n```kotlin\nclass DemoActivity : AppCompatActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        \n        //When the activity is destroyed, the download task still continues to download\n        val downloadTask = GlobalScope.download(\"url\")\n        downloadTask.start()\n    }\n}\n```\n\n- Set the file name and save path\n\nPass directly to the download method:\n\n```kotlin\nval downloadTask = GlobalScope.download(\"url\", \"saveName\", \"savePath\")\n```\n\nCustom DownloadParam:\n\n```kotlin\nval downloadParam = DownloadParam(\"url\", \"saveName\", \"savePath\")\nval downloadTask = lifecycleScope.download(downloadParam)\n```\n\nBy default, we use **url** as the only indicator of **DownloadTask**. When you need to change this default behavior, you can customize your own **DownloadParam**:\n\n```kotlin\nclass CustomDownloadParam(url: String, saveName: String, savePath: String) : DownloadParam(url, saveName, savePath) {\n    override fun tag(): String {\n        // Use the file path as a unique identifier\n        return savePath + saveName\n    }\n}\n\nval customDownloadParam = CustomDownloadParam(\"url\", \"saveName\", \"savePath\")\nval downloadTask = lifecycleScope.download(customDownloadParam)\n```\n\nWhen multiple pages use the same identifier (for example, the same url) to create a download task, the same DownloadTask will be returned, for example:\n\n```kotlin\n// same url\nval url = \"xxxx\"\n\nclass DemoActivity : AppCompatActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        //Create download task\n        val downloadTask = GlobalScope.download(url)\n\n        downloadTask.progress()\n            .onEach { progress ->  /* update progress */ }\n            .launchIn(lifecycleScope)\n\n        downloadTask.start()\n    }\n}\n\nclass OtherActivity : AppCompatActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        //Create a download task with the same url to get the download task created on the previous page\n        val downloadTask = GlobalScope.download(url)\n\n        downloadTask.progress()\n            .onEach { progress ->  /* update progress */ }\n            .launchIn(lifecycleScope)\n\n        downloadTask.start()\n    }\n}\n```\n\nBased on this, the same download progress and download status can be shared on any number of pages\n\n## Progress and State\n\n- Listen progress only\n\nThis method can be used in certain scenarios when only the download progress is needed\n\n```kotlin\nval downloadTask = lifecycleScope.download(\"url\")\n\ndownloadTask.progress()\n    .onEach { progress ->  /* update progress */ }\n    .launchIn(lifecycleScope) // using lifecycleScope\n\ndownloadTask.start()\n```\n\n> Use **lifecycleScope** to ensure that the monitoring is automatically released when the Activity or Fragment is destroyed\n\n\nYou can set the update interval for the progress() method. The default is to update every 200ms, such as:\n\n```kotlin\ndownloadTask.progress(500) // Set to update the progress every 500ms\n    .onEach { progress ->  \n        // update progress\n        setProgress(progress)\n    }\n    .launchIn(lifecycleScope)\n```\n\n- Listen progress and state\n\nWhen you need download status and download progress, use this method to get\n\n```kotlin\nval downloadTask = lifecycleScope.download(\"url\")\n\ndownloadTask.state()\n    .onEach { state ->  \n        // update state\n        setState(state)\n        // update progress\n        setProgress(state.progress)\n    }\n    .launchIn(lifecycleScope)\n\ndownloadTask.start()\n```\n\n> state has the following values：**None,Waiting,Downloading,Stopped,Failed,Succeed**\n\nSimilarly, you can set the progress update interval for the state() method\n\n\n## Start and Stop\n\n- Start download\n\n```kotlin\ndownloadTask.start()\n```\n\n- Stop download\n\n```kotlin\ndownloadTask.stop()\n```\n\n- Remove download\n\n```kotlin\ndownloadTask.remove()\n```\n\n\n## License\n\n> ```\n> Copyright 2021 Season.Zlc\n>\n> Licensed under the Apache License, Version 2.0 (the \"License\");\n> you may not use this file except in compliance with the License.\n> You may obtain a copy of the License at\n>\n>    http://www.apache.org/licenses/LICENSE-2.0\n>\n> Unless required by applicable law or agreed to in writing, software\n> distributed under the License is distributed on an \"AS IS\" BASIS,\n> WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n> See the License for the specific language governing permissions and\n> limitations under the License.\n> ```\n"
  },
  {
    "path": "app/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "app/build.gradle",
    "content": "apply plugin: 'com.android.application'\napply plugin: 'kotlin-android'\n\nandroid {\n    compileSdkVersion 28\n\n    defaultConfig {\n        applicationId \"zlc.season.downloadxdemo\"\n        minSdkVersion 21\n        //noinspection ExpiredTargetSdkVersion\n        targetSdkVersion 28\n        versionCode 1\n        versionName \"1.0\"\n\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'\n        }\n    }\n\n    lintOptions {\n        abortOnError false\n    }\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    kotlinOptions {\n        jvmTarget = \"1.8\"\n    }\n\n    buildFeatures {\n        viewBinding true\n    }\n}\n\ndependencies {\n    implementation fileTree(dir: 'libs', include: ['*.jar'])\n    implementation \"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version\"\n    implementation 'androidx.appcompat:appcompat:1.2.0'\n    implementation 'com.google.android.material:material:1.4.0'\n    implementation 'androidx.core:core-ktx:1.3.2'\n    implementation 'androidx.fragment:fragment-ktx:1.2.5'\n    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'\n    implementation \"io.coil-kt:coil:1.1.1\"\n    implementation 'com.github.ssseasonnn:Yasha:1.1.4'\n    implementation 'com.github.ssseasonnn:Bracer:1.0.7'\n\n    implementation project(path: ':downloadx')\n\n    testImplementation 'junit:junit:4.12'\n    androidTestImplementation 'androidx.test.ext:junit:1.1.2'\n    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'\n}\n"
  },
  {
    "path": "app/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n"
  },
  {
    "path": "app/src/androidTest/java/zlc/season/downloadxdemo/ExampleInstrumentedTest.kt",
    "content": "package zlc.season.downloadxdemo\n\nimport androidx.test.platform.app.InstrumentationRegistry\nimport androidx.test.ext.junit.runners.AndroidJUnit4\n\nimport org.junit.Test\nimport org.junit.runner.RunWith\n\nimport org.junit.Assert.*\n\n/**\n * Instrumented test, which will execute on an Android device.\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\n@RunWith(AndroidJUnit4::class)\nclass ExampleInstrumentedTest {\n    @Test\n    fun useAppContext() {\n        // Context of the app under test.\n        val appContext = InstrumentationRegistry.getInstrumentation().targetContext\n        assertEquals(\"zlc.season.downloadxdemo\", appContext.packageName)\n    }\n}\n"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"zlc.season.downloadxdemo\">\n\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />\n    <uses-permission android:name=\"android.permission.REQUEST_INSTALL_PACKAGES\" />\n\n    <application\n        android:allowBackup=\"true\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:networkSecurityConfig=\"@xml/network_config\"\n        android:roundIcon=\"@mipmap/ic_launcher_round\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/AppTheme\">\n        <activity\n            android:name=\".TestActivity\"\n            android:exported=\"true\">\n\n        </activity>\n        <activity\n            android:name=\".MainActivity\"\n            android:exported=\"true\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n        </activity>\n        <activity android:name=\".DetailActivity\" />\n        <activity android:name=\".HistoryActivity\" />\n\n        <provider\n            android:name=\".ApkFileProvider\"\n            android:authorities=\"${applicationId}.provider\"\n            android:exported=\"false\"\n            android:grantUriPermissions=\"true\">\n            <meta-data\n                android:name=\"android.support.FILE_PROVIDER_PATHS\"\n                android:resource=\"@xml/apk_file_provider\" />\n        </provider>\n    </application>\n\n</manifest>"
  },
  {
    "path": "app/src/main/java/zlc/season/downloadxdemo/ApkFileProvider.kt",
    "content": "package zlc.season.downloadxdemo\n\nimport androidx.core.content.FileProvider\n\nclass ApkFileProvider : FileProvider()"
  },
  {
    "path": "app/src/main/java/zlc/season/downloadxdemo/AppInfoManager.kt",
    "content": "package zlc.season.downloadxdemo\n\nimport com.google.gson.Gson\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\n\nobject AppInfoManager {\n    suspend fun getAppInfoList(): List<AppListResp.AppInfo> {\n        return withContext(Dispatchers.IO) {\n            val list = Gson().fromJson(appListJson, AppListResp::class.java)\n            list.appList\n        }\n    }\n\n    val appListJson = \"\"\"\n{\n  \"appList\": [\n    {\n      \"pkgName\": \"com.tencent.ggame\",\n      \"channelId\": \"\",\n      \"source\": 52513621,\n      \"appId\": 52513588,\n      \"apkId\": 102432290,\n      \"appName\": \"腾讯广东麻将\",\n      \"fileSize\": 52489311,\n      \"versionCode\": 174,\n      \"versionName\": \"1.7.4\",\n      \"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\",\n      \"totalDownloadTimes\": 1892987,\n      \"shortDesc\": \"正宗广东麻将手游\",\n      \"apkMd5\": \"69E7E4E87B798032925A2CA9D99F4F22\",\n      \"minSdkVersion\": 16,\n      \"parentCategoryID\": -2,\n      \"signatureMd5\": \"A8DF121F79960593B23A558E2154FFBA\",\n      \"categoryId\": 121,\n      \"categoryName\": \"网络游戏\",\n      \"averageRating\": 3,\n      \"publishTime\": 0,\n      \"realTimeReportFlag\": 0,\n      \"extData\": \"\",\n      \"logoUrl\": \"https://pp.myapp.com/ma_icon/0/icon_52513588_1658376667/256\",\n      \"iconUrl\": \"https://pp.myapp.com/ma_icon/0/icon_52513588_1658376667/96\",\n      \"recommendId\": \"BwYCGQIAAw+ZEBUsRgxHTlhuZ3RpcGdYR3RRASxgGXYAiAyWAKyyYwNTsckACAoAATIAAxD5QAILCgACMgADEPxABwsKAAMyAAMQ/UAICwoABDIAAxD+QAgLCgAFMgADEP9ABgsKAAYyAAMRB0ACCwoABzIAAxEIQAILCgAIMgADEQlAAgvVQDlLKgNxdY7s/A/wEAP2EQ8GCeaWl+WcsOS4uyABugv2EjE1MjUxMzU4OF8xNjYwNzM0MzQ1ODE3NTI0NzU5M180MjA1NDk3Nzk1NjE4MjU2OTc2+BMADQYCMTAWEDQ3OjAuMDQxOTA4MjMxOjAGAjEzFm10dz0wOnRndz0wOnRhZ3c9MDpxaXM9MDpxaXI9MDpsZGFzPTAuMTU0MDc3NzQ1OmN2cj0wOnFtMnNpbT0wOm1hdGNoX3R5cGU9MDpzZWFyY2hfbHZsPTA6cnFfbWF0Y2g6NTpxY19tYXRjaDo1BgQyMDE4FgswLjAwMzEzMTA1NAYEMjAxORYJ5paX5Zyw5Li7BghhY2N1cmF0ZRYBMQYEcGN2chYLMC4wODQzMTEzNjYGBXF1ZXJ5FgnmlpflnLDkuLsGCnF1ZXJ5X2ZsYWcWATEGDHJld3JpdGVxdWVyeRYABgN0Z3cWBjAuMDAwMQYCdHcWBjAuMDAwMQYJdXNlcl90eXBlFgEwBgZ6cHJpY2UWATD1FAAAAAAAAAAA/f8ADH8ABBDg6x0=\"\n    },\n    {\n      \"pkgName\": \"com.tencent.qqgame.qqhlupwvga\",\n      \"channelId\": \"\",\n      \"source\": 10103134,\n      \"appId\": 10103101,\n      \"apkId\": 104735997,\n      \"appName\": \"欢乐升级（腾讯）\",\n      \"fileSize\": 130472533,\n      \"versionCode\": 43020,\n      \"versionName\": \"4.3.2\",\n      \"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\",\n      \"totalDownloadTimes\": 26475921,\n      \"shortDesc\": \"腾讯官方出品的欢乐升级\",\n      \"apkMd5\": \"C2F17F4006AB308BAF0B29D102DD6566\",\n      \"minSdkVersion\": 16,\n      \"parentCategoryID\": -2,\n      \"signatureMd5\": \"F6A0BB7245074B9F080D03796F8919DB\",\n      \"categoryId\": 148,\n      \"categoryName\": \"棋牌中心\",\n      \"averageRating\": 4,\n      \"publishTime\": 0,\n      \"realTimeReportFlag\": 0,\n      \"extData\": \"\",\n      \"logoUrl\": \"https://pp.myapp.com/ma_icon/0/icon_10103101_1657014288/256\",\n      \"iconUrl\": \"https://pp.myapp.com/ma_icon/0/icon_10103101_1657014288/96\",\n      \"recommendId\": \"BwYCGQIAAw+ZEBUsRgxHTlhuZ3RpcGdYR3RRASxgGnYAiAyWAKyyYwNTsckACAoAATIAAxD5QAILCgACMgADEPxABwsKAAMyAAMQ/UAICwoABDIAAxD+QAgLCgAFMgADEP9ABgsKAAYyAAMRB0ACCwoABzIAAxEIQAILCgAIMgADEQlAAgvVQDkAbDlxdY7s/A/wEAP2EQ8GCeaWl+WcsOS4uyABugv2EjExMDEwMzEwMV8xNjYwNzM0MzQ1ODE3NTI0NzU5M180MjA1NDk3Nzk1NjE4MjU2OTc2+BMADQYCMTAWEDQ3OjAuMTAyMDg3Mjc5OjAGAjEzFm10dz0wOnRndz0wOnRhZ3c9MDpxaXM9MDpxaXI9MDpsZGFzPTAuODk3NjI3NTY0OmN2cj0wOnFtMnNpbT0wOm1hdGNoX3R5cGU9MDpzZWFyY2hfbHZsPTA6cnFfbWF0Y2g6NTpxY19tYXRjaDo1BgQyMDE4FgswLjAwOTE3ODkyNgYEMjAxORYJ5paX5Zyw5Li7BghhY2N1cmF0ZRYBMQYEcGN2chYLMC4wODMzMzgxNzEGBXF1ZXJ5FgnmlpflnLDkuLsGCnF1ZXJ5X2ZsYWcWATEGDHJld3JpdGVxdWVyeRYABgN0Z3cWBjAuMDAwMQYCdHcWBjAuMDAwMQYJdXNlcl90eXBlFgEwBgZ6cHJpY2UWATD1FAAAAAAAAAAA/f8ADH8ABBDg6x0=\"\n    },\n    {\n      \"pkgName\": \"com.ourgame.mahjong.danji\",\n      \"channelId\": \"\",\n      \"source\": 273687,\n      \"appId\": 273654,\n      \"apkId\": 105063525,\n      \"appName\": \"单机麻将-开心版\",\n      \"fileSize\": 60238101,\n      \"versionCode\": 31927,\n      \"versionName\": \"7.3.19.27\",\n      \"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\",\n      \"totalDownloadTimes\": 13671289,\n      \"shortDesc\": \"上桌论英雄，争先当雀神！\",\n      \"apkMd5\": \"49B44FE0E45A7FF1AB02AF9B348022DC\",\n      \"minSdkVersion\": 17,\n      \"parentCategoryID\": -2,\n      \"signatureMd5\": \"7F49616F29A5888427DA005028176EEE\",\n      \"categoryId\": 148,\n      \"categoryName\": \"棋牌中心\",\n      \"averageRating\": 4,\n      \"publishTime\": 0,\n      \"realTimeReportFlag\": 0,\n      \"extData\": \"\",\n      \"logoUrl\": \"https://pp.myapp.com/ma_icon/0/icon_273654_1658307138/256\",\n      \"iconUrl\": \"https://pp.myapp.com/ma_icon/0/icon_273654_1658307138/96\",\n      \"recommendId\": \"BwYCFgIAAw+ZEBUsRgxHTlhuZ3RpcGdYR3RRASxgG3YAiAyWAKyyYwNTsckACAoAATIAAxD5QAILCgACMgADEPxABwsKAAMyAAMQ/UAICwoABDIAAxD+QAgLCgAFMgADEP9ABgsKAAYyAAMRB0ACCwoABzIAAxEIQAILCgAIMgADEQlAAgvVQDhXbPFxdY7s/A/wEAP2EQ8GCeaWl+WcsOS4uyABugv2Ei8yNzM2NTRfMTY2MDczNDM0NTgxNzUyNDc1OTNfNDIwNTQ5Nzc5NTYxODI1Njk3NvgTAA0GAjEwFhA0NzowLjA0NTU4OTUwOTowBgIxMxZsdHc9MDp0Z3c9MDp0YWd3PTA6cWlzPTA6cWlyPTA6bGRhcz0wLjE1Nzk0NTYyOmN2cj0wOnFtMnNpbT0wOm1hdGNoX3R5cGU9MDpzZWFyY2hfbHZsPTA6cnFfbWF0Y2g6NTpxY19tYXRjaDo1BgQyMDE4FgswLjAwMzkzMTA2NgYEMjAxORYJ5paX5Zyw5Li7BghhY2N1cmF0ZRYBMQYEcGN2chYLMC4wODExMzc2ODcGBXF1ZXJ5FgnmlpflnLDkuLsGCnF1ZXJ5X2ZsYWcWATEGDHJld3JpdGVxdWVyeRYABgN0Z3cWBjAuMDAwMQYCdHcWBjAuMDAwMQYJdXNlcl90eXBlFgEwBgZ6cHJpY2UWATD1FAAAAAAAAAAA/f8ADH8ABBDg6x0=\"\n    },\n    {\n      \"pkgName\": \"com.gdy.yyb\",\n      \"channelId\": \"\",\n      \"source\": 52637269,\n      \"appId\": 52637236,\n      \"apkId\": 104990247,\n      \"appName\": \"干瞪眼\",\n      \"fileSize\": 45351985,\n      \"versionCode\": 70309,\n      \"versionName\": \"7.3.9\",\n      \"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\",\n      \"totalDownloadTimes\": 92229,\n      \"shortDesc\": \"一轮出完让他干瞪眼\",\n      \"apkMd5\": \"74043F69111880A873832E75B1535AC3\",\n      \"minSdkVersion\": 14,\n      \"parentCategoryID\": -2,\n      \"signatureMd5\": \"172C769C70E4586A9359A2ECEA2649B4\",\n      \"categoryId\": 148,\n      \"categoryName\": \"棋牌中心\",\n      \"averageRating\": 3,\n      \"publishTime\": 0,\n      \"realTimeReportFlag\": 0,\n      \"extData\": \"\",\n      \"logoUrl\": \"https://pp.myapp.com/ma_icon/0/icon_52637236_1658109485/256\",\n      \"iconUrl\": \"https://pp.myapp.com/ma_icon/0/icon_52637236_1658109485/96\",\n      \"recommendId\": \"BwYCGQIAAw+ZEBUsRgxHTlhuZ3RpcGdYR3RRASxgHHYAiAyWAKyyYwNTsckACAoAATIAAxD5QAILCgACMgADEPxABwsKAAMyAAMQ/UAICwoABDIAAxD+QAgLCgAFMgADEP9ABgsKAAYyAAMRB0ACCwoABzIAAxEIQAILCgAIMgADEQlAAgvVQDVkkGlxdY7s/A/wEAP2EQ8GCeaWl+WcsOS4uyABugv2EjE1MjYzNzIzNl8xNjYwNzM0MzQ1ODE3NTI0NzU5M180MjA1NDk3Nzk1NjE4MjU2OTc2+BMADQYCMTAWEDQ3OjAuMDM4OTE1MDU4OjAGAjEzFm10dz0wOnRndz0wOnRhZ3c9MDpxaXM9MDpxaXI9MDpsZGFzPTAuOTkwNzQ3OTU2OmN2cj0wOnFtMnNpbT0wOm1hdGNoX3R5cGU9MDpzZWFyY2hfbHZsPTA6cnFfbWF0Y2g6NTpxY19tYXRjaDo1BgQyMDE4FgswLjAwMjIxNzkzNQYEMjAxORYJ5paX5Zyw5Li7BghhY2N1cmF0ZRYBMQYEcGN2chYLMC4wNzEzMDg3NjIGBXF1ZXJ5FgnmlpflnLDkuLsGCnF1ZXJ5X2ZsYWcWATEGDHJld3JpdGVxdWVyeRYABgN0Z3cWBjAuMDAwMQYCdHcWBjAuMDAwMQYJdXNlcl90eXBlFgEwBgZ6cHJpY2UWATD1FAAAAAAAAAAA/f8ADH8ABBDg6x0=\"\n    },\n    {\n      \"pkgName\": \"com.tencent.tmgp.jinxiuddz\",\n      \"channelId\": \"\",\n      \"source\": 54172738,\n      \"appId\": 54172705,\n      \"apkId\": 98945691,\n      \"appName\": \"英雄斗地主\",\n      \"fileSize\": 84962197,\n      \"versionCode\": 1,\n      \"versionName\": \"1.27\",\n      \"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\",\n      \"totalDownloadTimes\": 4370,\n      \"shortDesc\": \"\",\n      \"apkMd5\": \"E9C56679E0F72140578F8B7AC5E3C536\",\n      \"minSdkVersion\": 16,\n      \"parentCategoryID\": -2,\n      \"signatureMd5\": \"B08E94AB2AE6AD9E8696B32F1AD823F7\",\n      \"categoryId\": 148,\n      \"categoryName\": \"棋牌中心\",\n      \"averageRating\": 3,\n      \"publishTime\": 0,\n      \"realTimeReportFlag\": 0,\n      \"extData\": \"\",\n      \"logoUrl\": \"https://pp.myapp.com/ma_icon/0/icon_54172705_1635823131/256\",\n      \"iconUrl\": \"https://pp.myapp.com/ma_icon/0/icon_54172705_1635823131/96\",\n      \"recommendId\": \"BwYCHAIAAw+ZEBUsRgxHTlhuZ3RpcGdYR3RRASxgHXYAiAyWAKyyYwNTsckACAoAATIAAxD5QAILCgACMgADEPxABwsKAAMyAAMQ/UAICwoABDIAAxD+QAgLCgAFMgADEP9ABgsKAAYyAAMRB0ACCwoABzIAAxEIQAILCgAIMgADEQlAAgvVQDKlCMAfIS3s/A/wEAP2EQ8GCeaWl+WcsOS4uyABugv2EjE1NDE3MjcwNV8xNjYwNzM0MzQ1ODE3NTI0NzU5M180MjA1NDk3Nzk1NjE4MjU2OTc2+BMADQYCMTAWGjc5OjAuMjE2MzM1MjU0OjAuODQzMjY2MTc0BgIxMxZodHc9MC42NTp0Z3c9MDp0YWd3PTA6cWlzPTM6cWlyPTAuNjpsZGFzPTA6Y3ZyPTA6cW0yc2ltPTA6bWF0Y2hfdHlwZT0yOnNlYXJjaF9sdmw9MjpycV9tYXRjaDo1OnFjX21hdGNoOjUGBDIwMTgWCzAuMDMwMTA1NDcyBgQyMDE5FgnmlpflnLDkuLsGCGFjY3VyYXRlFgExBgRwY3ZyFgswLjA1OTk4MTg4MwYFcXVlcnkWCeaWl+WcsOS4uwYKcXVlcnlfZmxhZxYBMQYMcmV3cml0ZXF1ZXJ5FgAGA3RndxYGMC4wMDAxBgJ0dxYEMC42NQYJdXNlcl90eXBlFgEwBgZ6cHJpY2UWATD1FAAAAAAAAAAA/f8ADH8ABBDg6x0=\"\n    },\n    {\n      \"pkgName\": \"com.tencent.tmgp.lljjyyb2\",\n      \"channelId\": \"\",\n      \"source\": 54205905,\n      \"appId\": 54205872,\n      \"apkId\": 105434988,\n      \"appName\": \"乐乐竞技斗地主\",\n      \"fileSize\": 60127573,\n      \"versionCode\": 2200,\n      \"versionName\": \"2.2.0\",\n      \"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\",\n      \"totalDownloadTimes\": 1735,\n      \"shortDesc\": \"\",\n      \"apkMd5\": \"22EF8602EB5EB244D9B60F872FDCD91C\",\n      \"minSdkVersion\": 19,\n      \"parentCategoryID\": -2,\n      \"signatureMd5\": \"D6AE8A16AB9B034460663473405385E1\",\n      \"categoryId\": 148,\n      \"categoryName\": \"棋牌中心\",\n      \"averageRating\": 5,\n      \"publishTime\": 0,\n      \"realTimeReportFlag\": 0,\n      \"extData\": \"\",\n      \"logoUrl\": \"https://pp.myapp.com/ma_icon/0/icon_54205872_1658914399/256\",\n      \"iconUrl\": \"https://pp.myapp.com/ma_icon/0/icon_54205872_1658914399/96\",\n      \"recommendId\": \"BwYCGwIAAw+ZEBUsRgxHTlhuZ3RpcGdYR3RRASxgHnYAiAyWAKyyYwNTsckACAoAATIAAxD5QAILCgACMgADEPxABwsKAAMyAAMQ/UAICwoABDIAAxD+QAgLCgAFMgADEP9ABgsKAAYyAAMRB0ACCwoABzIAAxEIQAILCgAIMgADEQlAAgvVQDIx1z+fIS3s/A/wEAP2EQ8GCeaWl+WcsOS4uyABugv2EjE1NDIwNTg3Ml8xNjYwNzM0MzQ1ODE3NTI0NzU5M180MjA1NDk3Nzk1NjE4MjU2OTc2+BMADQYCMTAWGTc5OjAuMjIxNzQzMTM6MC44NjY1MjQ1NTIGAjEzFmh0dz0wLjY1OnRndz0wOnRhZ3c9MDpxaXM9MzpxaXI9MC41OmxkYXM9MDpjdnI9MDpxbTJzaW09MDptYXRjaF90eXBlPTI6c2VhcmNoX2x2bD0yOnJxX21hdGNoOjU6cWNfbWF0Y2g6NQYEMjAxOBYLMC4wMzIwMTc1MDEGBDIwMTkWCeaWl+WcsOS4uwYIYWNjdXJhdGUWATEGBHBjdnIWCzAuMDU4NDgxOTY5BgVxdWVyeRYJ5paX5Zyw5Li7BgpxdWVyeV9mbGFnFgExBgxyZXdyaXRlcXVlcnkWAAYDdGd3FgYwLjAwMDEGAnR3FgQwLjY1Bgl1c2VyX3R5cGUWATAGBnpwcmljZRYBMPUUAAAAAAAAAAD9/wAMfwAEEODrHQ==\"\n    },\n    {\n      \"pkgName\": \"com.cbfq.srddz\",\n      \"channelId\": \"\",\n      \"source\": 52515798,\n      \"appId\": 52515765,\n      \"apkId\": 105830041,\n      \"appName\": \"乐享四人斗地主\",\n      \"fileSize\": 74401217,\n      \"versionCode\": 929,\n      \"versionName\": \"9.2.9\",\n      \"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\",\n      \"totalDownloadTimes\": 356940,\n      \"shortDesc\": \"新用户登录即领可领取5元话费哦\",\n      \"apkMd5\": \"DEC4815412BFEDFDD02308835FF74999\",\n      \"minSdkVersion\": 21,\n      \"parentCategoryID\": -2,\n      \"signatureMd5\": \"1DAF2C9DECC930C11D4ADB45C6B69F82\",\n      \"categoryId\": 148,\n      \"categoryName\": \"棋牌中心\",\n      \"averageRating\": 4,\n      \"publishTime\": 0,\n      \"realTimeReportFlag\": 0,\n      \"extData\": \"\",\n      \"logoUrl\": \"https://pp.myapp.com/ma_icon/0/icon_52515765_1660287617/256\",\n      \"iconUrl\": \"https://pp.myapp.com/ma_icon/0/icon_52515765_1660287617/96\",\n      \"recommendId\": \"BwYCGAIAAw+ZEBUsRgxHTlhuZ3RpcGdYR3RRASxgH3YAiAyWAKyyYwNTsckACAoAATIAAxD5QAILCgACMgADEPxABwsKAAMyAAMQ/UAICwoABDIAAxD+QAgLCgAFMgADEP9ABgsKAAYyAAMRB0ACCwoABzIAAxEIQAILCgAIMgADEQlAAgvVQDF5pP/xdY7s/A/wEAP2EQ8GCeaWl+WcsOS4uyABugv2EjE1MjUxNTc2NV8xNjYwNzM0MzQ1ODE3NTI0NzU5M180MjA1NDk3Nzk1NjE4MjU2OTc2+BMADQYCMTAWEDQ3OjAuMDUyMDQyODQxOjAGAjEzFm10dz0wOnRndz0wOnRhZ3c9MDpxaXM9MDpxaXI9MDpsZGFzPTAuOTg1Nzc1NjE5OmN2cj0wOnFtMnNpbT0wOm1hdGNoX3R5cGU9MDpzZWFyY2hfbHZsPTA6cnFfbWF0Y2g6NTpxY19tYXRjaDo1BgQyMDE4FgowLjAwMzQ3MzEyBgQyMDE5FgnmlpflnLDkuLsGCGFjY3VyYXRlFgExBgRwY3ZyFgswLjA1ODI0OTkxMwYFcXVlcnkWCeaWl+WcsOS4uwYKcXVlcnlfZmxhZxYBMQYMcmV3cml0ZXF1ZXJ5FgAGA3RndxYGMC4wMDAxBgJ0dxYGMC4wMDAxBgl1c2VyX3R5cGUWATAGBnpwcmljZRYBMPUUAAAAAAAAAAD9/wAMfwAEEODrHQ==\"\n    },\n    {\n      \"pkgName\": \"com.tencent.tmgp.speedmobile\",\n      \"channelId\": \"\",\n      \"source\": 52488608,\n      \"appId\": 52488575,\n      \"apkId\": 104549788,\n      \"appName\": \"QQ飞车手游\",\n      \"fileSize\": 2096029603,\n      \"versionCode\": 1320002188,\n      \"versionName\": \"1.32.0.2188\",\n      \"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\",\n      \"totalDownloadTimes\": 113372898,\n      \"shortDesc\": \"经典国民竞速手游\",\n      \"apkMd5\": \"D09201BF06A2EAFA68833DDAEEB38A5F\",\n      \"minSdkVersion\": 21,\n      \"parentCategoryID\": -2,\n      \"signatureMd5\": \"9BCBAFE32AE8382CC224F5AAB0EE8383\",\n      \"categoryId\": 151,\n      \"categoryName\": \"体育竞速\",\n      \"averageRating\": 4,\n      \"publishTime\": 0,\n      \"realTimeReportFlag\": 0,\n      \"extData\": \"\",\n      \"logoUrl\": \"https://pp.myapp.com/ma_icon/0/icon_52488575_1658377825/256\",\n      \"iconUrl\": \"https://pp.myapp.com/ma_icon/0/icon_52488575_1658377825/96\",\n      \"recommendId\": \"BwYCGQIAAw+ZEBUsRgxHTlhuZ3RpcGdYR3RRASxgIHYAiAyWAKyyYwNTsckACAoAATIAAxD5QAILCgACMgADEPxABwsKAAMyAAMQ/UAICwoABDIAAxD+QAgLCgAFMgADEP9ABgsKAAYyAAMRB0ACCwoABzIAAxEIQAILCgAIMgADEQlAAgvVQCxOKZ7i6xzs/A/wEAP2EQ8GCeaWl+WcsOS4uyABugv2EjE1MjQ4ODU3NV8xNjYwNzM0MzQ1ODE3NTI0NzU5M180MjA1NDk3Nzk1NjE4MjU2OTc2+BMADQYCMTAWEDQ3OjAuMDY5MTU0MjczOjAGAjEzFm10dz0wOnRndz0wOnRhZ3c9MDpxaXM9MDpxaXI9MDpsZGFzPTAuMDQxNzU1ODYxOmN2cj0wOnFtMnNpbT0wOm1hdGNoX3R5cGU9MDpzZWFyY2hfbHZsPTA6cnFfbWF0Y2g6NTpxY19tYXRjaDo1BgQyMDE4FgswLjAwNjc4MzA4OQYEMjAxORYJ5paX5Zyw5Li7BghhY2N1cmF0ZRYBMQYEcGN2chYLMC4wNDcxNzQ4NzEGBXF1ZXJ5FgnmlpflnLDkuLsGCnF1ZXJ5X2ZsYWcWATEGDHJld3JpdGVxdWVyeRYABgN0Z3cWBjAuMDAwMQYCdHcWBjAuMDAwMQYJdXNlcl90eXBlFgEwBgZ6cHJpY2UWATD1FAAAAAAAAAAA/f8ADH8ABBDg6x0=\"\n    },\n    {\n      \"pkgName\": \"com.qqgame.happymj\",\n      \"channelId\": \"\",\n      \"source\": 10125401,\n      \"appId\": 10125368,\n      \"apkId\": 105120675,\n      \"appName\": \"腾讯欢乐麻将全集\",\n      \"fileSize\": 270064595,\n      \"versionCode\": 77630,\n      \"versionName\": \"7.7.63\",\n      \"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\",\n      \"totalDownloadTimes\": 327695044,\n      \"shortDesc\": \"全国各地麻将玩法合集\",\n      \"apkMd5\": \"D830417642DF843242A0BD78601D1B14\",\n      \"minSdkVersion\": 16,\n      \"parentCategoryID\": -2,\n      \"signatureMd5\": \"F6A0BB7245074B9F080D03796F8919DB\",\n      \"categoryId\": 148,\n      \"categoryName\": \"棋牌中心\",\n      \"averageRating\": 4,\n      \"publishTime\": 0,\n      \"realTimeReportFlag\": 0,\n      \"extData\": \"\",\n      \"logoUrl\": \"https://pp.myapp.com/ma_icon/0/icon_10125368_1658475371/256\",\n      \"iconUrl\": \"https://pp.myapp.com/ma_icon/0/icon_10125368_1658475371/96\",\n      \"recommendId\": \"BwYCGQIAAw+ZEBUsRgxHTlhuZ3RpcGdYR3RRASxgIXYAiAyWAKyyYwNTsckACAoAATIAAxD5QAILCgACMgADEPxABwsKAAMyAAMQ/UAICwoABDIAAxD+QAgLCgAFMgADEP9ABgsKAAYyAAMRB0ACCwoABzIAAxEIQAILCgAIMgADEQlAAgvVQCaIMK7i6xzs/A/wEAP2EQ8GCeaWl+WcsOS4uyABugv2EjExMDEyNTM2OF8xNjYwNzM0MzQ1ODE3NTI0NzU5M180MjA1NDk3Nzk1NjE4MjU2OTc2+BMADQYCMTAWEDQ3OjAuMTczNTE3MjM1OjAGAjEzFm10dz0wOnRndz0wOnRhZ3c9MDpxaXM9MDpxaXI9MDpsZGFzPTAuMTc3NTYxMjgzOmN2cj0wOnFtMnNpbT0wOm1hdGNoX3R5cGU9MDpzZWFyY2hfbHZsPTA6cnFfbWF0Y2g6NTpxY19tYXRjaDo1BgQyMDE4FgswLjAxODIyNDE0MQYEMjAxORYJ5paX5Zyw5Li7BghhY2N1cmF0ZRYBMQYEcGN2chYLMC4wMzc1NTI2NTUGBXF1ZXJ5FgnmlpflnLDkuLsGCnF1ZXJ5X2ZsYWcWATEGDHJld3JpdGVxdWVyeRYABgN0Z3cWBjAuMDAwMQYCdHcWBjAuMDAwMQYJdXNlcl90eXBlFgEwBgZ6cHJpY2UWATD1FAAAAAAAAAAA/f8ADH8ABBDg6x0=\"\n    },\n    {\n      \"pkgName\": \"com.tencent.tmgp.ibirdgame.doudizhu\",\n      \"channelId\": \"\",\n      \"source\": 54211549,\n      \"appId\": 54211516,\n      \"apkId\": 100771090,\n      \"appName\": \"笨鸟斗地主\",\n      \"fileSize\": 111990879,\n      \"versionCode\": 3,\n      \"versionName\": \"1.3\",\n      \"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\",\n      \"totalDownloadTimes\": 1030,\n      \"shortDesc\": \"\",\n      \"apkMd5\": \"A7120AEB0DE5C59E3415C33149F5F6BA\",\n      \"minSdkVersion\": 21,\n      \"parentCategoryID\": -2,\n      \"signatureMd5\": \"B86815FCC30CFAE5EFCCAA07FD9F6C52\",\n      \"categoryId\": 148,\n      \"categoryName\": \"棋牌中心\",\n      \"averageRating\": 0,\n      \"publishTime\": 0,\n      \"realTimeReportFlag\": 0,\n      \"extData\": \"\",\n      \"logoUrl\": \"https://pp.myapp.com/ma_icon/0/icon_54211516_1640055072/256\",\n      \"iconUrl\": \"https://pp.myapp.com/ma_icon/0/icon_54211516_1640055072/96\",\n      \"recommendId\": \"BwYCGwIAAw+ZEBUsRgxHTlhuZ3RpcGdYR3RRASxgInYAiAyWAKyyYwNTsckACAoAATIAAxD5QAILCgACMgADEPxABwsKAAMyAAMQ/UAICwoABDIAAxD+QAgLCgAFMgADEP9ABgsKAAYyAAMRB0ACCwoABzIAAxEIQAILCgAIMgADEQlAAgvVQCH2J+A+Qlvs/A/wEAP2EQ8GCeaWl+WcsOS4uyABugv2EjE1NDIxMTUxNl8xNjYwNzM0MzQ1ODE3NTI0NzU5M180MjA1NDk3Nzk1NjE4MjU2OTc2+BMADQYCMTAWGjc5OjAuMjIyNzk5MzU5OjAuOTAzOTI4ODg1BgIxMxZodHc9MC42NTp0Z3c9MDp0YWd3PTA6cWlzPTM6cWlyPTAuNjpsZGFzPTA6Y3ZyPTA6cW0yc2ltPTA6bWF0Y2hfdHlwZT0yOnNlYXJjaF9sdmw9MjpycV9tYXRjaDo1OnFjX21hdGNoOjUGBDIwMTgWCzAuMDI1MjEzNDI1BgQyMDE5FgnmlpflnLDkuLsGCGFjY3VyYXRlFgExBgRwY3ZyFgowLjAyNzc2ODkxBgVxdWVyeRYJ5paX5Zyw5Li7BgpxdWVyeV9mbGFnFgExBgxyZXdyaXRlcXVlcnkWAAYDdGd3FgYwLjAwMDEGAnR3FgQwLjY1Bgl1c2VyX3R5cGUWATAGBnpwcmljZRYBMPUUAAAAAAAAAAD9/wAMfwAEEODrHQ==\"\n    }\n  ]\n}        \n    \"\"\".trimIndent()\n}"
  },
  {
    "path": "app/src/main/java/zlc/season/downloadxdemo/AppListResp.kt",
    "content": "package zlc.season.downloadxdemo\n\n\nimport com.google.gson.annotations.SerializedName\nimport kotlinx.coroutines.Job\nimport zlc.season.yasha.YashaItem\n\ndata class AppListResp(\n    @SerializedName(\"appList\")\n    val appList: List<AppInfo> = listOf(),\n) {\n    data class AppInfo(\n        @SerializedName(\"appId\")\n        val appId: Int = 0,\n\n        @SerializedName(\"apkMd5\")\n        val apkMd5: String = \"\",\n\n        @SerializedName(\"apkUrl\")\n        val apkUrl: String = \"\",\n\n        @SerializedName(\"appDownCount\")\n        val appDownCount: Long = 0,\n\n        @SerializedName(\"appName\")\n        val appName: String = \"\",\n\n        @SerializedName(\"averageRating\")\n        val averageRating: Double = 0.0,\n\n        @SerializedName(\"categoryId\")\n        val categoryId: Int = 0,\n\n        @SerializedName(\"categoryName\")\n        val categoryName: String = \"\",\n\n        @SerializedName(\"editorIntro\")\n        val editorIntro: String = \"\",\n\n        @SerializedName(\"fileSize\")\n        val fileSize: Int = 0,\n\n        @SerializedName(\"iconUrl\")\n        val iconUrl: String = \"\",\n\n        @SerializedName(\"images\")\n        val images: List<String> = listOf(),\n\n        @SerializedName(\"pkgName\")\n        val pkgName: String = \"\",\n\n        @SerializedName(\"versionCode\")\n        val versionCode: Int = 0,\n\n        @SerializedName(\"versionName\")\n        val versionName: String = \"\"\n    ) : YashaItem {\n\n        @Transient\n        var progressJob: Job? = null\n    }\n}"
  },
  {
    "path": "app/src/main/java/zlc/season/downloadxdemo/DetailActivity.kt",
    "content": "package zlc.season.downloadxdemo\n\nimport android.os.Bundle\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.lifecycle.lifecycleScope\nimport coil.load\nimport kotlinx.coroutines.GlobalScope\nimport kotlinx.coroutines.flow.*\nimport zlc.season.bracer.mutableParams\nimport zlc.season.bracer.params\nimport zlc.season.downloadx.State\nimport zlc.season.downloadx.download\nimport zlc.season.downloadxdemo.databinding.ActivityDetailBinding\n\nclass DetailActivity : AppCompatActivity() {\n    val binding by lazy { ActivityDetailBinding.inflate(layoutInflater) }\n    var appInfo by mutableParams<AppListResp.AppInfo>()\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContentView(binding.root)\n\n        binding.icon.load(appInfo.iconUrl)\n        binding.title.text = appInfo.appName\n        binding.desc.text = appInfo.editorIntro\n\n        val downloadTask = GlobalScope.download(appInfo.apkUrl)\n\n        downloadTask.state()\n            .onEach { binding.button.setState(it) }\n            .launchIn(lifecycleScope)\n\n        binding.button.setOnClickListener {\n            when {\n                downloadTask.isSucceed() -> {\n                    installApk(downloadTask.file()!!)\n                }\n                downloadTask.isStarted() -> {\n                    downloadTask.stop()\n                }\n                else -> {\n                    downloadTask.start()\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/zlc/season/downloadxdemo/HistoryActivity.kt",
    "content": "package zlc.season.downloadxdemo\n\nimport android.os.Bundle\nimport androidx.appcompat.app.AppCompatActivity\nimport zlc.season.downloadxdemo.databinding.ActivityHistoryBinding\n\nclass HistoryActivity : AppCompatActivity() {\n    val binding by lazy { ActivityHistoryBinding.inflate(layoutInflater) }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContentView(binding.root)\n    }\n}"
  },
  {
    "path": "app/src/main/java/zlc/season/downloadxdemo/MainActivity.kt",
    "content": "package zlc.season.downloadxdemo\n\nimport android.os.Bundle\nimport android.view.View\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.lifecycle.lifecycleScope\nimport coil.load\nimport kotlinx.coroutines.GlobalScope\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.launchIn\nimport kotlinx.coroutines.flow.onEach\nimport zlc.season.bracer.startActivity\nimport zlc.season.downloadx.download\nimport zlc.season.downloadxdemo.databinding.ActivityMainBinding\nimport zlc.season.downloadxdemo.databinding.AppInfoItemBinding\nimport zlc.season.yasha.YashaDataSource\nimport zlc.season.yasha.YashaItem\nimport zlc.season.yasha.linear\n\n\nclass MainActivity : AppCompatActivity() {\n    private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }\n    private val dataSource by lazy { AppListDataSource() }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContentView(binding.root)\n\n        dataSource.loading.onEach {\n            binding.progress.visibility = if (it) View.VISIBLE else View.GONE\n        }.launchIn(lifecycleScope)\n\n        dataSource.retry.onEach {\n            binding.btnRetry.visibility = if (it) View.VISIBLE else View.GONE\n        }.launchIn(lifecycleScope)\n\n        binding.btnRetry.setOnClickListener {\n            dataSource.invalidate()\n        }\n\n        binding.recyclerView.linear(dataSource) {\n            renderBindingItem<AppListResp.AppInfo, AppInfoItemBinding> {\n                onAttach {\n                    val downloadTask = GlobalScope.download(data.apkUrl)\n\n                    data.progressJob?.cancel()\n                    data.progressJob = downloadTask.state()\n                        .onEach {\n                            itemBinding.button.setState(it)\n                        }\n                        .launchIn(lifecycleScope)\n                }\n                onBind {\n                    itemBinding.title.text = data.appName\n                    itemBinding.desc.text = data.editorIntro\n                    itemBinding.icon.load(data.iconUrl)\n\n                    itemBinding.root.setOnClickListener {\n                        startActivity<DetailActivity> {\n                            appInfo = data\n                        }\n                    }\n                    itemBinding.button.setOnClickListener {\n                        val downloadTask = GlobalScope.download(data.apkUrl)\n                        when {\n                            downloadTask.isSucceed() -> {\n                                installApk(downloadTask.file()!!)\n                            }\n                            downloadTask.isStarted() -> {\n                                downloadTask.stop()\n                            }\n                            else -> {\n                                downloadTask.start()\n                            }\n                        }\n                    }\n                }\n\n                onDetach {\n                    data.progressJob?.cancel()\n                }\n            }\n        }\n    }\n\n    class AppListDataSource : YashaDataSource() {\n        val retry = MutableStateFlow(false)\n        val loading = MutableStateFlow(false)\n\n        override suspend fun loadInitial(): List<YashaItem> {\n            return try {\n                loading.value = true\n                retry.value = false\n                AppInfoManager.getAppInfoList()\n            } catch (e: Exception) {\n                e.printStackTrace()\n                retry.value = true\n                emptyList()\n            } finally {\n                loading.value = false\n            }\n        }\n    }\n}\n\n"
  },
  {
    "path": "app/src/main/java/zlc/season/downloadxdemo/ProgressButton.kt",
    "content": "package zlc.season.downloadxdemo\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport android.view.LayoutInflater\nimport android.widget.FrameLayout\nimport zlc.season.downloadx.Progress\nimport zlc.season.downloadx.State\nimport zlc.season.downloadxdemo.databinding.LayoutProgressButtonBinding\n\nclass ProgressButton @JvmOverloads constructor(\n    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0\n) : FrameLayout(context, attrs, defStyleAttr) {\n    val binding = LayoutProgressButtonBinding.inflate(LayoutInflater.from(context), this, true)\n\n    fun setState(state: State) {\n        binding.progress.max = state.progress.totalSize.toInt()\n        binding.progress.progress = state.progress.downloadSize.toInt()\n\n        when (state) {\n            is State.None -> {\n                binding.button.text = \"下载\"\n            }\n            is State.Waiting -> {\n                binding.button.text = \"等待中\"\n            }\n            is State.Downloading -> {\n                binding.button.text = state.progress.percentStr()\n            }\n            is State.Failed -> {\n                binding.button.text = \"重试\"\n            }\n            is State.Stopped -> {\n                binding.button.text = \"继续\"\n            }\n            is State.Succeed -> {\n                binding.button.text = \"安装\"\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/zlc/season/downloadxdemo/TestActivity.kt",
    "content": "package zlc.season.downloadxdemo\n\nimport android.os.Bundle\nimport android.util.Log\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.lifecycle.lifecycleScope\nimport kotlinx.coroutines.GlobalScope\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.launchIn\nimport kotlinx.coroutines.flow.onEach\nimport zlc.season.downloadx.download\nimport zlc.season.downloadxdemo.databinding.ActivityTestBinding\n\nclass TestActivity : AppCompatActivity() {\n    var stateJob: Job? = null\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        val binding = ActivityTestBinding.inflate(layoutInflater)\n        setContentView(binding.root)\n\n        val downloadTask =\n            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\")\n\n        binding.btnTest.setOnClickListener {\n            downloadTask.start()\n            stateJob?.cancel()\n            stateJob = downloadTask.state(1000)\n                .onEach {\n                    Log.e(\"Download\", \"state -> $it, progress -> ${it.progress.percentStr()}\")\n                }\n                .launchIn(lifecycleScope)\n        }\n\n    }\n}"
  },
  {
    "path": "app/src/main/java/zlc/season/downloadxdemo/Utils.kt",
    "content": "package zlc.season.downloadxdemo\n\nimport android.content.Context\nimport android.content.Intent\nimport android.content.Intent.*\nimport android.net.Uri.fromFile\nimport android.os.Build.VERSION.SDK_INT\nimport android.os.Build.VERSION_CODES.N\nimport androidx.core.content.FileProvider.getUriForFile\nimport java.io.File\n\nfun Context.installApk(file: File) {\n    val intent = Intent(ACTION_VIEW)\n    val authority = \"$packageName.provider\"\n    val uri = if (SDK_INT >= N) {\n        getUriForFile(this, authority, file)\n    } else {\n        fromFile(file)\n    }\n    intent.setDataAndType(uri, \"application/vnd.android.package-archive\")\n    intent.addFlags(FLAG_ACTIVITY_NEW_TASK)\n    intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION)\n    startActivity(intent)\n}"
  },
  {
    "path": "app/src/main/res/drawable/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"108\"\n    android:viewportHeight=\"108\">\n    <path\n        android:fillColor=\"#3DDC84\"\n        android:pathData=\"M0,0h108v108h-108z\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M9,0L9,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,0L19,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M29,0L29,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M39,0L39,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M49,0L49,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M59,0L59,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M69,0L69,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M79,0L79,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M89,0L89,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M99,0L99,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,9L108,9\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,19L108,19\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,29L108,29\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,39L108,39\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,49L108,49\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,59L108,59\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,69L108,69\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,79L108,79\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,89L108,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,99L108,99\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,29L89,29\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,39L89,39\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,49L89,49\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,59L89,59\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,69L89,69\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,79L89,79\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M29,19L29,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M39,19L39,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M49,19L49,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M59,19L59,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M69,19L69,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M79,19L79,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/progress.xml",
    "content": "<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:id=\"@android:id/background\">\n        <shape>\n            <corners android:radius=\"20dp\" />\n            <solid android:color=\"#EFB33B\" />\n            <stroke\n                android:width=\"0.5dp\"\n                android:color=\"@android:color/holo_orange_dark\" />\n        </shape>\n    </item>\n    <item android:id=\"@android:id/progress\">\n        <clip>\n            <shape>\n                <corners android:radius=\"20dp\" />\n                <solid android:color=\"@android:color/holo_orange_dark\" />\n            </shape>\n        </clip>\n    </item>\n</layer-list>"
  },
  {
    "path": "app/src/main/res/drawable-v24/ic_launcher_foreground.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"108\"\n    android:viewportHeight=\"108\">\n    <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\">\n        <aapt:attr name=\"android:fillColor\">\n            <gradient\n                android:endX=\"85.84757\"\n                android:endY=\"92.4963\"\n                android:startX=\"42.9492\"\n                android:startY=\"49.59793\"\n                android:type=\"linear\">\n                <item\n                    android:color=\"#44000000\"\n                    android:offset=\"0.0\" />\n                <item\n                    android:color=\"#00000000\"\n                    android:offset=\"1.0\" />\n            </gradient>\n        </aapt:attr>\n    </path>\n    <path\n        android:fillColor=\"#FFFFFF\"\n        android:fillType=\"nonZero\"\n        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\"\n        android:strokeWidth=\"1\"\n        android:strokeColor=\"#00000000\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/layout/activity_detail.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <ImageView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        tools:background=\"@android:color/darker_gray\" />\n\n    <ImageView\n        android:id=\"@+id/icon\"\n        android:layout_width=\"90dp\"\n        android:layout_height=\"90dp\"\n        android:layout_marginTop=\"120dp\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        tools:background=\"@color/colorAccent\" />\n\n    <TextView\n        android:id=\"@+id/title\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"8dp\"\n        android:textColor=\"@android:color/black\"\n        android:textSize=\"20sp\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@+id/icon\"\n        tools:text=\"title\" />\n\n    <TextView\n        android:id=\"@+id/desc\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"0dp\"\n        android:layout_marginStart=\"10dp\"\n        android:layout_marginLeft=\"10dp\"\n        android:layout_marginTop=\"16dp\"\n        android:layout_marginEnd=\"10dp\"\n        android:layout_marginBottom=\"10dp\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@+id/title\"\n        tools:text=\"desc\" />\n\n    <zlc.season.downloadxdemo.ProgressButton\n        android:id=\"@+id/button\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"40dp\"\n        android:layout_marginStart=\"10dp\"\n        android:layout_marginEnd=\"10dp\"\n        android:layout_marginBottom=\"10dp\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\" />\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "app/src/main/res/layout/activity_history.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <androidx.recyclerview.widget.RecyclerView\n        android:id=\"@+id/recycler_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "app/src/main/res/layout/activity_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\".MainActivity\">\n\n    <androidx.recyclerview.widget.RecyclerView\n        android:id=\"@+id/recycler_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n    <Button\n        android:id=\"@+id/btn_retry\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"重试\"\n        android:visibility=\"gone\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n    <ProgressBar\n        android:id=\"@+id/progress\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "app/src/main/res/layout/activity_test.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\".TestActivity\">\n\n    <Button\n        android:id=\"@+id/btn_test\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:text=\"test\" />\n</FrameLayout>"
  },
  {
    "path": "app/src/main/res/layout/app_info_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:layout_marginTop=\"16dp\"\n    android:layout_marginBottom=\"16dp\">\n\n    <ImageView\n        android:id=\"@+id/icon\"\n        android:layout_width=\"50dp\"\n        android:layout_height=\"50dp\"\n        android:layout_marginLeft=\"20dp\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        tools:src=\"@color/colorAccent\" />\n\n    <TextView\n        android:id=\"@+id/title\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginStart=\"10dp\"\n        android:layout_marginLeft=\"10dp\"\n        android:layout_marginEnd=\"10dp\"\n        android:layout_marginRight=\"10dp\"\n        android:maxLines=\"1\"\n        android:textColor=\"@android:color/black\"\n        android:textSize=\"15sp\"\n        app:layout_constraintEnd_toStartOf=\"@+id/button\"\n        app:layout_constraintStart_toEndOf=\"@+id/icon\"\n        app:layout_constraintTop_toTopOf=\"@+id/icon\"\n        tools:text=\"title\" />\n\n    <TextView\n        android:id=\"@+id/desc\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"0dp\"\n        android:layout_marginStart=\"10dp\"\n        android:layout_marginLeft=\"10dp\"\n        android:layout_marginTop=\"2dp\"\n        android:layout_marginEnd=\"10dp\"\n        android:layout_marginRight=\"10dp\"\n        android:ellipsize=\"end\"\n        android:gravity=\"center_vertical\"\n        android:maxLines=\"1\"\n        app:layout_constraintBottom_toBottomOf=\"@+id/icon\"\n        app:layout_constraintEnd_toStartOf=\"@+id/button\"\n        app:layout_constraintStart_toEndOf=\"@+id/icon\"\n        app:layout_constraintTop_toBottomOf=\"@+id/title\"\n        tools:text=\"desc\" />\n\n    <zlc.season.downloadxdemo.ProgressButton\n        android:id=\"@+id/button\"\n        android:layout_width=\"70dp\"\n        android:layout_height=\"40dp\"\n        android:layout_marginRight=\"20dp\"\n        app:layout_constraintBottom_toBottomOf=\"@+id/icon\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"@+id/icon\" />\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "app/src/main/res/layout/history_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:layout_marginTop=\"5dp\"\n    android:layout_marginBottom=\"5dp\">\n\n    <ImageView\n        android:id=\"@+id/icon\"\n        android:layout_width=\"70dp\"\n        android:layout_height=\"70dp\"\n        android:layout_marginLeft=\"20dp\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        tools:src=\"@color/colorAccent\" />\n\n    <TextView\n        android:id=\"@+id/title\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginStart=\"10dp\"\n        android:layout_marginLeft=\"10dp\"\n        android:layout_marginEnd=\"10dp\"\n        android:layout_marginRight=\"10dp\"\n        android:textColor=\"@android:color/black\"\n        android:textSize=\"20sp\"\n        app:layout_constraintEnd_toStartOf=\"@+id/button\"\n        app:layout_constraintStart_toEndOf=\"@+id/icon\"\n        app:layout_constraintTop_toTopOf=\"@+id/icon\"\n        tools:text=\"title\" />\n\n    <ProgressBar\n        android:id=\"@+id/progress\"\n        style=\"@style/Widget.AppCompat.ProgressBar.Horizontal\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"0dp\"\n        android:layout_marginStart=\"10dp\"\n        android:layout_marginLeft=\"10dp\"\n        android:layout_marginTop=\"2dp\"\n        android:layout_marginEnd=\"10dp\"\n        android:layout_marginRight=\"10dp\"\n        app:layout_constraintBottom_toBottomOf=\"@+id/icon\"\n        app:layout_constraintEnd_toStartOf=\"@+id/button\"\n        app:layout_constraintStart_toEndOf=\"@+id/icon\"\n        app:layout_constraintTop_toBottomOf=\"@+id/title\"\n        tools:progress=\"50\" />\n\n    <Button\n        android:id=\"@+id/button\"\n        android:layout_width=\"70dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginRight=\"20dp\"\n        app:layout_constraintBottom_toBottomOf=\"@+id/icon\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"@+id/icon\"\n        tools:text=\"下载\" />\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "app/src/main/res/layout/layout_progress_button.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"40dp\">\n\n    <ProgressBar\n        android:id=\"@+id/progress\"\n        style=\"@style/Widget.AppCompat.ProgressBar.Horizontal\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"0dp\"\n        android:progressDrawable=\"@drawable/progress\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        tools:max=\"100\"\n        tools:progress=\"90\" />\n\n    <TextView\n        android:id=\"@+id/button\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"0dp\"\n        android:gravity=\"center\"\n        android:text=\"下载\"\n        android:textColor=\"@android:color/white\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\" />\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\" />\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"colorPrimary\">#6200EE</color>\n    <color name=\"colorPrimaryDark\">#3700B3</color>\n    <color name=\"colorAccent\">#03DAC5</color>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">DownloadXDemo</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/styles.xml",
    "content": "<resources>\n\n    <!-- Base application theme. -->\n    <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.DarkActionBar\">\n        <!-- Customize your theme here. -->\n        <item name=\"colorPrimary\">@color/colorPrimary</item>\n        <item name=\"colorPrimaryDark\">@color/colorPrimaryDark</item>\n        <item name=\"colorAccent\">@color/colorAccent</item>\n    </style>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/xml/apk_file_provider.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <paths>\n        <external-path\n            name=\"external_files\"\n            path=\".\" />\n        <root-path\n            name=\"files\"\n            path=\"\" />\n    </paths>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/xml/network_config.xml",
    "content": "<network-security-config>\n    <base-config cleartextTrafficPermitted=\"true\" />\n    <debug-overrides>\n        <trust-anchors>\n            <certificates src=\"system\" />\n            <certificates src=\"user\" />\n        </trust-anchors>\n    </debug-overrides>\n</network-security-config>"
  },
  {
    "path": "app/src/test/java/zlc/season/downloadxdemo/ExampleUnitTest.kt",
    "content": "package zlc.season.downloadxdemo\n\nimport org.junit.Test\n\nimport org.junit.Assert.*\n\n/**\n * Example local unit test, which will execute on the development machine (host).\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\nclass ExampleUnitTest {\n    @Test\n    fun addition_isCorrect() {\n        assertEquals(4, 2 + 2)\n    }\n}\n"
  },
  {
    "path": "build.gradle",
    "content": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\n\nbuildscript {\n    ext.kotlin_version = '1.7.0'\n    repositories {\n        google()\n        mavenCentral()\n        maven { url 'https://jitpack.io' }\n    }\n    dependencies {\n        classpath 'com.android.tools.build:gradle:7.2.2'\n        classpath \"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version\"\n\n        // NOTE: Do not place your application dependencies here; they belong\n        // in the individual module build.gradle files\n    }\n}\n\nallprojects {\n    repositories {\n        google()\n        mavenCentral()\n        maven { url 'https://jitpack.io' }\n    }\n}\n\ntask clean(type: Delete) {\n    delete rootProject.buildDir\n}\n"
  },
  {
    "path": "downloadx/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "downloadx/build.gradle",
    "content": "apply plugin: 'com.android.library'\napply plugin: 'kotlin-android'\napply plugin: 'maven-publish'\n\nandroid {\n    compileSdkVersion 28\n\n    defaultConfig {\n        minSdkVersion 16\n        //noinspection ExpiredTargetSdkVersion\n        targetSdkVersion 28\n\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n        consumerProguardFiles 'consumer-rules.pro'\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'\n        }\n    }\n\n    lintOptions {\n        abortOnError false\n    }\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n    kotlinOptions {\n        jvmTarget = '1.8'\n    }\n}\n\ntask androidSourcesJar(type: Jar) {\n    classifier 'sources'\n    from android.sourceSets.main.java.srcDirs\n}\n\nproject.afterEvaluate {\n    publishing {\n        publications {\n            release(MavenPublication) {\n                from components.release\n                artifact androidSourcesJar // optional sources\n            }\n        }\n    }\n}\n\ndependencies {\n    implementation \"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version\"\n    api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'\n    api 'androidx.appcompat:appcompat:1.2.0'\n    api 'androidx.core:core-ktx:1.3.2'\n\n    api 'com.squareup.retrofit2:retrofit:2.9.0'\n    api 'com.squareup.retrofit2:converter-gson:2.9.0'\n    api \"com.squareup.okhttp3:okhttp:4.9.0\"\n    api \"com.squareup.okio:okio:2.9.0\"\n    api 'com.github.ssseasonnn:ClarityPotion:1.0.4'\n\n    testImplementation 'junit:junit:4.12'\n    androidTestImplementation 'androidx.test.ext:junit:1.1.2'\n    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'\n}\n"
  },
  {
    "path": "downloadx/consumer-rules.pro",
    "content": ""
  },
  {
    "path": "downloadx/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n"
  },
  {
    "path": "downloadx/src/androidTest/java/zlc/season/downloadx/ExampleInstrumentedTest.kt",
    "content": "package zlc.season.downloadx\n\nimport androidx.test.platform.app.InstrumentationRegistry\nimport androidx.test.ext.junit.runners.AndroidJUnit4\n\nimport org.junit.Test\nimport org.junit.runner.RunWith\n\nimport org.junit.Assert.*\n\n/**\n * Instrumented test, which will execute on an Android device.\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\n@RunWith(AndroidJUnit4::class)\nclass ExampleInstrumentedTest {\n    @Test\n    fun useAppContext() {\n        // Context of the app under test.\n        val appContext = InstrumentationRegistry.getInstrumentation().targetContext\n        assertEquals(\"zlc.season.downloadx.test\", appContext.packageName)\n    }\n}\n"
  },
  {
    "path": "downloadx/src/main/AndroidManifest.xml",
    "content": "<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",
    "content": "package zlc.season.downloadx\n\nimport kotlinx.coroutines.CoroutineScope\nimport zlc.season.downloadx.helper.Default\nimport zlc.season.downloadx.core.DownloadTask\nimport zlc.season.downloadx.core.DownloadParam\nimport zlc.season.downloadx.core.DownloadConfig\n\nfun CoroutineScope.download(\n    url: String,\n    saveName: String = \"\",\n    savePath: String = Default.DEFAULT_SAVE_PATH,\n    downloadConfig: DownloadConfig = DownloadConfig()\n): DownloadTask {\n    val downloadParam = DownloadParam(url, saveName, savePath)\n    val task = DownloadTask(this, downloadParam, downloadConfig)\n    return downloadConfig.taskManager.add(task)\n}\n\nfun CoroutineScope.download(\n    downloadParam: DownloadParam,\n    downloadConfig: DownloadConfig = DownloadConfig()\n): DownloadTask {\n    val task = DownloadTask(this, downloadParam, downloadConfig)\n    return downloadConfig.taskManager.add(task)\n}"
  },
  {
    "path": "downloadx/src/main/java/zlc/season/downloadx/Progress.kt",
    "content": "package zlc.season.downloadx\n\nimport zlc.season.downloadx.utils.formatSize\nimport zlc.season.downloadx.utils.ratio\n\n\nclass Progress(\n    var downloadSize: Long = 0,\n    var totalSize: Long = 0,\n    /**\n     * 用于标识一个链接是否是分块下载, 如果该值为true, 那么totalSize为-1\n     */\n    var isChunked: Boolean = false\n) {\n    /**\n     * Return total size str. eg: 10M\n     */\n    fun totalSizeStr(): String {\n        return totalSize.formatSize()\n    }\n\n    /**\n     * Return download size str. eg: 3M\n     */\n    fun downloadSizeStr(): String {\n        return downloadSize.formatSize()\n    }\n\n    /**\n     * Return percent number.\n     */\n    fun percent(): Double {\n        if (isChunked) return 0.0\n        return downloadSize ratio totalSize\n    }\n\n    /**\n     * Return percent string.\n     */\n    fun percentStr(): String {\n        return \"${percent()}%\"\n    }\n}"
  },
  {
    "path": "downloadx/src/main/java/zlc/season/downloadx/State.kt",
    "content": "package zlc.season.downloadx\n\nsealed class State {\n    var progress: Progress = Progress()\n        internal set\n\n    class None : State()\n    class Waiting : State()\n    class Downloading : State()\n    class Stopped : State()\n    class Failed : State()\n    class Succeed : State()\n}"
  },
  {
    "path": "downloadx/src/main/java/zlc/season/downloadx/core/DownloadConfig.kt",
    "content": "package zlc.season.downloadx.core\n\nimport okhttp3.ResponseBody\nimport retrofit2.Response\nimport zlc.season.downloadx.helper.Default.DEFAULT_RANGE_CURRENCY\nimport zlc.season.downloadx.helper.Default.DEFAULT_RANGE_SIZE\nimport zlc.season.downloadx.helper.apiCreator\n\nclass DownloadConfig(\n    /**\n     * 禁用断点续传\n     */\n    val disableRangeDownload: Boolean = false,\n    /**\n     * 下载管理\n     */\n    val taskManager: TaskManager = DefaultTaskManager,\n    /**\n     * 下载队列\n     */\n    val queue: DownloadQueue = DefaultDownloadQueue.get(),\n\n    /**\n     * 自定义header\n     */\n    val customHeader: Map<String, String> = emptyMap(),\n\n    /**\n     * 分片下载每片的大小\n     */\n    val rangeSize: Long = DEFAULT_RANGE_SIZE,\n    /**\n     * 分片下载并行数量\n     */\n    val rangeCurrency: Int = DEFAULT_RANGE_CURRENCY,\n\n    /**\n     * 下载器分发\n     */\n    val dispatcher: DownloadDispatcher = DefaultDownloadDispatcher,\n\n    /**\n     * 文件校验\n     */\n    val validator: FileValidator = DefaultFileValidator,\n\n    /**\n     * http client\n     */\n    httpClientFactory: HttpClientFactory = DefaultHttpClientFactory\n) {\n    private val api = apiCreator(httpClientFactory.create())\n\n    suspend fun request(url: String, header: Map<String, String>): Response<ResponseBody> {\n        val tempHeader = mutableMapOf<String, String>().also {\n            it.putAll(customHeader)\n            it.putAll(header)\n        }\n        return api.get(url, tempHeader)\n    }\n}"
  },
  {
    "path": "downloadx/src/main/java/zlc/season/downloadx/core/DownloadParam.kt",
    "content": "package zlc.season.downloadx.core\n\n\nopen class DownloadParam(\n    var url: String,\n    var saveName: String = \"\",\n    var savePath: String = \"\",\n    var extra: String = \"\"\n) {\n\n    /**\n     * Each task with unique tag.\n     */\n    open fun tag() = url\n\n\n    override fun equals(other: Any?): Boolean {\n        if (other == null) return false\n        if (this === other) return true\n\n        return if (other is DownloadParam) {\n            tag() == other.tag()\n        } else {\n            false\n        }\n    }\n\n    override fun hashCode(): Int {\n        return tag().hashCode()\n    }\n}"
  },
  {
    "path": "downloadx/src/main/java/zlc/season/downloadx/core/DownloadQueue.kt",
    "content": "package zlc.season.downloadx.core\n\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.channels.Channel\nimport kotlinx.coroutines.channels.consumeEach\nimport zlc.season.downloadx.helper.Default.MAX_TASK_NUMBER\nimport java.util.concurrent.*\n\ninterface DownloadQueue {\n    suspend fun enqueue(task: DownloadTask)\n\n    suspend fun dequeue(task: DownloadTask)\n}\n\n@OptIn(DelicateCoroutinesApi::class)\nclass DefaultDownloadQueue private constructor(private val maxTask: Int) : DownloadQueue {\n    companion object {\n        private val lock = Any()\n        private var instance: DefaultDownloadQueue? = null\n\n        fun get(maxTask: Int = MAX_TASK_NUMBER): DefaultDownloadQueue {\n            if (instance == null) {\n                synchronized(lock) {\n                    if (instance == null) {\n                        instance = DefaultDownloadQueue(maxTask)\n                    }\n                }\n            }\n            return instance!!\n        }\n    }\n\n    private val channel = Channel<DownloadTask>()\n    private val tempMap = ConcurrentHashMap<String, DownloadTask>()\n\n    init {\n        GlobalScope.launch {\n            repeat(maxTask) {\n                launch {\n                    channel.consumeEach {\n                        if (contain(it)) {\n                            it.suspendStart()\n                            dequeue(it)\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    override suspend fun enqueue(task: DownloadTask) {\n        tempMap[task.param.tag()] = task\n        channel.send(task)\n    }\n\n    override suspend fun dequeue(task: DownloadTask) {\n        tempMap.remove(task.param.tag())\n    }\n\n    private fun contain(task: DownloadTask): Boolean {\n        return tempMap[task.param.tag()] != null\n    }\n}"
  },
  {
    "path": "downloadx/src/main/java/zlc/season/downloadx/core/DownloadTask.kt",
    "content": "package zlc.season.downloadx.core\n\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.flow.*\nimport zlc.season.downloadx.Progress\nimport zlc.season.downloadx.State\nimport zlc.season.downloadx.helper.Default\nimport zlc.season.downloadx.utils.clear\nimport zlc.season.downloadx.utils.closeQuietly\nimport zlc.season.downloadx.utils.fileName\nimport zlc.season.downloadx.utils.log\nimport java.io.File\n\n@OptIn(ExperimentalCoroutinesApi::class)\nopen class DownloadTask(\n    val coroutineScope: CoroutineScope,\n    val param: DownloadParam,\n    val config: DownloadConfig\n) {\n    private val stateHolder by lazy { StateHolder() }\n\n    private var downloadJob: Job? = null\n    private var downloader: Downloader? = null\n\n    private val downloadProgressFlow = MutableStateFlow(0)\n    private val downloadStateFlow = MutableStateFlow<State>(stateHolder.none)\n\n    fun isStarted(): Boolean {\n        return stateHolder.isStarted()\n    }\n\n    fun isFailed(): Boolean {\n        return stateHolder.isFailed()\n    }\n\n    fun isSucceed(): Boolean {\n        return stateHolder.isSucceed()\n    }\n\n    fun canStart(): Boolean {\n        return stateHolder.canStart()\n    }\n\n    private fun checkJob() = downloadJob?.isActive == true\n\n    /**\n     * 获取下载文件\n     */\n    fun file(): File? {\n        return if (param.saveName.isNotEmpty() && param.savePath.isNotEmpty()) {\n            File(param.savePath, param.saveName)\n        } else {\n            null\n        }\n    }\n\n    /**\n     * 开始下载，添加到下载队列\n     */\n    fun start() {\n        coroutineScope.launch {\n            if (checkJob()) return@launch\n\n            notifyWaiting()\n            try {\n                config.queue.enqueue(this@DownloadTask)\n            } catch (e: Exception) {\n                if (e !is CancellationException) {\n                    notifyFailed()\n                }\n                e.log()\n            }\n        }\n    }\n\n    /**\n     * 开始下载并等待下载完成，直接开始下载，不添加到下载队列\n     */\n    suspend fun suspendStart() {\n        if (checkJob()) return\n\n        downloadJob?.cancel()\n        val errorHandler = CoroutineExceptionHandler { _, throwable ->\n            throwable.log()\n            if (throwable !is CancellationException) {\n                coroutineScope.launch {\n                    notifyFailed()\n                }\n            }\n        }\n        downloadJob = coroutineScope.launch(errorHandler + Dispatchers.IO) {\n            val response = config.request(param.url, Default.RANGE_CHECK_HEADER)\n            try {\n                if (!response.isSuccessful || response.body() == null) {\n                    throw RuntimeException(\"request failed\")\n                }\n\n                if (param.saveName.isEmpty()) {\n                    param.saveName = response.fileName()\n                }\n                if (param.savePath.isEmpty()) {\n                    param.savePath = Default.DEFAULT_SAVE_PATH\n                }\n\n                if (downloader == null) {\n                    downloader = config.dispatcher.dispatch(this@DownloadTask, response)\n                }\n\n                notifyStarted()\n\n                val deferred = async(Dispatchers.IO) { downloader?.download(param, config, response) }\n                deferred.await()\n\n                notifySucceed()\n            } catch (e: Exception) {\n                if (e !is CancellationException) {\n                    notifyFailed()\n                }\n                e.log()\n            } finally {\n                response.closeQuietly()\n            }\n        }\n        downloadJob?.join()\n    }\n\n    /**\n     * 停止下载\n     */\n    fun stop() {\n        coroutineScope.launch {\n            if (isStarted()) {\n                config.queue.dequeue(this@DownloadTask)\n                downloadJob?.cancel()\n                notifyStopped()\n            }\n        }\n    }\n\n    /**\n     * 移除任务\n     */\n    fun remove(deleteFile: Boolean = true) {\n        stop()\n        config.taskManager.remove(this)\n        if (deleteFile) {\n            file()?.clear()\n        }\n    }\n\n    /**\n     * @param interval 更新进度间隔时间，单位ms\n     * @param ensureLast 能否收到最后一个进度\n     */\n    fun progress(interval: Long = 200, ensureLast: Boolean = true): Flow<Progress> {\n        return downloadProgressFlow.flatMapLatest {\n            // make sure send once\n            var hasSend = false\n            channelFlow {\n                while (currentCoroutineContext().isActive) {\n                    val progress = getProgress()\n\n                    if (hasSend && stateHolder.isEnd()) {\n                        if (!ensureLast) {\n                            break\n                        }\n                    }\n\n                    send(progress)\n                    \"url ${param.url} progress ${progress.percentStr()}\".log()\n                    hasSend = true\n\n                    if (progress.isComplete()) break\n\n                    delay(interval)\n                }\n            }\n        }\n    }\n\n    /**\n     * @param interval 更新进度间隔时间，单位ms\n     */\n    fun state(interval: Long = 200): Flow<State> {\n        return downloadStateFlow.combine(progress(interval, ensureLast = false)) { l, r -> l.apply { progress = r } }\n    }\n\n    suspend fun getProgress(): Progress {\n        return downloader?.queryProgress() ?: Progress()\n    }\n\n    fun getState() = stateHolder.currentState\n\n    private suspend fun notifyWaiting() {\n        stateHolder.updateState(stateHolder.waiting, getProgress())\n        downloadStateFlow.value = stateHolder.currentState\n        \"url ${param.url} download task waiting.\".log()\n    }\n\n    private suspend fun notifyStarted() {\n        stateHolder.updateState(stateHolder.downloading, getProgress())\n        downloadStateFlow.value = stateHolder.currentState\n        downloadProgressFlow.value = downloadProgressFlow.value + 1\n        \"url ${param.url} download task start.\".log()\n    }\n\n    private suspend fun notifyStopped() {\n        stateHolder.updateState(stateHolder.stopped, getProgress())\n        downloadStateFlow.value = stateHolder.currentState\n        \"url ${param.url} download task stopped.\".log()\n    }\n\n    private suspend fun notifyFailed() {\n        stateHolder.updateState(stateHolder.failed, getProgress())\n        downloadStateFlow.value = stateHolder.currentState\n        \"url ${param.url} download task failed.\".log()\n    }\n\n    private suspend fun notifySucceed() {\n        stateHolder.updateState(stateHolder.succeed, getProgress())\n        downloadStateFlow.value = stateHolder.currentState\n        \"url ${param.url} download task succeed.\".log()\n    }\n\n    private fun Progress.isComplete(): Boolean {\n        return totalSize > 0 && totalSize == downloadSize\n    }\n\n    class StateHolder {\n        val none by lazy { State.None() }\n        val waiting by lazy { State.Waiting() }\n        val downloading by lazy { State.Downloading() }\n        val stopped by lazy { State.Stopped() }\n        val failed by lazy { State.Failed() }\n        val succeed by lazy { State.Succeed() }\n\n        var currentState: State = none\n\n        fun isStarted(): Boolean {\n            return currentState is State.Waiting || currentState is State.Downloading\n        }\n\n        fun isFailed(): Boolean {\n            return currentState is State.Failed\n        }\n\n        fun isSucceed(): Boolean {\n            return currentState is State.Succeed\n        }\n\n        fun canStart(): Boolean {\n            return currentState is State.None || currentState is State.Failed || currentState is State.Stopped\n        }\n\n        fun isEnd(): Boolean {\n            return currentState is State.None || currentState is State.Waiting || currentState is State.Stopped || currentState is State.Failed || currentState is State.Succeed\n        }\n\n        fun updateState(new: State, progress: Progress): State {\n            currentState = new.apply { this.progress = progress }\n            return currentState\n        }\n    }\n}"
  },
  {
    "path": "downloadx/src/main/java/zlc/season/downloadx/core/Downloader.kt",
    "content": "package zlc.season.downloadx.core\n\nimport kotlinx.coroutines.CompletableDeferred\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.DelicateCoroutinesApi\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.GlobalScope\nimport kotlinx.coroutines.ObsoleteCoroutinesApi\nimport kotlinx.coroutines.channels.SendChannel\nimport kotlinx.coroutines.channels.actor\nimport okhttp3.ResponseBody\nimport retrofit2.Response\nimport zlc.season.downloadx.Progress\nimport java.io.File\n\nclass QueryProgress(val completableDeferred: CompletableDeferred<Progress>)\n\ninterface Downloader {\n    var actor: SendChannel<QueryProgress>\n\n    suspend fun queryProgress(): Progress\n\n    suspend fun download(\n        downloadParam: DownloadParam,\n        downloadConfig: DownloadConfig,\n        response: Response<ResponseBody>\n    )\n}\n\n@OptIn(ObsoleteCoroutinesApi::class, DelicateCoroutinesApi::class)\nabstract class BaseDownloader(protected val coroutineScope: CoroutineScope) : Downloader {\n    protected var totalSize: Long = 0L\n    protected var downloadSize: Long = 0L\n    protected var isChunked: Boolean = false\n\n    private val progress = Progress()\n\n    override var actor = GlobalScope.actor<QueryProgress>(Dispatchers.IO) {\n        for (each in channel) {\n            each.completableDeferred.complete(progress.also {\n                it.downloadSize = downloadSize\n                it.totalSize = totalSize\n                it.isChunked = isChunked\n            })\n        }\n    }\n\n    override suspend fun queryProgress(): Progress {\n        val ack = CompletableDeferred<Progress>()\n        val queryProgress = QueryProgress(ack)\n        actor.send(queryProgress)\n        return ack.await()\n    }\n\n    fun DownloadParam.dir(): File {\n        return File(savePath)\n    }\n\n    fun DownloadParam.file(): File {\n        return File(savePath, saveName)\n    }\n}"
  },
  {
    "path": "downloadx/src/main/java/zlc/season/downloadx/core/Extensions.kt",
    "content": "package zlc.season.downloadx.core\n\nimport okhttp3.OkHttpClient\nimport okhttp3.Protocol\nimport okhttp3.ResponseBody\nimport retrofit2.Response\nimport zlc.season.downloadx.utils.contentLength\nimport zlc.season.downloadx.utils.isSupportRange\nimport java.io.File\nimport java.util.concurrent.TimeUnit\n\ninterface HttpClientFactory {\n    fun create(): OkHttpClient\n}\n\nobject DefaultHttpClientFactory : HttpClientFactory {\n    override fun create(): OkHttpClient {\n        return OkHttpClient().newBuilder()\n            .protocols(listOf(Protocol.HTTP_2, Protocol.HTTP_1_1))\n            .connectTimeout(15, TimeUnit.SECONDS)\n            .readTimeout(120, TimeUnit.SECONDS)\n            .writeTimeout(120, TimeUnit.SECONDS)\n            .build()\n    }\n}\n\ninterface DownloadDispatcher {\n    fun dispatch(downloadTask: DownloadTask, resp: Response<ResponseBody>): Downloader\n}\n\nobject DefaultDownloadDispatcher : DownloadDispatcher {\n    override fun dispatch(downloadTask: DownloadTask, resp: Response<ResponseBody>): Downloader {\n        return if (downloadTask.config.disableRangeDownload || !resp.isSupportRange()) {\n            NormalDownloader(downloadTask.coroutineScope)\n        } else {\n            RangeDownloader(downloadTask.coroutineScope)\n        }\n    }\n}\n\ninterface FileValidator {\n    fun validate(\n        file: File,\n        param: DownloadParam,\n        resp: Response<ResponseBody>\n    ): Boolean\n}\n\nobject DefaultFileValidator : FileValidator {\n    override fun validate(\n        file: File,\n        param: DownloadParam,\n        resp: Response<ResponseBody>\n    ): Boolean {\n        return file.length() == resp.contentLength()\n    }\n}"
  },
  {
    "path": "downloadx/src/main/java/zlc/season/downloadx/core/NormalDownloader.kt",
    "content": "package zlc.season.downloadx.core\n\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.coroutineScope\nimport kotlinx.coroutines.isActive\nimport okhttp3.ResponseBody\nimport okio.buffer\nimport okio.sink\nimport retrofit2.Response\nimport zlc.season.downloadx.utils.closeQuietly\nimport zlc.season.downloadx.utils.contentLength\nimport zlc.season.downloadx.utils.isChunked\nimport zlc.season.downloadx.utils.recreate\nimport zlc.season.downloadx.utils.shadow\nimport java.io.File\n\nclass NormalDownloader(coroutineScope: CoroutineScope) : BaseDownloader(coroutineScope) {\n    companion object {\n        private const val BUFFER_SIZE = 8192L\n    }\n\n    private var alreadyDownloaded = false\n\n    private lateinit var file: File\n    private lateinit var shadowFile: File\n\n    override suspend fun download(\n        downloadParam: DownloadParam,\n        downloadConfig: DownloadConfig,\n        response: Response<ResponseBody>\n    ) {\n        try {\n            file = downloadParam.file()\n            shadowFile = file.shadow()\n\n            val contentLength = response.contentLength()\n            val isChunked = response.isChunked()\n\n            downloadPrepare(downloadParam, contentLength)\n\n            if (alreadyDownloaded) {\n                this.downloadSize = contentLength\n                this.totalSize = contentLength\n                this.isChunked = isChunked\n            } else {\n                this.totalSize = contentLength\n                this.downloadSize = 0\n                this.isChunked = isChunked\n                startDownload(response.body()!!)\n            }\n        } finally {\n            response.closeQuietly()\n        }\n    }\n\n    private fun downloadPrepare(downloadParam: DownloadParam, contentLength: Long) {\n        //make sure dir is exists\n        val fileDir = downloadParam.dir()\n        if (!fileDir.exists() || !fileDir.isDirectory) {\n            fileDir.mkdirs()\n        }\n\n        if (file.exists()) {\n            if (file.length() == contentLength) {\n                alreadyDownloaded = true\n            } else {\n                file.delete()\n                shadowFile.recreate()\n            }\n        } else {\n            shadowFile.recreate()\n        }\n    }\n\n    private suspend fun startDownload(body: ResponseBody) = coroutineScope {\n        val deferred = async(Dispatchers.IO) {\n            val source = body.source()\n            val sink = shadowFile.sink().buffer()\n            val buffer = sink.buffer\n\n            var readLen = source.read(buffer, BUFFER_SIZE)\n            while (isActive && readLen != -1L) {\n                downloadSize += readLen\n                readLen = source.read(buffer, BUFFER_SIZE)\n                sink.flush()\n            }\n            sink.flush()\n        }\n        deferred.await()\n\n        if (isActive) {\n            shadowFile.renameTo(file)\n        }\n    }\n}"
  },
  {
    "path": "downloadx/src/main/java/zlc/season/downloadx/core/RangeDownloader.kt",
    "content": "package zlc.season.downloadx.core\n\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.channels.SendChannel\nimport kotlinx.coroutines.channels.actor\nimport kotlinx.coroutines.channels.consumeEach\nimport okhttp3.ResponseBody\nimport retrofit2.Response\nimport zlc.season.downloadx.core.Range.Companion.RANGE_SIZE\nimport zlc.season.downloadx.utils.*\nimport java.io.File\n\n@OptIn(ObsoleteCoroutinesApi::class)\nclass RangeDownloader(coroutineScope: CoroutineScope) : BaseDownloader(coroutineScope) {\n    private lateinit var file: File\n    private lateinit var shadowFile: File\n    private lateinit var tmpFile: File\n    private lateinit var rangeTmpFile: RangeTmpFile\n\n    override suspend fun download(\n        downloadParam: DownloadParam,\n        downloadConfig: DownloadConfig,\n        response: Response<ResponseBody>\n    ) {\n        try {\n            file = downloadParam.file()\n            shadowFile = file.shadow()\n            tmpFile = file.tmp()\n\n            val alreadyDownloaded = checkFiles(downloadParam, downloadConfig, response)\n\n            if (alreadyDownloaded) {\n                downloadSize = response.contentLength()\n                totalSize = response.contentLength()\n            } else {\n                val last = rangeTmpFile.lastProgress()\n                downloadSize = last.downloadSize\n                totalSize = last.totalSize\n                startDownload(downloadParam, downloadConfig)\n            }\n        } finally {\n            response.closeQuietly()\n        }\n    }\n\n    private fun checkFiles(\n        param: DownloadParam,\n        config: DownloadConfig,\n        response: Response<ResponseBody>\n    ): Boolean {\n        var alreadyDownloaded = false\n\n        //make sure dir is exists\n        val fileDir = param.dir()\n        if (!fileDir.exists() || !fileDir.isDirectory) {\n            fileDir.mkdirs()\n        }\n\n        val contentLength = response.contentLength()\n        val rangeSize = config.rangeSize\n        val totalRanges = response.calcRanges(rangeSize)\n\n        if (file.exists()) {\n            if (config.validator.validate(file, param, response)) {\n                alreadyDownloaded = true\n            } else {\n                file.delete()\n                recreateFiles(contentLength, totalRanges, rangeSize)\n            }\n        } else {\n            if (shadowFile.exists() && tmpFile.exists()) {\n                rangeTmpFile = RangeTmpFile(tmpFile)\n                rangeTmpFile.read()\n\n                if (!rangeTmpFile.isValid(contentLength, totalRanges)) {\n                    recreateFiles(contentLength, totalRanges, rangeSize)\n                }\n            } else {\n                recreateFiles(contentLength, totalRanges, rangeSize)\n            }\n        }\n\n        return alreadyDownloaded\n    }\n\n    private fun recreateFiles(contentLength: Long, totalRanges: Long, rangeSize: Long) {\n        tmpFile.recreate()\n        shadowFile.recreate(contentLength)\n        rangeTmpFile = RangeTmpFile(tmpFile)\n        rangeTmpFile.write(contentLength, totalRanges, rangeSize)\n    }\n\n    private suspend fun startDownload(param: DownloadParam, config: DownloadConfig) {\n        val progressChannel = coroutineScope.actor<Int> {\n            channel.consumeEach { downloadSize += it }\n        }\n\n        rangeTmpFile.undoneRanges().parallel(max = config.rangeCurrency) {\n            it.download(param, config, progressChannel)\n        }\n\n        progressChannel.close()\n\n        shadowFile.renameTo(file)\n        tmpFile.delete()\n    }\n\n    private suspend fun Range.download(\n        param: DownloadParam,\n        config: DownloadConfig,\n        sendChannel: SendChannel<Int>\n    ) = coroutineScope {\n        val deferred = async(Dispatchers.IO) {\n            val url = param.url\n            val rangeHeader = mapOf(\"Range\" to \"bytes=${current}-${end}\")\n\n            val response = config.request(url, rangeHeader)\n            if (!response.isSuccessful || response.body() == null) {\n                throw RuntimeException(\"Request failed!\")\n            }\n\n            response.body()?.use {\n                it.byteStream().use { source ->\n                    val tmpFileBuffer = tmpFile.mappedByteBuffer(startByte(), RANGE_SIZE)\n                    val shadowFileBuffer = shadowFile.mappedByteBuffer(current, remainSize())\n\n                    val buffer = ByteArray(8192)\n                    var readLen = source.read(buffer)\n\n                    while (isActive && readLen != -1) {\n                        shadowFileBuffer.put(buffer, 0, readLen)\n                        current += readLen\n\n                        tmpFileBuffer.putLong(16, current)\n\n                        sendChannel.send(readLen)\n\n                        readLen = source.read(buffer)\n                    }\n                }\n            }\n        }\n        deferred.await()\n    }\n}"
  },
  {
    "path": "downloadx/src/main/java/zlc/season/downloadx/core/RangeTmpFile.kt",
    "content": "package zlc.season.downloadx.core\n\nimport okio.*\nimport okio.ByteString.Companion.decodeHex\nimport zlc.season.downloadx.Progress\nimport java.io.File\n\nclass RangeTmpFile(private val tmpFile: File) {\n    private val fileHeader = FileHeader()\n    private val fileContent = FileContent()\n\n    fun write(totalSize: Long, totalRanges: Long, rangeSize: Long) {\n        tmpFile.sink().buffer().use {\n            fileHeader.write(it, totalSize, totalRanges)\n            fileContent.write(it, totalSize, totalRanges, rangeSize)\n        }\n    }\n\n    fun read() {\n        tmpFile.source().buffer().use {\n            fileHeader.read(it)\n            fileContent.read(it, fileHeader.totalRanges)\n        }\n    }\n\n    fun isValid(totalSize: Long, totalRanges: Long): Boolean {\n        return fileHeader.check(totalSize, totalRanges)\n    }\n\n    fun undoneRanges(): List<Range> {\n        return fileContent.ranges.filter { !it.isComplete() }\n    }\n\n    fun lastProgress(): Progress {\n        val totalSize = fileHeader.totalSize\n        val downloadSize = fileContent.downloadSize()\n\n        return Progress(downloadSize, totalSize)\n    }\n}\n\n/**\n * Save tmp file base info\n */\nprivate class FileHeader(\n    var totalSize: Long = 0L,\n    var totalRanges: Long = 0L\n) {\n\n    companion object {\n        const val FILE_HEADER_MAGIC_NUMBER = \"a1b2c3d4e5f6\"\n\n        //How to calc: ByteString.decodeHex(FILE_HEADER_MAGIC_NUMBER).size() = 6\n        const val FILE_HEADER_MAGIC_NUMBER_SIZE = 6L\n\n        //total header size\n        const val FILE_HEADER_SIZE = FILE_HEADER_MAGIC_NUMBER_SIZE + 16L\n    }\n\n    fun write(sink: BufferedSink, totalSize: Long, totalRanges: Long) {\n        this.totalSize = totalSize\n        this.totalRanges = totalRanges\n\n        sink.apply {\n            write(FILE_HEADER_MAGIC_NUMBER.decodeHex())\n            writeLong(totalSize)\n            writeLong(totalRanges)\n        }\n    }\n\n    fun read(source: BufferedSource) {\n        val header = source.readByteString(FILE_HEADER_MAGIC_NUMBER_SIZE).hex()\n        if (header != FILE_HEADER_MAGIC_NUMBER) {\n            throw IllegalStateException(\"not a tmp file\")\n        }\n        totalSize = source.readLong()\n        totalRanges = source.readLong()\n    }\n\n    fun check(totalSize: Long, totalRanges: Long): Boolean {\n        return this.totalSize == totalSize &&\n                this.totalRanges == totalRanges\n    }\n}\n\n/**\n * Save file range info\n */\nprivate class FileContent {\n    val ranges = mutableListOf<Range>()\n\n    fun write(\n        sink: BufferedSink,\n        totalSize: Long,\n        totalRanges: Long,\n        rangeSize: Long\n    ) {\n        ranges.clear()\n\n        slice(totalSize, totalRanges, rangeSize)\n\n        ranges.forEach {\n            it.write(sink)\n        }\n    }\n\n    fun read(source: BufferedSource, totalRanges: Long) {\n        ranges.clear()\n        for (i in 0 until totalRanges) {\n            ranges.add(Range().read(source))\n        }\n    }\n\n    fun downloadSize(): Long {\n        var downloadSize = 0L\n        ranges.forEach {\n            downloadSize += it.completeSize()\n        }\n        return downloadSize\n    }\n\n    private fun slice(totalSize: Long, totalRanges: Long, rangeSize: Long) {\n        var start = 0L\n\n        for (i in 0 until totalRanges) {\n            val end = if (i == totalRanges - 1) {\n                totalSize - 1\n            } else {\n                start + rangeSize - 1\n            }\n\n            ranges.add(Range(i, start, start, end))\n\n            start += rangeSize\n        }\n    }\n}\n\nclass Range(\n    var index: Long = 0L,\n    var start: Long = 0L,\n    var current: Long = 0L,\n    var end: Long = 0L\n) {\n\n    companion object {\n        const val RANGE_SIZE = 32L //each Long is 8 bytes\n    }\n\n    fun write(sink: BufferedSink): Range {\n        sink.apply {\n            writeLong(index)\n            writeLong(start)\n            writeLong(current)\n            writeLong(end)\n        }\n        return this\n    }\n\n    fun read(source: BufferedSource): Range {\n        val buffer = Buffer()\n        source.readFully(buffer, RANGE_SIZE)\n\n        buffer.apply {\n            index = readLong()\n            start = readLong()\n            current = readLong()\n            end = readLong()\n        }\n\n        return this\n    }\n\n    fun isComplete() = (current - end) == 1L\n\n    fun remainSize() = end - current + 1\n\n    fun completeSize() = current - start\n\n    /**\n     * Return the starting position of the range\n     */\n    fun startByte() = FileHeader.FILE_HEADER_SIZE + RANGE_SIZE * index\n}"
  },
  {
    "path": "downloadx/src/main/java/zlc/season/downloadx/core/TaskManager.kt",
    "content": "package zlc.season.downloadx.core\n\nimport java.util.concurrent.ConcurrentHashMap\n\ninterface TaskManager {\n    fun add(task: DownloadTask): DownloadTask\n\n    fun remove(task: DownloadTask)\n}\n\nobject DefaultTaskManager : TaskManager {\n    private val taskMap = ConcurrentHashMap<String, DownloadTask>()\n\n    override fun add(task: DownloadTask): DownloadTask {\n        if (taskMap[task.param.tag()] == null) {\n            taskMap[task.param.tag()] = task\n        }\n        return taskMap[task.param.tag()]!!\n    }\n\n    override fun remove(task: DownloadTask) {\n        taskMap.remove(task.param.tag())\n    }\n}"
  },
  {
    "path": "downloadx/src/main/java/zlc/season/downloadx/helper/Default.kt",
    "content": "package zlc.season.downloadx.helper\n\nimport zlc.season.claritypotion.ClarityPotion\n\nobject Default {\n    /**\n     * 默认的保存路径\n     */\n    val DEFAULT_SAVE_PATH = ClarityPotion.context.filesDir.path\n\n    /**\n     * 默认的分片大小\n     */\n    const val DEFAULT_RANGE_SIZE = 5L * 1024 * 1024\n\n    /**\n     * 单个任务同时下载的分片数量\n     */\n    const val DEFAULT_RANGE_CURRENCY = 5\n\n    /**\n     * 同时下载的任务数量\n     */\n    const val MAX_TASK_NUMBER = 3\n\n    /**\n     * 默认的Header\n     */\n    val RANGE_CHECK_HEADER = mapOf(\"Range\" to \"bytes=0-\")\n}"
  },
  {
    "path": "downloadx/src/main/java/zlc/season/downloadx/helper/Request.kt",
    "content": "package zlc.season.downloadx.helper\n\nimport okhttp3.OkHttpClient\nimport okhttp3.ResponseBody\nimport retrofit2.Response\nimport retrofit2.Retrofit\nimport retrofit2.converter.gson.GsonConverterFactory\nimport retrofit2.http.GET\nimport retrofit2.http.HeaderMap\nimport retrofit2.http.Streaming\nimport retrofit2.http.Url\n\ninternal const val FAKE_BASE_URL = \"http://www.example.com\"\n\ninternal fun apiCreator(client: OkHttpClient): Api {\n    val retrofit = Retrofit.Builder()\n        .baseUrl(FAKE_BASE_URL)\n        .client(client)\n        .addConverterFactory(GsonConverterFactory.create())\n        .build()\n    return retrofit.create(Api::class.java)\n}\n\ninternal interface Api {\n\n    @GET\n    @Streaming\n    suspend fun get(\n        @Url url: String,\n        @HeaderMap headers: Map<String, String>\n    ): Response<ResponseBody>\n}"
  },
  {
    "path": "downloadx/src/main/java/zlc/season/downloadx/utils/FileUtils.kt",
    "content": "package zlc.season.downloadx.utils\n\nimport java.io.File\nimport java.io.RandomAccessFile\nimport java.nio.MappedByteBuffer\nimport java.nio.channels.FileChannel\n\nfun File.shadow(): File {\n    val shadowPath = \"$canonicalPath.download\"\n    return File(shadowPath)\n}\n\nfun File.tmp(): File {\n    val tmpPath = \"$canonicalPath.tmp\"\n    return File(tmpPath)\n}\n\nfun File.recreate(length: Long = 0L) {\n    delete()\n    val created = createNewFile()\n    if (created) {\n        setLength(length)\n    } else {\n        throw IllegalStateException(\"File create failed!\")\n    }\n}\n\nfun File.setLength(length: Long = 0L) {\n    RandomAccessFile(this, \"rw\").setLength(length)\n}\n\nfun File.channel(): FileChannel {\n    return RandomAccessFile(this, \"rw\").channel\n}\n\nfun File.mappedByteBuffer(position: Long, size: Long): MappedByteBuffer {\n    val channel = channel()\n    val map = channel.map(FileChannel.MapMode.READ_WRITE, position, size)\n    channel.closeQuietly()\n    return map\n}\n\nfun File.clear() {\n    val shadow = shadow()\n    val tmp = tmp()\n    shadow.delete()\n    tmp.delete()\n    delete()\n}\n"
  },
  {
    "path": "downloadx/src/main/java/zlc/season/downloadx/utils/HttpUtil.kt",
    "content": "package zlc.season.downloadx.utils\n\nimport okhttp3.ResponseBody\nimport retrofit2.Response\nimport java.io.Closeable\nimport java.util.*\nimport java.util.regex.Pattern\n\n/** Closes this, ignoring any checked exceptions. */\nfun Closeable.closeQuietly() {\n    try {\n        close()\n    } catch (rethrown: RuntimeException) {\n        throw rethrown\n    } catch (_: Exception) {\n    }\n}\n\nfun Response<ResponseBody>.closeQuietly() {\n    body()?.closeQuietly()\n    errorBody()?.closeQuietly()\n}\n\nfun Response<*>.url(): String {\n    return raw().request.url.toString()\n}\n\nfun Response<*>.contentLength(): Long {\n    return header(\"Content-Length\").toLongOrDefault(-1)\n}\n\nfun Response<*>.isChunked(): Boolean {\n    return header(\"Transfer-Encoding\") == \"chunked\"\n}\n\nfun Response<*>.isSupportRange(): Boolean {\n    if (code() == 206\n        || header(\"Content-Range\").isNotEmpty()\n        || header(\"Accept-Ranges\") == \"bytes\"\n    ) {\n        return true\n    }\n    return false\n}\n\nfun Response<*>.fileName(): String {\n    val url = url()\n\n    var fileName = contentDisposition()\n    if (fileName.isEmpty()) {\n        fileName = getFileNameFromUrl(url)\n    }\n\n    return fileName\n}\n\nfun Response<*>.calcRanges(rangeSize: Long): Long {\n    val totalSize = contentLength()\n    val remainder = totalSize % rangeSize\n    val result = totalSize / rangeSize\n\n    return if (remainder == 0L) {\n        result\n    } else {\n        result + 1\n    }\n}\n\nprivate fun Response<*>.contentDisposition(): String {\n    val contentDisposition = header(\"Content-Disposition\").toLowerCase(Locale.getDefault())\n\n    if (contentDisposition.isEmpty()) {\n        return \"\"\n    }\n\n    val matcher = Pattern.compile(\".*filename=(.*)\").matcher(contentDisposition)\n    if (!matcher.find()) {\n        return \"\"\n    }\n\n    var result = matcher.group(1)\n    if (result.startsWith(\"\\\"\")) {\n        result = result.substring(1)\n    }\n    if (result.endsWith(\"\\\"\")) {\n        result = result.substring(0, result.length - 1)\n    }\n\n    result = result.replace(\"/\", \"_\", false)\n\n    return result\n}\n\nfun getFileNameFromUrl(url: String): String {\n    var temp = url\n    if (temp.isNotEmpty()) {\n        val fragment = temp.lastIndexOf('#')\n        if (fragment > 0) {\n            temp = temp.substring(0, fragment)\n        }\n\n        val query = temp.lastIndexOf('?')\n        if (query > 0) {\n            temp = temp.substring(0, query)\n        }\n\n        val filenamePos = temp.lastIndexOf('/')\n        val filename = if (0 <= filenamePos) temp.substring(filenamePos + 1) else temp\n\n        if (filename.isNotEmpty() && Pattern.matches(\"[a-zA-Z_0-9.\\\\-()%]+\", filename)) {\n            return filename\n        }\n    }\n\n    return \"\"\n}\n\nprivate fun Response<*>.header(key: String): String {\n    val header = headers()[key]\n    return header ?: \"\"\n}"
  },
  {
    "path": "downloadx/src/main/java/zlc/season/downloadx/utils/LogUtil.kt",
    "content": "package zlc.season.downloadx.utils\n\nimport android.util.Log\n\nvar LOG_ENABLE = false\n\nconst val LOG_TAG = \"DownloadX\"\n\nfun <T> T.log(prefix: String = \"\"): T {\n    val prefixStr = if (prefix.isEmpty()) \"\" else \"[$prefix] \"\n    if (LOG_ENABLE) {\n        if (this is Throwable) {\n            Log.w(LOG_TAG, prefixStr + this.message, this)\n        } else {\n            Log.d(LOG_TAG, prefixStr + toString())\n        }\n    }\n    return this\n}"
  },
  {
    "path": "downloadx/src/main/java/zlc/season/downloadx/utils/Util.kt",
    "content": "package zlc.season.downloadx.utils\n\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.channels.Channel\nimport kotlinx.coroutines.channels.consumeEach\nimport java.math.BigDecimal\nimport java.util.concurrent.atomic.AtomicInteger\n\nfun String.toLongOrDefault(defaultValue: Long): Long {\n    return try {\n        toLong()\n    } catch (_: NumberFormatException) {\n        defaultValue\n    }\n}\n\nfun Long.formatSize(): String {\n    require(this >= 0) { \"Size must larger than 0.\" }\n\n    val byte = this.toDouble()\n    val kb = byte / 1024.0\n    val mb = byte / 1024.0 / 1024.0\n    val gb = byte / 1024.0 / 1024.0 / 1024.0\n    val tb = byte / 1024.0 / 1024.0 / 1024.0 / 1024.0\n\n    return when {\n        tb >= 1 -> \"${tb.decimal(2)} TB\"\n        gb >= 1 -> \"${gb.decimal(2)} GB\"\n        mb >= 1 -> \"${mb.decimal(2)} MB\"\n        kb >= 1 -> \"${kb.decimal(2)} KB\"\n        else -> \"${byte.decimal(2)} B\"\n    }\n}\n\nfun Double.decimal(digits: Int): Double {\n    return this.toBigDecimal()\n        .setScale(digits, BigDecimal.ROUND_HALF_UP)\n        .toDouble()\n}\n\ninfix fun Long.ratio(bottom: Long): Double {\n    if (bottom <= 0) {\n        return 0.0\n    }\n    val result = (this * 100.0).toBigDecimal()\n        .divide((bottom * 1.0).toBigDecimal(), 2, BigDecimal.ROUND_FLOOR)\n    return result.toDouble()\n}\n\nsuspend fun <T, R> (Collection<T>).parallel(\n    dispatcher: CoroutineDispatcher = Dispatchers.Default,\n    max: Int = 2,\n    action: suspend CoroutineScope.(T) -> R\n): Iterable<R> = coroutineScope {\n    val list = this@parallel\n    if (list.isEmpty()) return@coroutineScope listOf<R>()\n\n    val channel = Channel<T>()\n    val output = Channel<R>()\n\n    val counter = AtomicInteger(0)\n\n    launch {\n        list.forEach { channel.send(it) }\n        channel.close()\n    }\n\n    repeat(max) {\n        launch(dispatcher) {\n            channel.consumeEach {\n                output.send(action(it))\n                val completed = counter.incrementAndGet()\n                if (completed == list.size) {\n                    output.close()\n                }\n            }\n        }\n    }\n\n    val results = mutableListOf<R>()\n    for (item in output) {\n        results.add(item)\n    }\n\n    return@coroutineScope results\n}"
  },
  {
    "path": "downloadx/src/test/java/zlc/season/downloadx/ExampleUnitTest.kt",
    "content": "package zlc.season.downloadx\n\nimport org.junit.Test\n\nimport org.junit.Assert.*\n\n/**\n * Example local unit test, which will execute on the development machine (host).\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\nclass ExampleUnitTest {\n    @Test\n    fun addition_isCorrect() {\n        assertEquals(4, 2 + 2)\n    }\n}\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#Wed Dec 09 15:04:10 CST 2020\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-7.3.3-bin.zip\n"
  },
  {
    "path": "gradle.properties",
    "content": "## For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/build_environment.html\n#\n# Specifies the JVM arguments used for the daemon process.\n# The setting is particularly useful for tweaking memory settings.\n# Default value: -Xmx1024m -XX:MaxPermSize=256m\n# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8\n#\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. More details, visit\n# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects\n# org.gradle.parallel=true\n#Fri Sep 30 10:30:25 CST 2022\nkotlin.code.style=official\norg.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\\=\"-Xmx2048M\"\nandroid.useAndroidX=true\nandroid.enableJetifier=true\n"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env sh\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ]; do\n  ls=$(ls -ld \"$PRG\")\n  link=$(expr \"$ls\" : '.*-> \\(.*\\)$')\n  if expr \"$link\" : '/.*' >/dev/null; then\n    PRG=\"$link\"\n  else\n    PRG=$(dirname \"$PRG\")\"/$link\"\n  fi\ndone\nSAVED=\"$(pwd)\"\ncd \"$(dirname \\\"$PRG\\\")/\" >/dev/null\nAPP_HOME=\"$(pwd -P)\"\ncd \"$SAVED\" >/dev/null\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=$(basename \"$0\")\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS=\"\"\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn() {\n  echo \"$*\"\n}\n\ndie() {\n  echo\n  echo \"$*\"\n  echo\n  exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"$(uname)\" in\nCYGWIN*)\n  cygwin=true\n  ;;\nDarwin*)\n  darwin=true\n  ;;\nMINGW*)\n  msys=true\n  ;;\nNONSTOP*)\n  nonstop=true\n  ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ]; then\n  if [ -x \"$JAVA_HOME/jre/sh/java\" ]; then\n    # IBM's JDK on AIX uses strange locations for the executables\n    JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n  else\n    JAVACMD=\"$JAVA_HOME/bin/java\"\n  fi\n  if [ ! -x \"$JAVACMD\" ]; then\n    die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n  fi\nelse\n  JAVACMD=\"java\"\n  which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" -a \"$nonstop\" = \"false\" ]; then\n  MAX_FD_LIMIT=$(ulimit -H -n)\n  if [ $? -eq 0 ]; then\n    if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ]; then\n      MAX_FD=\"$MAX_FD_LIMIT\"\n    fi\n    ulimit -n $MAX_FD\n    if [ $? -ne 0 ]; then\n      warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n    fi\n  else\n    warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n  fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n  GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin; then\n  APP_HOME=$(cygpath --path --mixed \"$APP_HOME\")\n  CLASSPATH=$(cygpath --path --mixed \"$CLASSPATH\")\n  JAVACMD=$(cygpath --unix \"$JAVACMD\")\n\n  # We build the pattern for arguments to be converted via cygpath\n  ROOTDIRSRAW=$(find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null)\n  SEP=\"\"\n  for dir in $ROOTDIRSRAW; do\n    ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n    SEP=\"|\"\n  done\n  OURCYGPATTERN=\"(^($ROOTDIRS))\"\n  # Add a user-defined pattern to the cygpath arguments\n  if [ \"$GRADLE_CYGPATTERN\" != \"\" ]; then\n    OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n  fi\n  # Now convert the arguments - kludge to limit ourselves to /bin/sh\n  i=0\n  for arg in \"$@\"; do\n    CHECK=$(echo \"$arg\" | egrep -c \"$OURCYGPATTERN\" -)\n    CHECK2=$(echo \"$arg\" | egrep -c \"^-\") ### Determine if an option\n\n    if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ]; then ### Added a condition\n      eval $(echo args$i)=$(cygpath --path --ignore --mixed \"$arg\")\n    else\n      eval $(echo args$i)=\"\\\"$arg\\\"\"\n    fi\n    i=$((i + 1))\n  done\n  case $i in\n  0) set -- ;;\n  1) set -- \"$args0\" ;;\n  2) set -- \"$args0\" \"$args1\" ;;\n  3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n  4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n  5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n  6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n  7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n  8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n  9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n  esac\nfi\n\n# Escape application args\nsave() {\n  for i; do printf %s\\\\n \"$i\" | sed \"s/'/'\\\\\\\\''/g;1s/^/'/;\\$s/\\$/' \\\\\\\\/\"; done\n  echo \" \"\n}\nAPP_ARGS=$(save \"$@\")\n\n# Collect all arguments for the java command, following the shell quoting and substitution rules\neval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS \"\\\"-Dorg.gradle.appname=$APP_BASE_NAME\\\"\" -classpath \"\\\"$CLASSPATH\\\"\" org.gradle.wrapper.GradleWrapperMain \"$APP_ARGS\"\n\n# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong\nif [ \"$(uname)\" = \"Darwin\" ] && [ \"$HOME\" = \"$PWD\" ]; then\n  cd \"$(dirname \"$0\")\"\nfi\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@if \"%DEBUG%\" == \"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif \"%ERRORLEVEL%\" == \"0\" goto init\r\n\r\necho.\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto init\r\n\r\necho.\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:init\r\n@rem Get command-line arguments, handling Windows variants\r\n\r\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_args\r\n\r\n:win9xME_args\r\n@rem Slurp the command line arguments.\r\nset CMD_LINE_ARGS=\r\nset _SKIP=2\r\n\r\n:win9xME_args_slurp\r\nif \"x%~1\" == \"x\" goto execute\r\n\r\nset CMD_LINE_ARGS=%*\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\r\nexit /b 1\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "jitpack.yml",
    "content": "jdk: openjdk11\ninstall:\n  - ./gradlew build :downloadx:publishToMavenLocal"
  },
  {
    "path": "settings.gradle",
    "content": "rootProject.name='DownloadXDemo'\ninclude ':app'\ninclude ':downloadx'\n"
  }
]