[
  {
    "path": ".editorconfig",
    "content": "[*]\ncharset = utf-8\nij_java_use_single_class_imports = true\nindent_size = 4\nindent_style = space\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n\n[*.yml]\nindent_size = 2\n"
  },
  {
    "path": ".gitattributes",
    "content": "* text=auto eol=lf\n\n*.bat text eol=crlf\n*.jar binary"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: gradle\n    directory: \"/\"\n    schedule:\n      interval: daily\n      time: \"21:00\"\n    open-pull-requests-limit: 10\n    target-branch: master"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non: [ push, pull_request ]\n\njobs:\n  build:\n    name: Build\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        os: [ubuntu-18.04, macOS-latest, windows-2016]\n        java: [11, 11.0.3] \n    steps:\n      - uses: actions/checkout@v1\n      - name: set up JDK 11\n        uses: actions/setup-java@v2\n        with:\n          distribution: 'zulu'\n          java-version: ${{ matrix.java }}\n      - name: Make gradlew executable\n        run: chmod +x ./gradlew\n      - name: Build\n        run: ./gradlew --parallel app:assembleRelease\n"
  },
  {
    "path": ".gitignore",
    "content": ".classpath\n.DS_Store\n.externalNativeBuild\n.project\n.gradle\n.mtj.tmp\n.vscode\n.settings\n.cxx\n\n/.idea\n\nlocal.properties\nmaven-repository\nmvn-clone\nbuild\ncaptures\ngen\nout\ntarget\n\n*.class\n*.txt\n*.ear\n*.iml\n*.jar\n*.keystore\n*.log\n*.nar\n*.rar\n*.tar.gz\n*.war\n*.zip\n*.apk"
  },
  {
    "path": "README.md",
    "content": "![](https://images.xiaozhuanlan.com/photo/2021/b106fd65d34a4a724244e7c5b42a2372.jpg)\n\n[《重学安卓》](https://xiaozhuanlan.com/kunminx)付费读者加微信进群：myatejx\n\n> [免费试读](https://juejin.cn/post/7106042518457810952)，**[专栏目录](https://www.yuque.com/kunminx/fpmbc5/ghlwb5)**，**[更新动态](https://www.yuque.com/kunminx/fpmbc5/in59vu)**，[优惠政策](https://www.yuque.com/kunminx/fpmbc5/of601a)\n\n&nbsp;\n\n# 版权声明\n\n我们就本项目 \"被卖课\" 一事，在掘金发表一期专访 [《开源项目被人拿去做课程卖了 1000 多万是什么体验》](https://juejin.im/post/5ecb4950518825431a669897)\n\n本项目系我为方便开发者们 **无痛理解 Google 开源 Jetpack MVVM 中每个架构组件的 存在缘由、职责边界**，而 **精心设计的高频应用场景**，\n\n与此同时，本项目是作为 [《重学安卓》](https://xiaozhuanlan.com/topic/6017825943)专栏 Jetpack MVVM 系列文章 “配套项目” 而存在，**文章内容和项目代码设计均涉及本人对 Jetpack MVVM 独家理解，本人对此享有著作权**。\n\n任何组织或个人，未经与作者本人沟通，不得将本项目代码设计和本人对 Jetpack MVVM 独家理解用于 \"**打包贩卖、引流、出书 和 卖课**\" 等商业用途。\n\n&nbsp;\n\n# 架构图一览\n\n![](https://images.xiaozhuanlan.com/photo/2023/b10d6c52e0cdb4197725059399fad12f.jpg)\n\n&nbsp;\n\n# 前言\n\n上周我在各大 “技术社区” 发表了一篇 [《Jetpack MVVM 精讲》](https://juejin.im/post/5dafc49b6fb9a04e17209922)，原以为在 “知识网红” 唱衰 Android 的 2019 会无人问津，没想到文章一经发布，从 “国内知名公司” 架构师、技术经理，到 “世界级公司”  Android 开发都在看。\n\n且从读者反馈来看，近期大部分 Android 开发已跳出舒适圈，开始尝试认识和应用 Jetpack MVVM 到实际项目中。\n\n只可惜，关于 Jetpack MVVM，网上多是 **东拼西凑、人云亦云、通篇贴代码** 文章，这不仅不能提供 “完整视角” 帮助读者 首先明确背景状况，更是给还没入门 Jetpack 读者 **徒添困扰**、起 **劝退** 作用。\n\n好消息是，这一期，我们带着 **精心打磨 Jetpack MVVM 最佳实践案例** 来了！\n\n&nbsp;\n&nbsp;\n\n\n|                       爱不释手交互设计                       |                         连贯用户体验                         |                      可信源统一分发                      |\n| :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: |\n| ![](https://upload-images.jianshu.io/upload_images/57036-0a5cdc68f003211a.gif) | ![](https://upload-images.jianshu.io/upload_images/57036-2b21db531e51ff03.gif) | ![](https://upload-images.jianshu.io/upload_images/57036-9a541148ce5bed2e.gif) |\n\n\n\n|                   横竖屏布局无缝切换                   |\n| :----------------------------------------------------: |\n| ![](https://i.loli.net/2021/08/25/X9rado7AfnCEgv3.gif) |\n\n&nbsp;\n&nbsp;\n\n# 项目简介\n\n本人拥有 3 年 “移动端业务架构” 践行和设计经验，领导或参与团队 “重构” 中大型项目多达十数个，对 Jetpack MVVM 架构在 “确立规范化、标准化开发模式 以 **减少不可预期错误**” 所作的努力，有深入理解。\n\n\n\n在这个案例中，我将为你展示，Jetpack MVVM 是如何 **以简驭繁** 地将原本十分容易出错、一出错就会耽搁半天的开发工作，通过寥寥几行代码 轻而易举完成。\n\n> 👆👆👆 划重点！\n\n&nbsp;\n\n该项目中，\n\n> 我们为 **横、竖屏** 场景安排两套 **截然不同布局**，且在 [生命周期](https://xiaozhuanlan.com/topic/0213584967)、[重建机制](https://xiaozhuanlan.com/topic/7692814530)、[状态管理](https://xiaozhuanlan.com/topic/7692814530)、[DataBinding](https://xiaozhuanlan.com/topic/9816742350)、[ViewModel](https://xiaozhuanlan.com/topic/6257931840)、[LiveData](https://xiaozhuanlan.com/topic/0168753249) 、[Navigation](https://xiaozhuanlan.com/topic/5860149732) 等知识点帮助下，通过寥寥几行代码，轻松做到 **在横竖屏两种布局间 无缝切换，且不产生任何 预期外错误**。\n\n\n> 我们在多个 Fragment 页面 分别安排 **播放状态指示器**（包括 播放暂停按钮状态、播放列表当前索引指示 等），并向你展示 “如何” 及 “为何” 通过 [LiveData](https://xiaozhuanlan.com/topic/0168753249) **配合** 可信源 [ViewModel](https://xiaozhuanlan.com/topic/6257931840) 或单例，实现 **全应用范围内 “可追溯事件” 统一分发**。\n\n\n> 我们在 Fragment 和 Activity 之间分别安排 跨页面通信，从而向你展示 如何基于 **迪米特原则**（也称 最少知道原则）、通过 UnPeekLiveData 和 应用级 SharedViewModel 实现 **生命周期安全、确保消息同步可靠一致的 页面通信**。\n\n\n> 我们在 `ui.page` 、`domain.request` 、`data.repository` 等目录下，分别安排 视图控制器、[ViewModel](https://xiaozhuanlan.com/topic/6257931840) 、Dispatcher 、DataRepository 等 内容，从而向你展示，**单向依赖** 架构设计，是如何通过分层 数据请求和响应，**规避 “内存泄漏”** 等问题。\n\n\n> 本项目代码一律采用 经过 ISO 认证 标准化工业级语言 Java 来编写。且在上述类中，我们大都 **提供丰富注释**，助你理解 “骨架代码” 为何要如此设计、如此设计能 **在软件工程背景下** 避免哪些不可预期错误。\n\n&nbsp;\n&nbsp;\n\n除了 **在 \"以简驭繁\" 代码中 掌握 MVVM 最佳实践**，你还可从该项目中获得内容包括：\n\n1. 整洁代码风格 和 标准资源命名规范。\n2. 对 “视图控制器” 知识点的 深入理解 和 正确使用。\n3. AndroidX 和 Material Design 2 全面使用。\n4. ConstraintLayout 约束布局最佳实践。\n5. **优秀的 用户体验 和 交互设计**。\n6. 绝不使用 Dagger，绝不使用奇技淫巧、编写艰深晦涩代码。\n7. The one more thing is：\n\n即日起，可在 \"应用商店\" 下载体验！\n\n[![google-play1.png](https://upload-images.jianshu.io/upload_images/57036-f9dbd7810d38ae95.png)](https://www.coolapk.com/apk/247826) [![coolapk1.png](https://upload-images.jianshu.io/upload_images/57036-6cf24d0c9efe8362.png)](https://www.coolapk.com/apk/247826)\n\n\n&nbsp;\n&nbsp;\n\n# Thanks to\n\n[AndroidX](https://developer.android.google.cn/jetpack/androidx)\n\n[Jetpack](https://developer.android.google.cn/jetpack/)\n\n[material-components-android](https://github.com/material-components/material-components-android)\n\n[轻听](https://play.google.com/store/apps/details?id=com.tencent.qqmusiclocalplayer)\n\n[AndroidSlidingUpPanel](https://github.com/umano/AndroidSlidingUpPanel)\n\n项目中使用 图片素材 来自 [UnSplash](https://unsplash.com/) 提供 **免费授权图片**。\n\n项目中使用 音频素材 来自 [BenSound](https://www.bensound.com/) 提供 **免费授权音乐**。\n\n&nbsp;\n&nbsp;\n\n# Who is using\n\n根据小伙伴们 “开源库使用情况” 匿名调查问卷参与，截至 2022年5月28日，我们了解到\n\n包括 “腾讯音乐、网易、BMW、TCL” 在内诸多知名厂商软件，都参考过我们开源的此架构模式，或正在使用我们维护的 [UnPeek-LiveData](https://github.com/KunMinX/UnPeek-LiveData) 等框架。\n\n目前已将统计数据更新到 相关开源库 ReadMe 中，错过本次问卷调查的小伙伴也不用担心，我们继续对此保持开放，不定期将小伙伴们登记的公司和产品更新到表格，\n\n以便吸纳更多小伙伴参与对 “架构组件” 的使用和反馈，集众人所长，让组件得以不断演化和升级。\n\nhttps://wj.qq.com/s2/8362688/124a/\n\n| 集团 / 公司 / 品牌 / 团队                             | 产品           |\n| ----------------------------------------------------- | -------------- |\n| 腾讯音乐                                              | QQ 音乐        |\n| 网易                                                  | 网易云音乐     |\n| TCL                                                   | 内置文件管理器 |\n| 贵州广电网络                                          | 乐播播         |\n| 上海亿保健康管理有限公司                              | 安诺保         |\n|                                                       | 小辣椒         |\n| ezen                                                  | Egshig音乐     |\n| BMW                                                   | Speech         |\n| 上海互教信息有限公司                                  | 知心慧学教师   |\n| 美术宝                                                | 弹唱宝         |\n|                                                       | 网安           |\n| 字节跳动直播                                          | 直播 SDK       |\n| 一加手机                                              | OPNote         |\n\n&nbsp;\n&nbsp;\n\n\n# My Pages\n\nEmail：[kunminx@gmail.com](mailto:kunminx@gmail.com)\n\nJuejin：[KunMinX 在掘金](https://juejin.im/user/58ab0de9ac502e006975d757/posts)\n\n[《重学安卓》 专栏](https://xiaozhuanlan.com/kunminx)\n\n付费读者加微信进群：myatejx\n\n[![重学安卓小专栏](https://images.xiaozhuanlan.com/photo/2021/d493a54a32e38e7fbcfa68d424ebfd1e.png)](https://xiaozhuanlan.com/kunminx)\n\n&nbsp;\n&nbsp;\n\n# License\n\n```\nCopyright 2019-present KunMinX\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n   http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n```\n\n"
  },
  {
    "path": "app/build.gradle",
    "content": "apply plugin: \"com.android.application\"\n\nandroid {\n    namespace \"com.kunminx.puremusic\"\n    compileSdk appTargetSdk\n    defaultConfig {\n        applicationId \"com.kunminx.puremusic\"\n        minSdk appMinSdk\n        targetSdk appTargetSdk\n        versionCode appVersionCode\n        versionName appVersionName\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n    buildTypes {\n        debug {\n            applicationIdSuffix \".debug\"\n            manifestPlaceholders = [\n                APP_NAME: \"@string/app_name_debug\",\n            ]\n        }\n        release {\n            manifestPlaceholders = [\n                APP_NAME: \"@string/app_name\",\n            ]\n            minifyEnabled true\n            shrinkResources true\n            proguardFiles getDefaultProguardFile(\"proguard-android-optimize.txt\"), \"proguard-rules.pro\"\n        }\n    }\n\n    lintOptions {\n        checkReleaseBuilds false\n        abortOnError false\n    }\n\n    buildFeatures {\n        dataBinding true\n    }\n}\n\ndependencies {\n    implementation fileTree(dir: \"libs\", include: [\"*.jar\", \"*.aar\"])\n    implementation project(\":architecture\")\n\n    testImplementation \"junit:junit:4.13.2\"\n    androidTestImplementation \"androidx.test.ext:junit:1.1.5\"\n    androidTestImplementation \"androidx.test.espresso:espresso-core:3.5.1\"\n\n    implementation \"org.slf4j:slf4j-android:1.7.36\"\n    implementation \"com.sothree.slidinguppanel:library:3.4.0\"\n    implementation 'com.github.KunMinX:Jetpack-MusicPlayer:5.2.0'\n    implementation 'com.github.KunMinX.KeyValueX:keyvalue:3.7.0-beta'\n    annotationProcessor 'com.github.KunMinX.KeyValueX:keyvalue-compiler:3.7.0-beta'\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\n-keep class com.kunminx.puremusic.data.bean.** { *; }\n-keep class com.kunminx.puremusic.data.config.*\n-keep interface com.kunminx.puremusic.data.config.*\n-keep class com.kunminx.player.bean.** { *; }\n\n-keep class * implements android.os.Parcelable {\n  public static final android.os.Parcelable$Creator *;\n}\n\n-keepnames class * implements java.io.Serializable\n\n-keepclassmembers class * implements java.io.Serializable {\n    static final long serialVersionUID;\n    private static final java.io.ObjectStreamField[] serialPersistentFields;\n    !static !transient <fields>;\n    !private <fields>;\n    !private <methods>;\n    private void writeObject(java.io.ObjectOutputStream);\n    private void readObject(java.io.ObjectInputStream);\n    java.lang.Object writeReplace();\n    java.lang.Object readResolve();\n}\n\n# webview\n-keepclassmembers class fqcn.of.javascript.interface.for.Webview {\n   public *;\n}\n-keepclassmembers class * extends android.webkit.WebViewClient {\n    public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);\n    public boolean *(android.webkit.WebView, java.lang.String);\n}\n-keepclassmembers class * extends android.webkit.WebViewClient {\n    public void *(android.webkit.WebView, jav.lang.String);\n}\n\n\n# AndroidX\n\n-keep class com.google.android.material.** {*;}\n-keep class androidx.** {*;}\n-keep public class * extends androidx.**\n-keep interface androidx.** {*;}\n-dontwarn com.google.android.material.**\n-dontnote com.google.android.material.**\n-dontwarn androidx.**\n\n# OkHttp\n\n-dontwarn okhttp3.**\n-keep class okhttp3.**{*;}\n-dontwarn okio.**\n-keep class okio.**{*;}\n\n# glide\n-keep public class * implements com.bumptech.glide.module.GlideModule\n-keep public class * extends com.bumptech.glide.module.AppGlideModule\n-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {\n  **[] $VALUES;\n  public *;\n}\n\n# RxJava\n\n-keep class rx.schedulers.Schedulers {\n    public static <methods>;\n}\n-keep class rx.schedulers.ImmediateScheduler {\n    public <methods>;\n}\n-keep class rx.schedulers.TestScheduler {\n    public <methods>;\n}\n-keep class rx.schedulers.Schedulers {\n    public static ** test();\n}\n-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {\n    long producerIndex;\n    long consumerIndex;\n}\n-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef {\n    long producerNode;\n    long consumerNode;\n}\n-dontwarn sun.misc.Unsafe\n\n\n# Gson\n-keepattributes Signature\n-keepattributes *Annotation*\n-keep class sun.misc.Unsafe { *; }\n-keep class * implements com.google.gson.TypeAdapterFactory\n-keep class * implements com.google.gson.JsonSerializer\n-keep class * implements com.google.gson.JsonDeserializer\n"
  },
  {
    "path": "app/src/androidTest/java/com/kunminx/puremusic/ExampleInstrumentedTest.java",
    "content": "package com.kunminx.puremusic;\n\nimport static org.junit.Assert.assertEquals;\n\nimport android.content.Context;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport androidx.test.platform.app.InstrumentationRegistry;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/**\n * Instrumented test, which will execute on an Android device.\n *\n * @see <a href=\"http://d.android.com/tools/testing\">Testing documentation</a>\n */\n@RunWith(AndroidJUnit4.class)\npublic class ExampleInstrumentedTest {\n    @Test\n    public void useAppContext() {\n        // Context of the app under test.\n        Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();\n\n        assertEquals(\"com.kunminx.puremusic\", appContext.getPackageName());\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    xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n    <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE\" />\n\n    <application\n        android:allowBackup=\"true\"\n        android:icon=\"@drawable/ic_launcher\"\n        android:label=\"${APP_NAME}\"\n        android:networkSecurityConfig=\"@xml/network_security_config\"\n        android:roundIcon=\"@drawable/ic_launcher\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/AppTheme\"\n        android:usesCleartextTraffic=\"true\"\n        tools:ignore=\"AllowBackup,GoogleAppIndexingWarning,UnusedAttribute\">\n\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\n        <service android:name=\".ui.widget.PlayerService\" />\n\n        <receiver\n            android:name=\".domain.message.PlayerReceiver\"\n            android:exported=\"false\"\n            tools:ignore=\"ExportedReceiver\">\n            <intent-filter>\n                <action android:name=\"pure_music.kunminx.close\" />\n                <action android:name=\"pure_music.kunminx.pause\" />\n                <action android:name=\"pure_music.kunminx.next\" />\n                <action android:name=\"pure_music.kunminx.play\" />\n                <action android:name=\"pure_music.kunminx.previous\" />\n                <action android:name=\"android.intent.action.MEDIA_BUTTON\" />\n                <action android:name=\"android.media.AUDIO_BECOMING_NOISY\" />\n            </intent-filter>\n        </receiver>\n\n    </application>\n\n</manifest>\n"
  },
  {
    "path": "app/src/main/assets/summary.html",
    "content": "<!doctype html>\n<html>\n<head>\n    <meta charset='UTF-8'>\n    <meta content='width=device-width initial-scale=1' name='viewport'>\n    <title>前言</title>\n    <style type='text/css'>html {overflow-x: initial !important;}:root { --bg-color:#ffffff; --text-color:#333333; --select-text-bg-color:#B5D6FC; --select-text-font-color:auto; --monospace:\"Lucida Console\",Consolas,\"Courier\",monospace; }\nhtml { font-size: 14px; background-color: var(--bg-color); color: var(--text-color); font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; }\nbody { margin: 0px; padding: 0px; height: auto; bottom: 0px; top: 0px; left: 0px; right: 0px; font-size: 1rem; line-height: 1.42857; overflow-x: hidden; background: inherit; tab-size: 4; }\niframe { margin: auto; }\na.url { word-break: break-all; }\na:active, a:hover { outline: 0px; }\n.in-text-selection, ::selection { text-shadow: none; background: var(--select-text-bg-color); color: var(--select-text-font-color); }\n#write { margin: 0px auto; height: auto; width: inherit; word-break: normal; overflow-wrap: break-word; position: relative; white-space: normal; overflow-x: visible; padding-top: 40px; }\n#write.first-line-indent p { text-indent: 2em; }\n#write.first-line-indent li p, #write.first-line-indent p * { text-indent: 0px; }\n#write.first-line-indent li { margin-left: 2em; }\n.for-image #write { padding-left: 8px; padding-right: 8px; }\nbody.typora-export { padding-left: 30px; padding-right: 30px; }\n.typora-export .footnote-line, .typora-export li, .typora-export p { white-space: pre-wrap; }\n@media screen and (max-width: 500px) {\n  body.typora-export { padding-left: 0px; padding-right: 0px; }\n  #write { padding-left: 20px; padding-right: 20px; }\n  .CodeMirror-sizer { margin-left: 0px !important; }\n  .CodeMirror-gutters { display: none !important; }\n}\n#write li > figure:last-child { margin-bottom: 0.5rem; }\n#write ol, #write ul { position: relative; }\nimg { max-width: 100%; vertical-align: middle; }\nbutton, input, select, textarea { color: inherit; font: inherit; }\ninput[type=\"checkbox\"], input[type=\"radio\"] { line-height: normal; padding: 0px; }\n*, ::after, ::before { box-sizing: border-box; }\n#write h1, #write h2, #write h3, #write h4, #write h5, #write h6, #write p, #write pre { width: inherit; }\n#write h1, #write h2, #write h3, #write h4, #write h5, #write h6, #write p { position: relative; }\np { line-height: inherit; }\nh1, h2, h3, h4, h5, h6 { break-after: avoid-page; break-inside: avoid; orphans: 2; }\np { orphans: 4; }\nh1 { font-size: 2rem; }\nh2 { font-size: 1.8rem; }\nh3 { font-size: 1.6rem; }\nh4 { font-size: 1.4rem; }\nh5 { font-size: 1.2rem; }\nh6 { font-size: 1rem; }\n.md-math-block, .md-rawblock, h1, h2, h3, h4, h5, h6, p { margin-top: 1rem; margin-bottom: 1rem; }\n.hidden { display: none; }\n.md-blockmeta { color: rgb(204, 204, 204); font-weight: 700; font-style: italic; }\na { cursor: pointer; }\nsup.md-footnote { padding: 2px 4px; background-color: rgba(238, 238, 238, 0.7); color: rgb(85, 85, 85); border-radius: 4px; cursor: pointer; }\nsup.md-footnote a, sup.md-footnote a:hover { color: inherit; text-transform: inherit; text-decoration: inherit; }\n#write input[type=\"checkbox\"] { cursor: pointer; width: inherit; height: inherit; }\nfigure { overflow-x: auto; margin: 1.2em 0px; max-width: calc(100% + 16px); padding: 0px; }\nfigure > table { margin: 0px !important; }\ntr { break-inside: avoid; break-after: auto; }\nthead { display: table-header-group; }\ntable { border-collapse: collapse; border-spacing: 0px; width: 100%; overflow: auto; break-inside: auto; text-align: left; }\ntable.md-table td { min-width: 32px; }\n.CodeMirror-gutters { border-right: 0px; background-color: inherit; }\n.CodeMirror-linenumber { user-select: none; }\n.CodeMirror { text-align: left; }\n.CodeMirror-placeholder { opacity: 0.3; }\n.CodeMirror pre { padding: 0px 4px; }\n.CodeMirror-lines { padding: 0px; }\ndiv.hr:focus { cursor: none; }\n#write pre { white-space: pre-wrap; }\n#write.fences-no-line-wrapping pre { white-space: pre; }\n#write pre.ty-contain-cm { white-space: normal; }\n.CodeMirror-gutters { margin-right: 4px; }\n.md-fences { font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; overflow: visible; white-space: pre; background: inherit; position: relative !important; }\n.md-diagram-panel { width: 100%; margin-top: 10px; text-align: center; padding-top: 0px; padding-bottom: 8px; overflow-x: auto; }\n#write .md-fences.mock-cm { white-space: pre-wrap; }\n.md-fences.md-fences-with-lineno { padding-left: 0px; }\n#write.fences-no-line-wrapping .md-fences.mock-cm { white-space: pre; overflow-x: auto; }\n.md-fences.mock-cm.md-fences-with-lineno { padding-left: 8px; }\n.CodeMirror-line, twitterwidget { break-inside: avoid; }\n.footnotes { opacity: 0.8; font-size: 0.9rem; margin-top: 1em; margin-bottom: 1em; }\n.footnotes + .footnotes { margin-top: 0px; }\n.md-reset { margin: 0px; padding: 0px; border: 0px; outline: 0px; vertical-align: top; background: 0px 0px; text-decoration: none; text-shadow: none; float: none; position: static; width: auto; height: auto; white-space: nowrap; cursor: inherit; -webkit-tap-highlight-color: transparent; line-height: normal; font-weight: 400; text-align: left; box-sizing: content-box; direction: ltr; }\nli div { padding-top: 0px; }\nblockquote { margin: 1rem 0px; }\nli .mathjax-block, li p { margin: 0.5rem 0px; }\nli { margin: 0px; position: relative; }\nblockquote > :last-child { margin-bottom: 0px; }\nblockquote > :first-child, li > :first-child { margin-top: 0px; }\n.footnotes-area { color: rgb(136, 136, 136); margin-top: 0.714rem; padding-bottom: 0.143rem; white-space: normal; }\n#write .footnote-line { white-space: pre-wrap; }\n@media print {\n  body, html { border: 1px solid transparent; height: 99%; break-after: avoid; break-before: avoid; }\n  #write { margin-top: 0px; padding-top: 0px; border-color: transparent !important; }\n  .typora-export * { -webkit-print-color-adjust: exact; }\n  html.blink-to-pdf { font-size: 13px; }\n  .typora-export #write { padding-left: 32px; padding-right: 32px; padding-bottom: 0px; break-after: avoid; }\n  .typora-export #write::after { height: 0px; }\n}\n.footnote-line { margin-top: 0.714em; font-size: 0.7em; }\na img, img a { cursor: pointer; }\npre.md-meta-block { font-size: 0.8rem; min-height: 0.8rem; white-space: pre-wrap; background: rgb(204, 204, 204); display: block; overflow-x: hidden; }\np > .md-image:only-child:not(.md-img-error) img, p > img:only-child { display: block; margin: auto; }\np > .md-image:only-child { display: inline-block; width: 100%; }\n#write .MathJax_Display { margin: 0.8em 0px 0px; }\n.md-math-block { width: 100%; }\n.md-math-block:not(:empty)::after { display: none; }\n[contenteditable=\"true\"]:active, [contenteditable=\"true\"]:focus { outline: 0px; box-shadow: none; }\n.md-task-list-item { position: relative; list-style-type: none; }\n.task-list-item.md-task-list-item { padding-left: 0px; }\n.md-task-list-item > input { position: absolute; top: 0px; left: 0px; margin-left: -1.2em; margin-top: calc(1em - 10px); border: none; }\n.math { font-size: 1rem; }\n.md-toc { min-height: 3.58rem; position: relative; font-size: 0.9rem; border-radius: 10px; }\n.md-toc-content { position: relative; margin-left: 0px; }\n.md-toc-content::after, .md-toc::after { display: none; }\n.md-toc-item { display: block; color: rgb(65, 131, 196); }\n.md-toc-item a { text-decoration: none; }\n.md-toc-inner:hover { text-decoration: underline; }\n.md-toc-inner { display: inline-block; cursor: pointer; }\n.md-toc-h1 .md-toc-inner { margin-left: 0px; font-weight: 700; }\n.md-toc-h2 .md-toc-inner { margin-left: 2em; }\n.md-toc-h3 .md-toc-inner { margin-left: 4em; }\n.md-toc-h4 .md-toc-inner { margin-left: 6em; }\n.md-toc-h5 .md-toc-inner { margin-left: 8em; }\n.md-toc-h6 .md-toc-inner { margin-left: 10em; }\n@media screen and (max-width: 48em) {\n  .md-toc-h3 .md-toc-inner { margin-left: 3.5em; }\n  .md-toc-h4 .md-toc-inner { margin-left: 5em; }\n  .md-toc-h5 .md-toc-inner { margin-left: 6.5em; }\n  .md-toc-h6 .md-toc-inner { margin-left: 8em; }\n}\na.md-toc-inner { font-size: inherit; font-style: inherit; font-weight: inherit; line-height: inherit; }\n.footnote-line a:not(.reversefootnote) { color: inherit; }\n.md-attr { display: none; }\n.md-fn-count::after { content: \".\"; }\ncode, pre, samp, tt { font-family: var(--monospace); }\nkbd { margin: 0px 0.1em; padding: 0.1em 0.6em; font-size: 0.8em; color: rgb(36, 39, 41); background: rgb(255, 255, 255); border: 1px solid rgb(173, 179, 185); border-radius: 3px; box-shadow: rgba(12, 13, 14, 0.2) 0px 1px 0px, rgb(255, 255, 255) 0px 0px 0px 2px inset; white-space: nowrap; vertical-align: middle; }\n.md-comment { color: rgb(162, 127, 3); opacity: 0.8; font-family: var(--monospace); }\ncode { text-align: left; vertical-align: initial; }\na.md-print-anchor { white-space: pre !important; border-width: initial !important; border-style: none !important; border-color: initial !important; display: inline-block !important; position: absolute !important; width: 1px !important; right: 0px !important; outline: 0px !important; background: 0px 0px !important; text-decoration: initial !important; text-shadow: initial !important; }\n.md-inline-math .MathJax_SVG .noError { display: none !important; }\n.html-for-mac .inline-math-svg .MathJax_SVG { vertical-align: 0.2px; }\n.md-math-block .MathJax_SVG_Display { text-align: center; margin: 0px; position: relative; text-indent: 0px; max-width: none; max-height: none; min-height: 0px; min-width: 100%; width: auto; overflow-y: hidden; display: block !important; }\n.MathJax_SVG_Display, .md-inline-math .MathJax_SVG_Display { width: auto; margin: inherit; display: inline-block !important; }\n.MathJax_SVG .MJX-monospace { font-family: var(--monospace); }\n.MathJax_SVG .MJX-sans-serif { font-family: sans-serif; }\n.MathJax_SVG { display: inline; font-style: normal; font-weight: 400; line-height: normal; zoom: 90%; text-indent: 0px; text-align: left; text-transform: none; letter-spacing: normal; word-spacing: normal; overflow-wrap: normal; white-space: nowrap; float: none; direction: ltr; max-width: none; max-height: none; min-width: 0px; min-height: 0px; border: 0px; padding: 0px; margin: 0px; }\n.MathJax_SVG * { transition: none 0s ease 0s; }\n.MathJax_SVG_Display svg { vertical-align: middle !important; margin-bottom: 0px !important; margin-top: 0px !important; }\n.os-windows.monocolor-emoji .md-emoji { font-family: \"Segoe UI Symbol\", sans-serif; }\n.md-diagram-panel > svg { max-width: 100%; }\n[lang=\"mermaid\"] svg, [lang=\"flow\"] svg { max-width: 100%; height: auto; }\n[lang=\"mermaid\"] .node text { font-size: 1rem; }\ntable tr th { border-bottom: 0px; }\nvideo { max-width: 100%; display: block; margin: 0px auto; }\niframe { max-width: 100%; width: 100%; border: none; }\n.highlight td, .highlight tr { border: 0px; }\n\n\n.CodeMirror { height: auto; }\n.CodeMirror.cm-s-inner { background: inherit; }\n.CodeMirror-scroll { overflow: auto hidden; z-index: 3; }\n.CodeMirror-gutter-filler, .CodeMirror-scrollbar-filler { background-color: rgb(255, 255, 255); }\n.CodeMirror-gutters { border-right: 1px solid rgb(221, 221, 221); background: inherit; white-space: nowrap; }\n.CodeMirror-linenumber { padding: 0px 3px 0px 5px; text-align: right; color: rgb(153, 153, 153); }\n.cm-s-inner .cm-keyword { color: rgb(119, 0, 136); }\n.cm-s-inner .cm-atom, .cm-s-inner.cm-atom { color: rgb(34, 17, 153); }\n.cm-s-inner .cm-number { color: rgb(17, 102, 68); }\n.cm-s-inner .cm-def { color: rgb(0, 0, 255); }\n.cm-s-inner .cm-variable { color: rgb(0, 0, 0); }\n.cm-s-inner .cm-variable-2 { color: rgb(0, 85, 170); }\n.cm-s-inner .cm-variable-3 { color: rgb(0, 136, 85); }\n.cm-s-inner .cm-string { color: rgb(170, 17, 17); }\n.cm-s-inner .cm-property { color: rgb(0, 0, 0); }\n.cm-s-inner .cm-operator { color: rgb(152, 26, 26); }\n.cm-s-inner .cm-comment, .cm-s-inner.cm-comment { color: rgb(170, 85, 0); }\n.cm-s-inner .cm-string-2 { color: rgb(255, 85, 0); }\n.cm-s-inner .cm-meta { color: rgb(85, 85, 85); }\n.cm-s-inner .cm-qualifier { color: rgb(85, 85, 85); }\n.cm-s-inner .cm-builtin { color: rgb(51, 0, 170); }\n.cm-s-inner .cm-bracket { color: rgb(153, 153, 119); }\n.cm-s-inner .cm-tag { color: rgb(17, 119, 0); }\n.cm-s-inner .cm-attribute { color: rgb(0, 0, 204); }\n.cm-s-inner .cm-header, .cm-s-inner.cm-header { color: rgb(0, 0, 255); }\n.cm-s-inner .cm-quote, .cm-s-inner.cm-quote { color: rgb(0, 153, 0); }\n.cm-s-inner .cm-hr, .cm-s-inner.cm-hr { color: rgb(153, 153, 153); }\n.cm-s-inner .cm-link, .cm-s-inner.cm-link { color: rgb(0, 0, 204); }\n.cm-negative { color: rgb(221, 68, 68); }\n.cm-positive { color: rgb(34, 153, 34); }\n.cm-header, .cm-strong { font-weight: 700; }\n.cm-del { text-decoration: line-through; }\n.cm-em { font-style: italic; }\n.cm-link { text-decoration: underline; }\n.cm-error { color: red; }\n.cm-invalidchar { color: red; }\n.cm-constant { color: rgb(38, 139, 210); }\n.cm-defined { color: rgb(181, 137, 0); }\ndiv.CodeMirror span.CodeMirror-matchingbracket { color: rgb(0, 255, 0); }\ndiv.CodeMirror span.CodeMirror-nonmatchingbracket { color: rgb(255, 34, 34); }\n.cm-s-inner .CodeMirror-activeline-background { background: inherit; }\n.CodeMirror { position: relative; overflow: hidden; }\n.CodeMirror-scroll { height: 100%; outline: 0px; position: relative; box-sizing: content-box; background: inherit; }\n.CodeMirror-sizer { position: relative; }\n.CodeMirror-gutter-filler, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-vscrollbar { position: absolute; z-index: 6; display: none; }\n.CodeMirror-vscrollbar { right: 0px; top: 0px; overflow: hidden; }\n.CodeMirror-hscrollbar { bottom: 0px; left: 0px; overflow: hidden; }\n.CodeMirror-scrollbar-filler { right: 0px; bottom: 0px; }\n.CodeMirror-gutter-filler { left: 0px; bottom: 0px; }\n.CodeMirror-gutters { position: absolute; left: 0px; top: 0px; padding-bottom: 30px; z-index: 3; }\n.CodeMirror-gutter { white-space: normal; height: 100%; box-sizing: content-box; padding-bottom: 30px; margin-bottom: -32px; display: inline-block; }\n.CodeMirror-gutter-wrapper { position: absolute; z-index: 4; background: 0px 0px !important; border: none !important; }\n.CodeMirror-gutter-background { position: absolute; top: 0px; bottom: 0px; z-index: 4; }\n.CodeMirror-gutter-elt { position: absolute; cursor: default; z-index: 4; }\n.CodeMirror-lines { cursor: text; }\n.CodeMirror pre { border-radius: 0px; border-width: 0px; background: 0px 0px; font-family: inherit; font-size: inherit; margin: 0px; white-space: pre; overflow-wrap: normal; color: inherit; z-index: 2; position: relative; overflow: visible; }\n.CodeMirror-wrap pre { overflow-wrap: break-word; white-space: pre-wrap; word-break: normal; }\n.CodeMirror-code pre { border-right: 30px solid transparent; width: fit-content; }\n.CodeMirror-wrap .CodeMirror-code pre { border-right: none; width: auto; }\n.CodeMirror-linebackground { position: absolute; left: 0px; right: 0px; top: 0px; bottom: 0px; z-index: 0; }\n.CodeMirror-linewidget { position: relative; z-index: 2; overflow: auto; }\n.CodeMirror-wrap .CodeMirror-scroll { overflow-x: hidden; }\n.CodeMirror-measure { position: absolute; width: 100%; height: 0px; overflow: hidden; visibility: hidden; }\n.CodeMirror-measure pre { position: static; }\n.CodeMirror div.CodeMirror-cursor { position: absolute; visibility: hidden; border-right: none; width: 0px; }\n.CodeMirror div.CodeMirror-cursor { visibility: hidden; }\n.CodeMirror-focused div.CodeMirror-cursor { visibility: inherit; }\n.cm-searching { background: rgba(255, 255, 0, 0.4); }\n@media print {\n  .CodeMirror div.CodeMirror-cursor { visibility: hidden; }\n}\n\n\n:root { --side-bar-bg-color: #fafafa; --control-text-color: #777; }\nhtml { font-size: 16px; }\nbody { font-family: \"Open Sans\", \"Clear Sans\", \"Helvetica Neue\", Helvetica, Arial, sans-serif; color: rgb(51, 51, 51); line-height: 1.6; }\n#write { max-width: 860px; margin: 0px auto; padding: 30px 30px 100px; }\n#write > ul:first-child, #write > ol:first-child { margin-top: 30px; }\na { color: rgb(65, 131, 196); }\nh1, h2, h3, h4, h5, h6 { position: relative; margin-top: 1rem; margin-bottom: 1rem; font-weight: bold; line-height: 1.4; cursor: text; }\nh1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, h4:hover a.anchor, h5:hover a.anchor, h6:hover a.anchor { text-decoration: none; }\nh1 tt, h1 code { font-size: inherit; }\nh2 tt, h2 code { font-size: inherit; }\nh3 tt, h3 code { font-size: inherit; }\nh4 tt, h4 code { font-size: inherit; }\nh5 tt, h5 code { font-size: inherit; }\nh6 tt, h6 code { font-size: inherit; }\nh1 { padding-bottom: 0.3em; font-size: 2.25em; line-height: 1.2; border-bottom: 1px solid rgb(238, 238, 238); }\nh2 { padding-bottom: 0.3em; font-size: 1.75em; line-height: 1.225; border-bottom: 1px solid rgb(238, 238, 238); }\nh3 { font-size: 1.5em; line-height: 1.43; }\nh4 { font-size: 1.25em; }\nh5 { font-size: 1em; }\nh6 { font-size: 1em; color: rgb(119, 119, 119); }\np, blockquote, ul, ol, dl, table { margin: 0.8em 0px; }\nli > ol, li > ul { margin: 0px; }\nhr { height: 2px; padding: 0px; margin: 16px 0px; background-color: rgb(231, 231, 231); border: 0px none; overflow: hidden; box-sizing: content-box; }\nli p.first { display: inline-block; }\nul, ol { padding-left: 30px; }\nul:first-child, ol:first-child { margin-top: 0px; }\nul:last-child, ol:last-child { margin-bottom: 0px; }\nblockquote { border-left: 4px solid rgb(223, 226, 229); padding: 0px 15px; color: rgb(119, 119, 119); }\nblockquote blockquote { padding-right: 0px; }\ntable { padding: 0px; word-break: initial; }\ntable tr { border-top: 1px solid rgb(223, 226, 229); margin: 0px; padding: 0px; }\ntable tr:nth-child(2n), thead { background-color: rgb(248, 248, 248); }\ntable tr th { font-weight: bold; border-width: 1px 1px 0px; border-top-style: solid; border-right-style: solid; border-left-style: solid; border-top-color: rgb(223, 226, 229); border-right-color: rgb(223, 226, 229); border-left-color: rgb(223, 226, 229); border-image: initial; border-bottom-style: initial; border-bottom-color: initial; margin: 0px; padding: 6px 13px; }\ntable tr td { border: 1px solid rgb(223, 226, 229); margin: 0px; padding: 6px 13px; }\ntable tr th:first-child, table tr td:first-child { margin-top: 0px; }\ntable tr th:last-child, table tr td:last-child { margin-bottom: 0px; }\n.CodeMirror-lines { padding-left: 4px; }\n.code-tooltip { box-shadow: rgba(0, 28, 36, 0.3) 0px 1px 1px 0px; border-top: 1px solid rgb(238, 242, 242); }\n.md-fences, code, tt { border: 1px solid rgb(231, 234, 237); background-color: rgb(248, 248, 248); border-radius: 3px; padding: 2px 4px 0px; font-size: 0.9em; }\ncode { background-color: rgb(243, 244, 244); padding: 0px 2px; }\n.md-fences { margin-bottom: 15px; margin-top: 15px; padding-top: 8px; padding-bottom: 6px; }\n.md-task-list-item > input { margin-left: -1.3em; }\n@media print {\n  html { font-size: 13px; }\n  table, pre { break-inside: avoid; }\n  pre { overflow-wrap: break-word; }\n}\n.md-fences { background-color: rgb(248, 248, 248); }\n#write pre.md-meta-block { padding: 1rem; font-size: 85%; line-height: 1.45; background-color: rgb(247, 247, 247); border: 0px; border-radius: 3px; color: rgb(119, 119, 119); margin-top: 0px !important; }\n.mathjax-block > .code-tooltip { bottom: 0.375rem; }\n.md-mathjax-midline { background: rgb(250, 250, 250); }\n#write > h3.md-focus::before { left: -1.5625rem; top: 0.375rem; }\n#write > h4.md-focus::before { left: -1.5625rem; top: 0.285714rem; }\n#write > h5.md-focus::before { left: -1.5625rem; top: 0.285714rem; }\n#write > h6.md-focus::before { left: -1.5625rem; top: 0.285714rem; }\n.md-image > .md-meta { border-radius: 3px; padding: 2px 0px 0px 4px; font-size: 0.9em; color: inherit; }\n.md-tag { color: rgb(167, 167, 167); opacity: 1; }\n.md-toc { margin-top: 20px; padding-bottom: 20px; }\n.sidebar-tabs { border-bottom: none; }\n#typora-quick-open { border: 1px solid rgb(221, 221, 221); background-color: rgb(248, 248, 248); }\n#typora-quick-open-item { background-color: rgb(250, 250, 250); border-color: rgb(254, 254, 254) rgb(229, 229, 229) rgb(229, 229, 229) rgb(238, 238, 238); border-style: solid; border-width: 1px; }\n.on-focus-mode blockquote { border-left-color: rgba(85, 85, 85, 0.12); }\nheader, .context-menu, .megamenu-content, footer { font-family: \"Segoe UI\", Arial, sans-serif; }\n.file-node-content:hover .file-node-icon, .file-node-content:hover .file-node-open-state { visibility: visible; }\n.mac-seamless-mode #typora-sidebar { background-color: var(--side-bar-bg-color); }\n.md-lang { color: rgb(180, 101, 77); }\n.html-for-mac .context-menu { --item-hover-bg-color: #E6F0FE; }\n#md-notification .btn { border: 0px; }\n.dropdown-menu .divider { border-color: rgb(229, 229, 229); }\n.ty-preferences .window-content { background-color: rgb(250, 250, 250); }\n.ty-preferences .nav-group-item.active { color: white; background: rgb(153, 153, 153); }\n\n .typora-export li, .typora-export p, .typora-export,  .footnote-line {white-space: normal;}\n\n\n\n    </style>\n</head>\n<body class='typora-export os-windows'>\n<div class='is-node' id='write'><p><a\n        href='https://xiaozhuanlan.com/kunminx'><span>《重学安卓》</span></a><span>付费读者加微信进群：myatejx</span>\n</p>\n    <p>&nbsp;</p>\n    <h2><a class=\"md-header-anchor\" name=\"前言\"></a><span>前言</span></h2>\n    <p><span>很高兴见到你！</span></p>\n    <p><span>上周我在 各大技术社区 发表了一篇 </span><a\n            href='https://juejin.im/post/5dafc49b6fb9a04e17209922'><span>《Jetpack MVVM 精讲》</span></a><span>，原以为在 知识网红 唱衰安卓 的 2019 会无人问津，没想到文章一经发布，从 国内知名公司 的架构师、技术经理，到 世界级公司 的 Android 开发 都在看。</span>\n    </p>\n    <p><img alt='reader_say.png'\n            referrerPolicy='no-referrer'\n            src='https://upload-images.jianshu.io/upload_images/57036-5445e7b4d66d97c7.png'/></p>\n    <p><span>并且从读者的反馈来看，近期大部分安卓开发 已跳出舒适圈，开始尝试认识和应用 Jetpack MVVM 到实际的项目开发中。</span></p>\n    <p><span>只可惜，关于 Jetpack MVVM，网上多是 </span><strong><span>东拼西凑、人云亦云、通篇贴代码</span></strong><span> 的文章，这不仅不能提供完整的视角 来帮助读者 首先明确背景状况，更是给还没入门 Jetpack 的读者 </span><strong><span>徒添困扰</span></strong><span>、起到 </span><strong><span>劝退</span></strong><span> 的作用。</span>\n    </p>\n    <p>\n        <span>好消息是，这一期，我们带着 </span><strong><span>精心打磨的 Jetpack MVVM 最佳实践案例</span></strong><span> 来了！</span>\n        &nbsp;</p>\n    <figure>\n        <table>\n            <thead>\n            <tr>\n                <th style='text-align:center;'><span>是让人 爱不释手 的 交互设计！</span></th>\n                <th style='text-align:center;'><span>是 连贯 的 用户体验</span></th>\n                <th style='text-align:center;'><span>可信源 的 统一分发</span></th>\n            </tr>\n            </thead>\n            <tbody>\n            <tr>\n                <td style='text-align:center;'><img\n                        alt='1231111323.gif'\n                        referrerPolicy='no-referrer'\n                        src='https://upload-images.jianshu.io/upload_images/57036-0a5cdc68f003211a.gif'/>\n                </td>\n                <td style='text-align:center;'><img\n                        alt='222.gif'\n                        referrerPolicy='no-referrer'\n                        src='https://upload-images.jianshu.io/upload_images/57036-2b21db531e51ff03.gif'/>\n                </td>\n                <td style='text-align:center;'><img\n                        alt='333.gif'\n                        referrerPolicy='no-referrer'\n                        src='https://upload-images.jianshu.io/upload_images/57036-9a541148ce5bed2e.gif'/>\n                </td>\n            </tr>\n            </tbody>\n        </table>\n    </figure>\n    <p>&nbsp;</p>\n    <figure>\n        <table>\n            <thead>\n            <tr>\n                <th style='text-align:center;'><span>横竖屏布局 的 无缝切换</span></th>\n            </tr>\n            </thead>\n            <tbody>\n            <tr>\n                <td style='text-align:center;'><img\n                        alt='444.gif'\n                        referrerPolicy='no-referrer'\n                        src='https://upload-images.jianshu.io/upload_images/57036-688f3eafc76cfa27.gif'/>\n                </td>\n            </tr>\n            </tbody>\n        </table>\n    </figure>\n    <p>&nbsp;</p>\n    <h2><a class=\"md-header-anchor\" name=\"项目简介\"></a><span>项目简介</span></h2>\n    <p><span>本人拥有 3 年的 移动端架构 践行和设计经验，领导或参与团队重构的 中大型项目 多达十数个，对 Jetpack MVVM 架构在 确立规范化、标准化 开发模式 以 </span><strong><span>减少不可预期的错误</span></strong><span> 所作的努力，有着深入的理解。</span>\n    </p>\n    <p><span>在这个案例中，我将为你展示，Jetpack MVVM 是如何 </span><strong><span>以简驭繁</span></strong><span> 地 将原本十分容易出错、一出错就会耽搁半天时间的开发工作，通过 寥寥的几行代码 轻而易举地完成。</span>\n    </p>\n    <blockquote><p><span>👆👆👆 划重点！</span></p></blockquote>\n    <p>&nbsp;</p>\n    <p><span>在这个项目中，</span></p>\n    <blockquote><p>\n        <span>我们为 </span><strong><span>横、竖屏</span></strong><span> 的情况 分别安排了两套 </span><strong><span>截然不同的布局</span></strong><span>，并且在 </span><a\n            href='https://xiaozhuanlan.com/topic/0213584967'><span>生命周期</span></a><span>、</span><a\n            href='https://xiaozhuanlan.com/topic/7692814530'><span>重建机制</span></a><span>、</span><a\n            href='https://xiaozhuanlan.com/topic/7692814530'><span>状态管理</span></a><span>、</span><a\n            href='https://xiaozhuanlan.com/topic/9816742350'><span>DataBinding</span></a><span>、</span><a\n            href='https://xiaozhuanlan.com/topic/6257931840'><span>ViewModel</span></a><span>、</span><a\n            href='https://xiaozhuanlan.com/topic/0168753249'><span>LiveData</span></a><span> 、</span><a\n            href='https://xiaozhuanlan.com/topic/5860149732'><span>Navigation</span></a><span> 等知识点的帮助下，通过寥寥几行代码，轻松做到 </span><strong><span>在横竖屏两种布局间 无缝地切换，并且不产生任何 预期外的错误</span></strong><span>。</span>\n    </p></blockquote>\n    <p>&nbsp;</p>\n    <blockquote><p><span>我们在多个 Fragment 页面 分别安排了 </span><strong><span>播放状态 指示器</span></strong><span>（包括 播放暂停按钮状态、播放列表当前索引指示 等），并向你展示了 如何 以及为何 通过 </span><a\n            href='https://xiaozhuanlan.com/topic/0168753249'><span>LiveData</span></a><span> </span><strong><span>配合</span></strong><span> 作为可信源 的 </span><a\n            href='https://xiaozhuanlan.com/topic/6257931840'><span>ViewModel</span></a><span> 或单例，来实现 </span><strong><span>全应用范围内 可追溯事件 的统一分发</span></strong><span>。</span>\n    </p></blockquote>\n    <p>&nbsp;</p>\n    <blockquote><p><span>我们在 Fragment 和 Activity 之间分别安排了 跨页面通信，从而向你展示 如何基于 </span><strong><span>迪米特原则</span></strong><span>（也称 最少知道原则）、通过 UnPeekLiveData 和 应用级 SharedViewModel 来实现 </span><strong><span>生命周期安全的、确保消息同步一致性和可靠性的 页面通信</span></strong><span>（事件回调）。</span>\n    </p></blockquote>\n    <p>&nbsp;</p>\n    <blockquote><p>\n        <span>我们在 </span><code>ui.page</code><span> 、</span><code>data.repository</code><span>、</span><code>bridge.request</code><span> 等目录下，分别安排了 视图控制器、</span><a\n            href='https://xiaozhuanlan.com/topic/6257931840'><span>ViewModel</span></a><span> 、DataRepository 等 内容，从而向你展示，</span><strong><span>单向依赖</span></strong><span> 的架构设计，是如何通过分层的 数据请求和响应，来 </span><strong><span>规避 内存泄漏</span></strong><span> 等问题。</span>\n    </p></blockquote>\n    <p>&nbsp;</p>\n    <blockquote><p>\n        <span>本项目的代码一律采用 经过 ISO 认证的 标准化工业级语言 Java 来编写。并且，在上述目录 所包含的 类中，我们大都 </span><strong><span>提供了丰富的注释</span></strong><span>，来帮助你理解 骨架代码 为何要如此设计、如此设计能够 </span><strong><span>在软件工程的背景下</span></strong><span> 避免哪些不可预期的错误。</span>\n    </p></blockquote>\n    <p>&nbsp;\n        &nbsp;</p>\n    <p><span>除了 </span><strong><span>在 以简驭繁 的代码中 掌握 MVVM 最佳实践</span></strong><span>，你还可以 从这个开源项目中 获得的内容 包括：</span>\n    </p>\n    <ol start=''>\n        <li><span>整洁的代码风格 和 标准的资源命名规范。</span></li>\n        <li><span>对 视图控制器 知识点的 深入理解 和 正确使用。</span></li>\n        <li><span>AndroidX 和 Material Design 2 的全面使用。</span></li>\n        <li><span>ConstraintLayout 约束布局的最佳实践。</span></li>\n        <li><strong><span>优秀的 用户体验 和 交互设计</span></strong><span>。</span></li>\n        <li><span>绝不使用 Dagger，绝不使用奇技淫巧、编写艰深晦涩的代码。</span></li>\n        <li><span>The one more thing is：</span></li>\n    </ol>\n    <p><span>即日起，可在 应用商店 下载体验！</span></p>\n    <p>&nbsp;</p>\n    <p><a href='https://www.coolapk.com/apk/247826'><img\n            alt='google-play1.png'\n            referrerPolicy='no-referrer'\n            src='https://upload-images.jianshu.io/upload_images/57036-f9dbd7810d38ae95.png'/></a><span> </span><a\n            href='https://www.coolapk.com/apk/247826'><img\n            alt='coolapk1.png'\n            referrerPolicy='no-referrer'\n            src='https://upload-images.jianshu.io/upload_images/57036-6cf24d0c9efe8362.png'/></a>\n    </p>\n    <p>&nbsp;\n        &nbsp;</p>\n    <h2><a class=\"md-header-anchor\" name=\"thanks-to\"></a><span>Thanks to</span></h2>\n    <p><a href='https://developer.android.google.cn/jetpack/androidx'><span>AndroidX</span></a></p>\n    <p><a href='https://developer.android.google.cn/jetpack/'><span>Jetpack</span></a></p>\n    <p><a href='https://github.com/material-components/material-components-android'><span>material-components-android</span></a>\n    </p>\n    <p><a href='https://play.google.com/store/apps/details?id=com.tencent.qqmusiclocalplayer'><span>轻听</span></a>\n    </p>\n    <p>\n        <a href='https://github.com/umano/AndroidSlidingUpPanel'><span>AndroidSlidingUpPanel</span></a>\n    </p>\n    <p><span>项目中使用的 图片素材 来自 </span><a href='https://unsplash.com/'><span>UnSplash</span></a><span> 提供的 </span><strong><span>无版权免费图片</span></strong><span>。</span>\n    </p>\n    <p><span>项目中使用的 音频素材 来自 </span><a\n            href='https://www.bensound.com/'><span>BenSound</span></a><span> 提供的 </span><strong><span>无版权免费音乐</span></strong><span>。</span>\n    </p>\n    <p>&nbsp;\n        &nbsp;</p>\n    <h2><a class=\"md-header-anchor\" name=\"my-pages\"></a><span>My Pages</span></h2>\n    <p><span>Email：</span><a href='mailto:kunminx@gmail.com'><span>kunminx@gmail.com</span></a></p>\n    <p><span>Home：</span><a href='https://www.kunminx.com/'><span>KunMinX 的个人博客</span></a></p>\n    <p><span>Juejin：</span><a href='https://juejin.im/user/58ab0de9ac502e006975d757/posts'><span>KunMinX 在掘金</span></a>\n    </p>\n    <p><a href='https://xiaozhuanlan.com/kunminx?rel=kunminx'><span>《重学安卓》 专栏</span></a></p>\n    <p><span>付费读者加微信进群：myatejx</span></p>\n    <p><a href='https://xiaozhuanlan.com/kunminx?rel=kunminx'><img\n            alt='重学安卓小专栏' referrerPolicy='no-referrer'\n            src='https://images.xiaozhuanlan.com/photo/2021/d493a54a32e38e7fbcfa68d424ebfd1e.png'/></a></p>\n    <h2><a class=\"md-header-anchor\" name=\"license\"></a><span>License</span></h2>\n    <pre class=\"md-fences md-end-block ty-contain-cm modeLoaded\" lang=\"\" spellcheck=\"false\"><div\n            class=\"CodeMirror cm-s-inner CodeMirror-wrap\" lang=\"\"><div\n            style=\"overflow: hidden; position: relative; width: 3px; height: 0px; top: 0px; left: 8px;\"><textarea\n            autocapitalize=\"off\" autocorrect=\"off\" spellcheck=\"false\"\n            style=\"position: absolute; bottom: -1em; padding: 0px; width: 1000px; height: 1em; outline: none;\"\n            tabindex=\"0\"></textarea></div><div\n            class=\"CodeMirror-scrollbar-filler\" cm-not-content=\"true\"></div><div\n            class=\"CodeMirror-gutter-filler\" cm-not-content=\"true\"></div><div\n            class=\"CodeMirror-scroll\" tabindex=\"-1\"><div class=\"CodeMirror-sizer\"\n                                                         style=\"margin-left: 0px; margin-bottom: 0px; border-right-width: 0px; padding-right: 0px; padding-bottom: 0px;\"><div\n            style=\"position: relative; top: 0px;\"><div class=\"CodeMirror-lines\" role=\"presentation\"><div\n            role=\"presentation\" style=\"position: relative; outline: none;\"><div\n            class=\"CodeMirror-measure\"><span><span>​</span>x</span></div><div\n            class=\"CodeMirror-measure\"></div><div style=\"position: relative; z-index: 1;\"></div><div\n            class=\"CodeMirror-code\" role=\"presentation\" style=\"\"><div class=\"CodeMirror-activeline\"\n                                                                      style=\"position: relative;\"><div\n            class=\"CodeMirror-activeline-background CodeMirror-linebackground\"></div><div\n            class=\"CodeMirror-gutter-background CodeMirror-activeline-gutter\"\n            style=\"left: 0px; width: 0px;\"></div><pre class=\" CodeMirror-line \" role=\"presentation\"><span\n            role=\"presentation\"\n            style=\"padding-right: 0.1px;\">Copyright 2018-present KunMinX</span></pre></div><pre\n            class=\" CodeMirror-line \" role=\"presentation\"><span role=\"presentation\"\n                                                                style=\"padding-right: 0.1px;\"><span\n            cm-text=\"\">​</span></span></pre><pre class=\" CodeMirror-line \" role=\"presentation\"><span\n            role=\"presentation\" style=\"padding-right: 0.1px;\">Licensed under the Apache License, Version 2.0 (the \"License\");</span></pre><pre\n            class=\" CodeMirror-line \" role=\"presentation\"><span role=\"presentation\"\n                                                                style=\"padding-right: 0.1px;\">you may not use this file except in compliance with the License.</span></pre><pre\n            class=\" CodeMirror-line \" role=\"presentation\"><span role=\"presentation\"\n                                                                style=\"padding-right: 0.1px;\">You may obtain a copy of the License at</span></pre><pre\n            class=\" CodeMirror-line \" role=\"presentation\"><span role=\"presentation\"\n                                                                style=\"padding-right: 0.1px;\"><span\n            cm-text=\"\">​</span></span></pre><pre class=\" CodeMirror-line \" role=\"presentation\"><span\n            role=\"presentation\" style=\"padding-right: 0.1px;\"> &nbsp; http://www.apache.org/licenses/LICENSE-2.0</span></pre><pre\n            class=\" CodeMirror-line \" role=\"presentation\"><span role=\"presentation\"\n                                                                style=\"padding-right: 0.1px;\"><span\n            cm-text=\"\">​</span></span></pre><pre class=\" CodeMirror-line \" role=\"presentation\"><span\n            role=\"presentation\" style=\"padding-right: 0.1px;\">Unless required by applicable law or agreed to in writing, software</span></pre><pre\n            class=\" CodeMirror-line \" role=\"presentation\"><span role=\"presentation\"\n                                                                style=\"padding-right: 0.1px;\">distributed under the License is distributed on an \"AS IS\" BASIS,</span></pre><pre\n            class=\" CodeMirror-line \" role=\"presentation\"><span role=\"presentation\"\n                                                                style=\"padding-right: 0.1px;\">WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.</span></pre><pre\n            class=\" CodeMirror-line \" role=\"presentation\"><span role=\"presentation\"\n                                                                style=\"padding-right: 0.1px;\">See the License for the specific language governing permissions and</span></pre><pre\n            class=\" CodeMirror-line \" role=\"presentation\"><span role=\"presentation\"\n                                                                style=\"padding-right: 0.1px;\">limitations under the License.</span></pre></div></div></div></div></div><div\n            style=\"position: absolute; height: 0px; width: 1px; border-bottom: 0px solid transparent; top: 291px;\"></div><div\n            class=\"CodeMirror-gutters\"\n            style=\"display: none; height: 291px;\"></div></div></div></pre>\n    <p>&nbsp;</p></div>\n</body>\n</html>\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/MainActivity.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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\npackage com.kunminx.puremusic;\n\nimport android.os.Bundle;\nimport android.view.View;\n\nimport androidx.drawerlayout.widget.DrawerLayout;\nimport androidx.navigation.NavController;\nimport androidx.navigation.Navigation;\n\nimport com.kunminx.architecture.ui.page.BaseActivity;\nimport com.kunminx.architecture.ui.page.DataBindingConfig;\nimport com.kunminx.architecture.ui.page.StateHolder;\nimport com.kunminx.architecture.ui.state.State;\nimport com.kunminx.puremusic.domain.event.Messages;\nimport com.kunminx.puremusic.domain.message.DrawerCoordinateManager;\nimport com.kunminx.puremusic.domain.message.PageMessenger;\nimport com.kunminx.puremusic.domain.proxy.PlayerManager;\n\n/**\n * Create by KunMinX at 19/10/16\n */\n\npublic class MainActivity extends BaseActivity {\n\n    //TODO tip 1：基于 \"单一职责原则\"，应将 ViewModel 划分为 state-ViewModel 和 result-ViewModel，\n    // state-ViewModel 职责仅限于托管、保存和恢复本页面 state，作用域仅限于本页面，\n    // result-ViewModel 职责仅限于 \"消息分发\" 场景承担 \"可信源\"，作用域依 \"数据请求\" 或 \"跨页通信\" 消息分发范围而定\n\n    // 如这么说无体会，详见 https://xiaozhuanlan.com/topic/8204519736\n\n    private MainActivityStates mStates;\n    private PageMessenger mMessenger;\n    private boolean mIsListened = false;\n\n    @Override\n    protected void initViewModel() {\n        mStates = getActivityScopeViewModel(MainActivityStates.class);\n        mMessenger = getApplicationScopeViewModel(PageMessenger.class);\n    }\n\n    @Override\n    protected DataBindingConfig getDataBindingConfig() {\n\n        //TODO tip 2: DataBinding 严格模式：\n        // 将 DataBinding 实例限制于 base 页面中，默认不向子类暴露，\n        // 通过这方式，彻底解决 View 实例 Null 安全一致性问题，\n        // 如此，View 实例 Null 安全性将和基于函数式编程思想的 Jetpack Compose 持平。\n        // 而 DataBindingConfig 就是在这样背景下，用于为 base 页面 DataBinding 提供绑定项。\n\n        // 如这么说无体会，详见 https://xiaozhuanlan.com/topic/9816742350 和 https://xiaozhuanlan.com/topic/2356748910\n\n        return new DataBindingConfig(R.layout.activity_main, BR.vm, mStates)\n            .addBindingParam(BR.listener, new ListenerHandler());\n    }\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n\n        PlayerManager.getInstance().init(this);\n\n        //TODO tip 6: 从 PublishSubject 接收回推的数据，并在回调中响应数据的变化，\n        // 也即通过 BehaviorSubject（例如 ObservableField）通知控件属性重新渲染，并为其兜住最后一次状态，\n\n        //如这么说无体会，详见 https://xiaozhuanlan.com/topic/6741932805\n\n        mMessenger.output(this, messages -> {\n            switch (messages.eventId) {\n                case Messages.EVENT_CLOSE_ACTIVITY_IF_ALLOWED:\n                    NavController nav = Navigation.findNavController(this, R.id.main_fragment_host);\n                    if (nav.getCurrentDestination() != null && nav.getCurrentDestination().getId() != R.id.mainFragment) {\n                        nav.navigateUp();\n                    } else if (Boolean.TRUE.equals(mStates.isDrawerOpened.get())) {\n\n                        //TODO 同 tip 3\n                        mStates.openDrawer.set(false);\n                    } else {\n                        super.onBackPressed();\n                    }\n                    break;\n                case Messages.EVENT_OPEN_DRAWER:\n\n                    //TODO yes：同 tip 2:\n                    // 此处将 drawer 的 open 和 close 都放在 drawerBindingAdapter 中操作，\n                    // 规避 View 实例 Null 安全一致性问题，因为横屏布局无 drawerLayout。\n                    // 此处如果用手动判空，很容易因疏忽而造成空引用。\n\n                    //TODO 此外，此处为 drawerLayout 绑定状态 \"openDrawer\"，使用 \"去防抖\" ObservableField 子类，\n                    // 主要考虑到 ObservableField 具有 \"防抖\" 特性，不适合该场景。\n\n                    //如这么说无体会，详见 https://xiaozhuanlan.com/topic/9816742350\n\n                    mStates.openDrawer.set(true);\n\n                    //TODO do not:（容易因疏忽埋下 View 实例 Null 安全一致性隐患）\n\n                    /*if (mBinding.dl != null) {\n                        if (aBoolean && !mBinding.dl.isDrawerOpen(GravityCompat.START)) {\n                            mBinding.dl.openDrawer(GravityCompat.START);\n                        } else {\n                            mBinding.dl.closeDrawer(GravityCompat.START);\n                        }\n                    }*/\n                    break;\n            }\n        });\n\n        DrawerCoordinateManager.getInstance().isEnableSwipeDrawer().observe(this, aBoolean -> {\n\n            //TODO yes: 同 tip 2\n\n            mStates.allowDrawerOpen.set(aBoolean);\n\n            // TODO do not:（容易因疏忽埋下 View 实例 Null 安全一致性隐患）\n\n            /*if (mBinding.dl != null) {\n                mBinding.dl.setDrawerLockMode(aBoolean\n                        ? DrawerLayout.LOCK_MODE_UNLOCKED\n                        : DrawerLayout.LOCK_MODE_LOCKED_CLOSED);\n            }*/\n        });\n    }\n\n    @Override\n    public void onWindowFocusChanged(boolean hasFocus) {\n        super.onWindowFocusChanged(hasFocus);\n        if (!mIsListened) {\n\n            // TODO tip 3：此处演示向 \"可信源\" 发送请求，以便实现 \"生命周期安全、消息分发可靠一致\" 的通知。\n\n            // 如这么说无体会，详见 https://xiaozhuanlan.com/topic/0168753249\n            // --------\n            // 与此同时，此处传达的另一思想是 \"最少知道原则\"，\n            // Activity 内部事情在 Activity 内部消化，不要试图在 fragment 中调用和操纵 Activity 内部东西。\n            // 因为 Activity 端的处理后续可能会改变，且可受用于更多 fragment，而不单单是本 fragment。\n\n            mMessenger.input(new Messages(Messages.EVENT_ADD_SLIDE_LISTENER));\n\n            mIsListened = true;\n        }\n    }\n\n    @Override\n    public void onBackPressed() {\n\n        // TODO 同 tip 3\n\n        mMessenger.input(new Messages(Messages.EVENT_CLOSE_SLIDE_PANEL_IF_EXPANDED));\n    }\n\n    public class ListenerHandler extends DrawerLayout.SimpleDrawerListener {\n        @Override\n        public void onDrawerOpened(View drawerView) {\n            super.onDrawerOpened(drawerView);\n            mStates.isDrawerOpened.set(true);\n        }\n\n        @Override\n        public void onDrawerClosed(View drawerView) {\n            super.onDrawerClosed(drawerView);\n            mStates.isDrawerOpened.set(false);\n            mStates.openDrawer.set(false);\n        }\n    }\n\n    //TODO tip 5：基于单一职责原则，抽取 Jetpack ViewModel \"状态保存和恢复\" 的能力作为 StateHolder，\n    // 并使用 ObservableField 的改良版子类 State 来承担 BehaviorSubject，用作所绑定控件的 \"可信数据源\"，\n    // 从而在收到来自 PublishSubject 的结果回推后，响应结果数据的变化，也即通知控件属性重新渲染，并为其兜住最后一次状态，\n\n    //如这么说无体会，详见 https://xiaozhuanlan.com/topic/6741932805\n\n    public static class MainActivityStates extends StateHolder {\n\n        public final State<Boolean> isDrawerOpened = new State<>(false);\n\n        public final State<Boolean> openDrawer = new State<>(false);\n\n        public final State<Boolean> allowDrawerOpen = new State<>(true);\n\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/data/api/APIs.java",
    "content": "package com.kunminx.puremusic.data.api;\n\n/**\n * Create by KunMinX at 2021/6/3\n */\npublic class APIs {\n    public final static String BASE_URL = \"https://test.com/\";\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/data/api/AccountService.java",
    "content": "package com.kunminx.puremusic.data.api;\n\nimport retrofit2.Call;\nimport retrofit2.http.Field;\nimport retrofit2.http.FormUrlEncoded;\nimport retrofit2.http.POST;\n\n/**\n * Create by KunMinX at 2021/6/3\n */\npublic interface AccountService {\n\n    @POST(\"xxx/login\")\n    @FormUrlEncoded\n    Call<String> login(\n        @Field(\"username\") String username,\n        @Field(\"password\") String password\n    );\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/data/bean/DownloadState.java",
    "content": "package com.kunminx.puremusic.data.bean;\n\n/**\n * Create by KunMinX at 2022/7/15\n * <p>\n * bean，原始数据，只读，\n * Java 我们通过移除 setter\n * kotlin 直接将字段设为 val 即可\n */\npublic class DownloadState {\n    public final boolean isForgive;\n    public final int progress;\n\n    public DownloadState() {\n        this.isForgive = false;\n        this.progress = 0;\n    }\n\n    public DownloadState(boolean isForgive, int progress) {\n        this.isForgive = isForgive;\n        this.progress = progress;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/data/bean/LibraryInfo.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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\npackage com.kunminx.puremusic.data.bean;\n\n/**\n * Create by KunMinX at 19/11/2\n * <p>\n * bean，原始数据，只读，\n * Java 我们通过移除 setter\n * kotlin 直接将字段设为 val 即可\n */\npublic class LibraryInfo {\n    private final String title;\n    private final String summary;\n    private final String url;\n\n    public LibraryInfo(String title, String summary, String url) {\n        this.title = title;\n        this.summary = summary;\n        this.url = url;\n    }\n\n    public String getTitle() {\n        return title;\n    }\n\n    public String getSummary() {\n        return summary;\n    }\n\n    public String getUrl() {\n        return url;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/data/bean/TestAlbum.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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\npackage com.kunminx.puremusic.data.bean;\n\nimport com.kunminx.player.bean.base.BaseAlbumItem;\nimport com.kunminx.player.bean.base.BaseArtistItem;\nimport com.kunminx.player.bean.base.BaseMusicItem;\n\nimport java.util.List;\n\n/**\n * Create by KunMinX at 19/10/31\n * <p>\n * bean，原始数据，只读\n * Java 我们通过移除 setter\n * kotlin 直接将字段设为 val 即可\n */\npublic class TestAlbum extends BaseAlbumItem<TestAlbum.TestMusic, TestAlbum.TestArtist> {\n\n    private String albumMid;\n    public TestAlbum(String albumId, String title, String summary, TestArtist artist, String coverImg, List<TestMusic> musics) {\n        super(albumId, title, summary, artist, coverImg, musics);\n    }\n\n    public String getAlbumMid() {\n        return albumMid;\n    }\n\n    public static class TestMusic extends BaseMusicItem<TestArtist> {\n\n        private String songMid;\n        public TestMusic(String musicId, String coverImg, String url, String title, TestArtist artist) {\n            super(musicId, coverImg, url, title, artist);\n        }\n\n        public String getSongMid() {\n            return songMid;\n        }\n    }\n\n    public static class TestArtist extends BaseArtistItem {\n\n        private String birthday;\n        public TestArtist(String name) {\n            super(name);\n        }\n\n        public String getBirthday() {\n            return birthday;\n        }\n    }\n}\n\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/data/bean/User.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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\npackage com.kunminx.puremusic.data.bean;\n\n/**\n * Create by KunMinX at 20/04/26\n * <p>\n * bean，原始数据，只读\n * Java 我们通过移除 setter\n * kotlin 直接将字段设为 val 即可\n */\npublic class User {\n    private final String name;\n    private final String password;\n\n    public User(String name, String password) {\n        this.name = name;\n        this.password = password;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public String getPassword() {\n        return password;\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/data/config/Configs.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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\npackage com.kunminx.puremusic.data.config;\n\nimport com.kunminx.architecture.data.config.keyvalue.KeyValueBoolean;\nimport com.kunminx.architecture.data.config.keyvalue.KeyValueInteger;\nimport com.kunminx.architecture.data.config.keyvalue.KeyValueSerializable;\nimport com.kunminx.architecture.data.config.keyvalue.KeyValueString;\nimport com.kunminx.keyvalue.annotation.KeyValueX;\nimport com.kunminx.puremusic.data.bean.User;\n\n/**\n * TODO tip 1：消除 Android 项目 KeyValue 样板代码，让 key、value、get、put、init 缩减为一，不再 KV 爆炸。\n * 如这么说无体会，详见 https://juejin.cn/post/7121955840319291428\n * <p>\n * Create by KunMinX at 18/9/28\n */\n@KeyValueX\npublic interface Configs {\n    KeyValueString token();\n    KeyValueBoolean isLogin();\n    KeyValueInteger alive();\n    KeyValueSerializable<User> user();\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/data/config/Const.java",
    "content": "package com.kunminx.puremusic.data.config;\n\nimport android.os.Environment;\n\nimport com.kunminx.architecture.utils.Utils;\nimport com.kunminx.puremusic.R;\n/**\n * Create by KunMinX at 2022/8/18\n */\npublic class Const {\n    public static final String COVER_PATH = Utils.getApp().getExternalFilesDir(Environment.DIRECTORY_PICTURES).getAbsolutePath();\n    public static final String COLUMN_LINK = Utils.getApp().getString(R.string.article_navigation);\n    public static final String PROJECT_LINK = Utils.getApp().getString(R.string.github_project);\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/data/repository/DataRepository.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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\npackage com.kunminx.puremusic.data.repository;\n\nimport android.annotation.SuppressLint;\n\nimport com.google.gson.Gson;\nimport com.google.gson.reflect.TypeToken;\nimport com.kunminx.architecture.data.response.DataResult;\nimport com.kunminx.architecture.data.response.ResponseStatus;\nimport com.kunminx.architecture.data.response.ResultSource;\nimport com.kunminx.architecture.domain.request.AsyncTask;\nimport com.kunminx.architecture.utils.Utils;\nimport com.kunminx.puremusic.R;\nimport com.kunminx.puremusic.data.api.APIs;\nimport com.kunminx.puremusic.data.api.AccountService;\nimport com.kunminx.puremusic.data.bean.LibraryInfo;\nimport com.kunminx.puremusic.data.bean.TestAlbum;\nimport com.kunminx.puremusic.data.bean.User;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.lang.reflect.Type;\nimport java.util.List;\nimport java.util.concurrent.TimeUnit;\n\nimport io.reactivex.Observable;\nimport okhttp3.OkHttpClient;\nimport okhttp3.logging.HttpLoggingInterceptor;\nimport retrofit2.Call;\nimport retrofit2.Response;\nimport retrofit2.Retrofit;\nimport retrofit2.converter.gson.GsonConverterFactory;\n\n/**\n * Create by KunMinX at 19/10/29\n */\npublic class DataRepository {\n\n    private static final DataRepository S_REQUEST_MANAGER = new DataRepository();\n\n    private DataRepository() {\n    }\n\n    public static DataRepository getInstance() {\n        return S_REQUEST_MANAGER;\n    }\n\n    private final Retrofit retrofit;\n\n    {\n        HttpLoggingInterceptor logging = new HttpLoggingInterceptor();\n        logging.setLevel(HttpLoggingInterceptor.Level.BODY);\n        OkHttpClient client = new OkHttpClient.Builder()\n            .connectTimeout(8, TimeUnit.SECONDS)\n            .readTimeout(8, TimeUnit.SECONDS)\n            .writeTimeout(8, TimeUnit.SECONDS)\n            .addInterceptor(logging)\n            .build();\n        retrofit = new Retrofit.Builder()\n            .baseUrl(APIs.BASE_URL)\n            .client(client)\n            .addConverterFactory(GsonConverterFactory.create())\n            .build();\n    }\n\n    //TODO tip: 通过 \"响应式框架\" 往领域层回推数据，\n    // 与此相对应，kotlin 下使用 flow{ ... emit(...) }.flowOn(Dispatchers.xx)\n\n    public Observable<DataResult<TestAlbum>> getFreeMusic() {\n        return AsyncTask.doIO(emitter -> {\n            Gson gson = new Gson();\n            Type type = new TypeToken<TestAlbum>() {\n            }.getType();\n            TestAlbum testAlbum = gson.fromJson(Utils.getApp().getString(R.string.free_music_json), type);\n            emitter.onNext(new DataResult<>(testAlbum, new ResponseStatus()));\n        });\n    }\n\n    public Observable<DataResult<List<LibraryInfo>>> getLibraryInfo() {\n        return AsyncTask.doIO(emitter -> {\n            Gson gson = new Gson();\n            Type type = new TypeToken<List<LibraryInfo>>() {\n            }.getType();\n            List<LibraryInfo> list = gson.fromJson(Utils.getApp().getString(R.string.library_json), type);\n            emitter.onNext(new DataResult<>(list, new ResponseStatus()));\n        });\n    }\n\n    /**\n     * TODO：模拟下载任务:\n     */\n    @SuppressLint(\"CheckResult\")\n    public Observable<Integer> downloadFile() {\n        return AsyncTask.doIO(emitter -> {\n            //在内存中模拟 \"数据读写\"，假装是在 \"文件 IO\"，\n\n            byte[] bytes = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20};\n            try (ByteArrayInputStream bis = new ByteArrayInputStream(bytes);\n                 ByteArrayOutputStream bos = new ByteArrayOutputStream()) {\n                int b;\n                while ((b = bis.read()) != -1) {\n                    Thread.sleep(500);\n                    emitter.onNext(b);\n                }\n            } catch (IOException | InterruptedException e) {\n                e.printStackTrace();\n            }\n        });\n    }\n\n    /**\n     * TODO 模拟登录的网络请求\n     *\n     * @param user ui 层填写的用户信息\n     */\n    public Observable<DataResult<String>> login(User user) {\n\n        // 使用 retrofit 或任意你喜欢的库实现网络请求。此处以 retrofit 写个简单例子，\n        // 并且如使用 rxjava，还可额外依赖 RxJavaCallAdapterFactory 来简化编写，具体自行网上查阅，此处不做累述，\n\n        return AsyncTask.doIO(emitter -> {\n            Call<String> call = retrofit.create(AccountService.class).login(user.getName(), user.getPassword());\n            Response<String> response;\n            try {\n                response = call.execute();\n                ResponseStatus responseStatus = new ResponseStatus(\n                    String.valueOf(response.code()), response.isSuccessful(), ResultSource.NETWORK);\n                emitter.onNext(new DataResult<>(response.body(), responseStatus));\n            } catch (IOException e) {\n                emitter.onNext(new DataResult<>(null,\n                    new ResponseStatus(e.getMessage(), false, ResultSource.NETWORK)));\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/domain/event/DownloadEvent.java",
    "content": "package com.kunminx.puremusic.domain.event;\n\nimport com.kunminx.puremusic.data.bean.DownloadState;\n\n/**\n * Create by KunMinX at 2022/7/4\n */\npublic class DownloadEvent {\n    public final static int EVENT_DOWNLOAD = 1;\n    public final static int EVENT_DOWNLOAD_GLOBAL = 2;\n\n    public final int eventId;\n    public final DownloadState downloadState;\n\n    public DownloadEvent(int eventId) {\n        this.eventId = eventId;\n        this.downloadState = new DownloadState();\n    }\n\n    public DownloadEvent(int eventId, DownloadState downloadState) {\n        this.eventId = eventId;\n        this.downloadState = downloadState;\n    }\n\n    public DownloadEvent copy(DownloadState downloadState) {\n        return new DownloadEvent(this.eventId, downloadState);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/domain/event/Messages.java",
    "content": "package com.kunminx.puremusic.domain.event;\n\n/**\n * Create by KunMinX at 2022/7/4\n */\npublic class Messages {\n    public final static int EVENT_CLOSE_SLIDE_PANEL_IF_EXPANDED = 1;\n    public final static int EVENT_CLOSE_ACTIVITY_IF_ALLOWED = 2;\n    public final static int EVENT_OPEN_DRAWER = 3;\n    public final static int EVENT_ADD_SLIDE_LISTENER = 4;\n    public final static int EVENT_LOGIN_SUCCESS = 5;\n\n    public final int eventId;\n\n    public Messages(int eventId) {\n        this.eventId = eventId;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/domain/message/DrawerCoordinateManager.java",
    "content": "/*\n *\n *  * Copyright 2018-present KunMinX\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 */\n\npackage com.kunminx.puremusic.domain.message;\n\nimport androidx.annotation.NonNull;\nimport androidx.lifecycle.DefaultLifecycleObserver;\nimport androidx.lifecycle.LifecycleOwner;\n\nimport com.kunminx.architecture.domain.message.MutableResult;\nimport com.kunminx.architecture.domain.message.Result;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * TODO tip 1：通过 Lifecycle 来实现 \"抽屉侧滑禁用与否的判断\" 的一致，\n * <p>\n * 每个 \"需要注册和监听生命周期来判断\" 的视图控制器，无需在各自内部手动书写解绑等操作。\n * 如这么说无体会，详见《为你还原一个真实的 Jetpack Lifecycle》\n * https://xiaozhuanlan.com/topic/3684721950\n * <p>\n * TODO tip 2：与此同时，作为用于 \"跨页面通信\" 单例，本类也承担 \"可信源\" 职责，\n * 所有对 Drawer 状态协调相关的请求都交由本单例处理，并统一分发给所有订阅者页面。\n * <p>\n * 如这么说无体会，详见《吃透 LiveData 本质，享用可靠消息鉴权机制》解析。\n * https://xiaozhuanlan.com/topic/6017825943\n * <p>\n * <p>\n * Create by KunMinX at 19/11/3\n */\npublic class DrawerCoordinateManager implements DefaultLifecycleObserver {\n\n    private static final DrawerCoordinateManager S_HELPER = new DrawerCoordinateManager();\n\n    private DrawerCoordinateManager() {\n    }\n\n    public static DrawerCoordinateManager getInstance() {\n        return S_HELPER;\n    }\n\n    private final List<String> tagOfSecondaryPages = new ArrayList<>();\n\n    private boolean isNoneSecondaryPage() {\n        return tagOfSecondaryPages.size() == 0;\n    }\n\n    private final MutableResult<Boolean> enableSwipeDrawer = new MutableResult<>();\n\n    public Result<Boolean> isEnableSwipeDrawer() {\n        return enableSwipeDrawer;\n    }\n\n    public void requestToUpdateDrawerMode(boolean pageOpened, String pageName) {\n        if (pageOpened) {\n            tagOfSecondaryPages.add(pageName);\n        } else {\n            tagOfSecondaryPages.remove(pageName);\n        }\n        enableSwipeDrawer.setValue(isNoneSecondaryPage());\n    }\n\n    //TODO tip 3：让 NetworkStateManager 可观察页面生命周期，\n    // 从而在进入或离开目标页面时，自动在此登记和处理抽屉的禁用和解禁，避免一系列不可预期问题。\n\n    // 关于 Lifecycle 组件的存在意义，可详见《为你还原一个真实的 Jetpack Lifecycle》解析\n    // https://xiaozhuanlan.com/topic/3684721950\n\n    @Override\n    public void onCreate(@NonNull LifecycleOwner owner) {\n\n        tagOfSecondaryPages.add(owner.getClass().getSimpleName());\n\n        enableSwipeDrawer.setValue(isNoneSecondaryPage());\n\n    }\n\n    @Override\n    public void onDestroy(@NonNull LifecycleOwner owner) {\n\n        tagOfSecondaryPages.remove(owner.getClass().getSimpleName());\n\n        enableSwipeDrawer.setValue(isNoneSecondaryPage());\n\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/domain/message/PageMessenger.java",
    "content": "package com.kunminx.puremusic.domain.message;\n\nimport com.kunminx.architecture.domain.dispatch.MviDispatcher;\nimport com.kunminx.puremusic.domain.event.Messages;\n\n/**\n * TODO:Note 2022.07.04\n *  `\n *  PageMessenger 是一个领域层组件，可用于 \"跨页面通信\" 场景，\n *  比如跳转到 login 页面完成登录后，login 页面反过来通知其他页面刷新状态，\n * <p>\n * PageMessenger 基于 MVI-Dispatcher 实现可靠的消息回推，\n * 通过消息队列、引用计数等设计，确保 \"消息都能被消费，且只消费一次\"，\n * 通过内聚设计，彻底杜绝 mutable 滥用等问题，\n * <p>\n * 鉴于本项目场景难发挥 MVI-Dispatcher 潜能，故目前仅以改造 DownloadRequester 和 SharedViewModel 为例，\n * 通过对比 SharedViewModel 和 PageMessenger 易得，后者可简洁优雅实现可靠一致的消息分发，\n * <p>\n * <p>\n * 具体可参见专为 MVI-Dispatcher 编写的领域层案例：\n * <p>\n * https://github.com/KunMinX/MVI-Dispatcher\n * <p>\n * Create by KunMinX at 2022/7/4\n */\npublic class PageMessenger extends MviDispatcher<Messages> {\n    @Override\n    protected void onHandle(Messages event) {\n        sendResult(event);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/domain/message/PlayerReceiver.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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\npackage com.kunminx.puremusic.domain.message;\n\nimport android.content.BroadcastReceiver;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.view.KeyEvent;\n\nimport com.kunminx.puremusic.domain.proxy.PlayerManager;\nimport com.kunminx.puremusic.ui.widget.PlayerService;\n\nimport java.util.Objects;\n\npublic class PlayerReceiver extends BroadcastReceiver {\n\n    @Override\n    public void onReceive(Context context, Intent intent) {\n\n        if (Objects.equals(intent.getAction(), Intent.ACTION_MEDIA_BUTTON)) {\n            if (intent.getExtras() == null) {\n                return;\n            }\n            KeyEvent keyEvent = (KeyEvent) intent.getExtras().get(Intent.EXTRA_KEY_EVENT);\n            if (keyEvent == null) {\n                return;\n            }\n            if (keyEvent.getAction() != KeyEvent.ACTION_DOWN) {\n                return;\n            }\n\n            switch (keyEvent.getKeyCode()) {\n                case KeyEvent.KEYCODE_HEADSETHOOK:\n                case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:\n                    PlayerManager.getInstance().togglePlay();\n                    break;\n                case KeyEvent.KEYCODE_MEDIA_PLAY:\n                    PlayerManager.getInstance().playAudio();\n                    break;\n                case KeyEvent.KEYCODE_MEDIA_PAUSE:\n                    PlayerManager.getInstance().pauseAudio();\n                    break;\n                case KeyEvent.KEYCODE_MEDIA_STOP:\n                    PlayerManager.getInstance().clear();\n                    break;\n                case KeyEvent.KEYCODE_MEDIA_NEXT:\n                    PlayerManager.getInstance().playNext();\n                    break;\n                case KeyEvent.KEYCODE_MEDIA_PREVIOUS:\n                    PlayerManager.getInstance().playPrevious();\n                    break;\n                default:\n            }\n\n        } else {\n\n            if (Objects.requireNonNull(intent.getAction()).equals(PlayerService.NOTIFY_PLAY)) {\n                PlayerManager.getInstance().playAudio();\n            } else if (intent.getAction().equals(PlayerService.NOTIFY_PAUSE)\n                || intent.getAction().equals(android.media.AudioManager.ACTION_AUDIO_BECOMING_NOISY)) {\n                PlayerManager.getInstance().pauseAudio();\n            } else if (intent.getAction().equals(PlayerService.NOTIFY_NEXT)) {\n                PlayerManager.getInstance().playNext();\n            } else if (intent.getAction().equals(PlayerService.NOTIFY_CLOSE)) {\n                PlayerManager.getInstance().clear();\n            } else if (intent.getAction().equals(PlayerService.NOTIFY_PREVIOUS)) {\n                PlayerManager.getInstance().playPrevious();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/domain/message/SharedViewModel.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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\npackage com.kunminx.puremusic.domain.message;\n\nimport androidx.lifecycle.ViewModel;\n\nimport com.kunminx.architecture.domain.message.MutableResult;\nimport com.kunminx.architecture.domain.message.Result;\n\n/**\n * TODO tip：本类专用于跨页面通信，\n * 本类已被 PageMessenger 类代替，具体可参见 PageMessenger 类说明\n * <p>\n * Create by KunMinX at 19/10/16\n */\n@Deprecated\npublic class SharedViewModel extends ViewModel {\n\n    //TODO tip 2：此处演示 UnPeekLiveData 配合 SharedViewModel 实现 \"生命周期安全、可靠一致\" 消息分发。\n\n    //TODO tip 3：为便于理解，原 UnPeekLiveData 已改名为 MutableResult；\n    // ProtectedUnPeekLiveData 改名 Result；\n\n    private final MutableResult<Boolean> toCloseSlidePanelIfExpanded = new MutableResult<>();\n\n    private final MutableResult<Boolean> toCloseActivityIfAllowed = new MutableResult<>();\n\n    private final MutableResult<Boolean> toOpenOrCloseDrawer = new MutableResult<>();\n\n    //TODO tip 4：可通过构造器方式配置 MutableResult\n\n    private final MutableResult<Boolean> toAddSlideListener =\n        new MutableResult.Builder<Boolean>().setAllowNullValue(false).create();\n\n    public Result<Boolean> isToAddSlideListener() {\n        return toAddSlideListener;\n    }\n\n    public Result<Boolean> isToCloseSlidePanelIfExpanded() {\n        return toCloseSlidePanelIfExpanded;\n    }\n\n    public Result<Boolean> isToCloseActivityIfAllowed() {\n        return toCloseActivityIfAllowed;\n    }\n\n    public Result<Boolean> isToOpenOrCloseDrawer() {\n        return toOpenOrCloseDrawer;\n    }\n\n    public void requestToCloseActivityIfAllowed(boolean allow) {\n        toCloseActivityIfAllowed.setValue(allow);\n    }\n\n    public void requestToOpenOrCloseDrawer(boolean open) {\n        toOpenOrCloseDrawer.setValue(open);\n    }\n\n    public void requestToCloseSlidePanelIfExpanded(boolean close) {\n        toCloseSlidePanelIfExpanded.setValue(close);\n    }\n\n    public void requestToAddSlideListener(boolean add) {\n        toAddSlideListener.setValue(add);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/domain/proxy/PlayerManager.java",
    "content": "/*\n * Copyright 2018-2019 KunMinX\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\npackage com.kunminx.puremusic.domain.proxy;\n\nimport android.content.Context;\nimport android.content.Intent;\n\nimport androidx.lifecycle.LiveData;\n\nimport com.danikula.videocache.HttpProxyCacheServer;\nimport com.kunminx.player.contract.ICacheProxy;\nimport com.kunminx.player.contract.IPlayController;\nimport com.kunminx.player.contract.IServiceNotifier;\nimport com.kunminx.player.domain.MusicDTO;\nimport com.kunminx.player.domain.PlayerController;\nimport com.kunminx.player.domain.PlayingInfoManager;\nimport com.kunminx.puremusic.data.bean.TestAlbum;\nimport com.kunminx.puremusic.ui.widget.PlayerService;\n\nimport net.steamcrafted.materialiconlib.MaterialDrawableBuilder;\n\nimport java.util.List;\n\n/**\n * Create by KunMinX at 19/10/31\n */\npublic class PlayerManager implements IPlayController<TestAlbum, TestAlbum.TestMusic, TestAlbum.TestArtist> {\n\n    private static final PlayerManager sManager = new PlayerManager();\n\n    private final PlayerController<TestAlbum, TestAlbum.TestMusic, TestAlbum.TestArtist> mController;\n\n    private PlayerManager() {\n        mController = new PlayerController<>();\n    }\n\n    public static PlayerManager getInstance() {\n        return sManager;\n    }\n\n    private boolean mIsInit;\n\n    public void init(Context context) {\n        if (!mIsInit) {\n            init(context, null, null);\n            mIsInit = true;\n        }\n    }\n\n    @Override\n    public void init(Context context, IServiceNotifier iServiceNotifier, ICacheProxy iCacheProxy) {\n        Context context1 = context.getApplicationContext();\n\n        HttpProxyCacheServer proxy = new HttpProxyCacheServer.Builder(context1)\n            .fileNameGenerator(url -> {\n                String[] split = url.split(\"/\");\n                return split[split.length - 1];\n            })\n            .maxCacheSize(2147483648L)\n            .build();\n\n        mController.init(context1, startOrStop -> {\n            Intent intent = new Intent(context1, PlayerService.class);\n            if (startOrStop) context1.startService(intent);\n            else context1.stopService(intent);\n        }, proxy::getProxyUrl);\n    }\n\n    @Override\n    public void loadAlbum(TestAlbum musicAlbum) {\n        TestAlbum album = mController.getAlbum();\n        if (album == null || !album.albumId.equals(musicAlbum.albumId)) {\n            mController.loadAlbum(musicAlbum);\n        }\n    }\n\n    @Override\n    public void loadAlbum(TestAlbum musicAlbum, int playIndex) {\n        mController.loadAlbum(musicAlbum, playIndex);\n    }\n\n    @Override\n    public void playAudio() {\n        mController.playAudio();\n    }\n\n    @Override\n    public void playAudio(int albumIndex) {\n        mController.playAudio(albumIndex);\n    }\n\n    @Override\n    public void playNext() {\n        mController.playNext();\n    }\n\n    @Override\n    public void playPrevious() {\n        mController.playPrevious();\n    }\n\n    @Override\n    public void playAgain() {\n        mController.playAgain();\n    }\n\n    @Override\n    public void pauseAudio() {\n        mController.pauseAudio();\n    }\n\n    @Override\n    public void resumeAudio() {\n        mController.resumeAudio();\n    }\n\n    @Override\n    public void clear() {\n        mController.clear();\n    }\n\n    @Override\n    public void changeMode() {\n        mController.changeMode();\n    }\n\n    @Override\n    public boolean isPlaying() {\n        return mController.isPlaying();\n    }\n\n    @Override\n    public boolean isPaused() {\n        return mController.isPaused();\n    }\n\n    @Override\n    public boolean isInit() {\n        return mController.isInit();\n    }\n\n    @Override\n    public void setSeek(int progress) {\n        mController.setSeek(progress);\n    }\n\n    @Override\n    public String getTrackTime(int progress) {\n        return mController.getTrackTime(progress);\n    }\n\n    @Override\n    public LiveData<MusicDTO<TestAlbum, TestAlbum.TestMusic, TestAlbum.TestArtist>> getUiStates() {\n        return mController.getUiStates();\n    }\n\n    @Override\n    public TestAlbum getAlbum() {\n        return mController.getAlbum();\n    }\n\n    @Override\n    public List<TestAlbum.TestMusic> getAlbumMusics() {\n        return mController.getAlbumMusics();\n    }\n\n    @Override\n    public void setChangingPlayingMusic(boolean changingPlayingMusic) {\n        mController.setChangingPlayingMusic(changingPlayingMusic);\n    }\n\n    @Override\n    public int getAlbumIndex() {\n        return mController.getAlbumIndex();\n    }\n\n    @Override\n    public Enum<PlayingInfoManager.RepeatMode> getRepeatMode() {\n        return mController.getRepeatMode();\n    }\n\n    @Override\n    public void togglePlay() {\n        mController.togglePlay();\n    }\n\n    @Override\n    public TestAlbum.TestMusic getCurrentPlayingMusic() {\n        return mController.getCurrentPlayingMusic();\n    }\n\n    public MaterialDrawableBuilder.IconValue getModeIcon(Enum<PlayingInfoManager.RepeatMode> mode) {\n        if (mode == PlayingInfoManager.RepeatMode.LIST_CYCLE) {\n            return MaterialDrawableBuilder.IconValue.REPEAT;\n        } else if (mode == PlayingInfoManager.RepeatMode.SINGLE_CYCLE) {\n            return MaterialDrawableBuilder.IconValue.REPEAT_ONCE;\n        } else {\n            return MaterialDrawableBuilder.IconValue.SHUFFLE;\n        }\n    }\n\n    public MaterialDrawableBuilder.IconValue getModeIcon() {\n        return getModeIcon(getRepeatMode());\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/domain/request/AccountRequester.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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\npackage com.kunminx.puremusic.domain.request;\n\nimport androidx.annotation.NonNull;\nimport androidx.lifecycle.DefaultLifecycleObserver;\nimport androidx.lifecycle.LifecycleOwner;\n\nimport com.kunminx.architecture.data.response.DataResult;\nimport com.kunminx.architecture.data.response.ResponseStatus;\nimport com.kunminx.architecture.data.response.ResultSource;\nimport com.kunminx.architecture.domain.message.MutableResult;\nimport com.kunminx.architecture.domain.message.Result;\nimport com.kunminx.architecture.domain.request.Requester;\nimport com.kunminx.puremusic.data.bean.User;\nimport com.kunminx.puremusic.data.repository.DataRepository;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport io.reactivex.Observer;\nimport io.reactivex.disposables.Disposable;\n\n/**\n * 用户账户 Request\n * <p>\n * TODO tip 1：让 UI 和业务分离，让数据总是从生产者流向消费者\n * <p>\n * UI逻辑和业务逻辑，本质区别在于，前者是数据的消费者，后者是数据的生产者，\n * \"领域层组件\" 作为数据的生产者，职责应仅限于 \"请求调度 和 结果分发\"，\n * <p>\n * 换言之，\"领域层组件\" 中应当只关注数据的生成，而不关注数据的使用，\n * 改变 UI 状态的逻辑代码，只应在表现层页面中编写、在 Observer 回调中响应数据的变化，\n * 将来升级到 Jetpack Compose 更是如此，\n * <p>\n * Activity {\n * onCreate(){\n * vm.livedata.observe { result->\n * panel.visible(result.show ? VISIBLE : GONE)\n * tvTitle.setText(result.title)\n * tvContent.setText(result.content)\n * }\n * }\n * <p>\n * 如这么说无体会，详见《Jetpack MVVM 分层设计》解析\n * https://xiaozhuanlan.com/topic/6741932805\n * <p>\n * <p>\n * Create by KunMinX at 20/04/26\n */\npublic class AccountRequester extends Requester implements DefaultLifecycleObserver {\n\n    //TODO tip 3：👆👆👆 让 accountRequest 可观察页面生命周期，\n    // 从而在页面即将退出、且登录请求由于网络延迟尚未完成时，\n    // 及时通知数据层取消本次请求，以避免资源浪费和一系列不可预期问题。\n\n    private final MutableResult<DataResult<String>> tokenResult = new MutableResult<>();\n\n    //TODO tip 4：应顺应 \"响应式编程\"，做好 \"单向数据流\" 开发，\n    // MutableResult 应仅限 \"鉴权中心\" 内部使用，且只暴露 immutable Result 给 UI 层，\n    // 通过 \"读写分离\" 实现数据从 \"领域层\" 到 \"表现层\" 的单向流动，\n\n    //如这么说无体会，详见《吃透 LiveData 本质，享用可靠消息鉴权机制》解析。\n    //https://xiaozhuanlan.com/topic/6017825943\n\n    public Result<DataResult<String>> getTokenResult() {\n        return tokenResult;\n    }\n\n    //TODO tip 5：模拟可取消的登录请求：\n    //\n    // 配合可观察页面生命周期的 accountRequest，\n    // 从而在页面即将退出、且登录请求由于网络延迟尚未完成时，\n    // 及时通知数据层取消本次请求，以避免资源浪费和一系列不可预期的问题。\n\n    private Disposable mDisposable;\n\n    //TODO tip 6: requester 作为数据的生产者，职责应仅限于 \"请求调度 和 结果分发\"，\n    //\n    // 换言之，此处只关注数据的生成和回推，不关注数据的使用，\n    // 改变 UI 状态的逻辑代码，只应在表现层页面中编写，例如 Jetpack Compose 的使用，\n\n    public void requestLogin(User user) {\n        DataRepository.getInstance().login(user).subscribe(new Observer<DataResult<String>>() {\n            @Override\n            public void onSubscribe(Disposable d) {\n                mDisposable = d;\n            }\n            @Override\n            public void onNext(DataResult<String> dataResult) {\n                tokenResult.postValue(dataResult);\n            }\n            @Override\n            public void onError(Throwable e) {\n                tokenResult.postValue(new DataResult<>(null,\n                    new ResponseStatus(e.getMessage(), false, ResultSource.NETWORK)));\n            }\n            @Override\n            public void onComplete() {\n                mDisposable = null;\n            }\n        });\n    }\n\n    public void cancelLogin() {\n        if (mDisposable != null && !mDisposable.isDisposed()) {\n            mDisposable.dispose();\n            mDisposable = null;\n        }\n    }\n\n    //TODO tip 7：让 accountRequest 可观察页面生命周期，\n    // 从而在页面即将退出、且登录请求由于网络延迟尚未完成时，\n    // 及时通知数据层取消本次请求，以避免资源浪费和一系列不可预期问题。\n\n    // 关于 Lifecycle 组件的存在意义，详见《为你还原一个真实的 Jetpack Lifecycle》解析\n    // https://xiaozhuanlan.com/topic/3684721950\n\n    @Override\n    public void onStop(@NonNull @NotNull LifecycleOwner owner) {\n        cancelLogin();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/domain/request/DownloadRequester.java",
    "content": "package com.kunminx.puremusic.domain.request;\n\nimport androidx.annotation.NonNull;\nimport androidx.lifecycle.LifecycleOwner;\n\nimport com.kunminx.architecture.domain.dispatch.MviDispatcher;\nimport com.kunminx.architecture.domain.request.AsyncTask;\nimport com.kunminx.puremusic.data.bean.DownloadState;\nimport com.kunminx.puremusic.data.repository.DataRepository;\nimport com.kunminx.puremusic.domain.event.DownloadEvent;\n\nimport io.reactivex.disposables.Disposable;\n\n/**\n * 数据下载 Request\n * <p>\n * TODO tip 1：让 UI 和业务分离，让数据总是从生产者流向消费者\n * <p>\n * UI逻辑和业务逻辑，本质区别在于，前者是数据的消费者，后者是数据的生产者，\n * \"领域层组件\" 作为数据的生产者，职责应仅限于 \"请求调度 和 结果分发\"，\n * <p>\n * 换言之，\"领域层组件\" 中应当只关注数据的生成，而不关注数据的使用，\n * 改变 UI 状态的逻辑代码，只应在表现层页面中编写、在 Observer 回调中响应数据的变化，\n * 将来升级到 Jetpack Compose 更是如此，\n * <p>\n * Activity {\n * onCreate(){\n * vm.livedata.observe { result->\n * panel.visible(result.show ? VISIBLE : GONE)\n * tvTitle.setText(result.title)\n * tvContent.setText(result.content)\n * }\n * }\n * <p>\n * 如这么说无体会，详见《Jetpack MVVM 分层设计》解析\n * https://xiaozhuanlan.com/topic/6741932805\n * <p>\n * <p>\n * Create by KunMinX at 20/03/18\n */\npublic class DownloadRequester extends MviDispatcher<DownloadEvent> {\n\n    private Disposable mDisposable;\n\n    //TODO Tip 2：基于 \"单一职责原则\"，宜将 Jetpack ViewModel 框架划分为 state-ViewModel 和 result-ViewModel，\n    // result-ViewModel 作为领域层组件，仅提取和继承 Jetpack ViewModel 框架中 \"作用域管理\" 的能力，\n    // 使业务实例能根据需要，被单个页面独享，或多个页面共享，例如：\n    //\n    // mDownloadRequester = getFragmentScopeViewModel(DownloadRequester.class);\n    // mGlobalDownloadRequester = getActivityScopeViewModel(DownloadRequester.class);\n    //\n    // 在本案例中，fragment 级作用域的 mDownloadRequester 只走 DownloadEvent.EVENT_DOWNLOAD 业务，\n    // Activity 级作用域的 mGlobalDownloadRequester 只走 DownloadEvent.EVENT_DOWNLOAD_GLOBAL 业务，\n    // 二者都为 SearchFragment 所持有，用于对比不同作用域的效果，\n\n    @Override\n    protected void onHandle(DownloadEvent event) {\n        DataRepository repo = DataRepository.getInstance();\n        switch (event.eventId) {\n            case DownloadEvent.EVENT_DOWNLOAD:\n                repo.downloadFile().subscribe(new AsyncTask.Observer<Integer>() {\n                    @Override\n                    public void onSubscribe(Disposable d) {\n                        mDisposable = d;\n                    }\n                    @Override\n                    public void onNext(Integer integer) {\n                        sendResult(event.copy(new DownloadState(true, integer)));\n                    }\n                });\n                break;\n            case DownloadEvent.EVENT_DOWNLOAD_GLOBAL:\n                repo.downloadFile().subscribe((AsyncTask.Observer<Integer>) integer -> {\n                    sendResult(event.copy(new DownloadState(true, integer)));\n                });\n                break;\n        }\n    }\n\n    @Override\n    public void onStop(@NonNull LifecycleOwner owner) {\n        super.onStop(owner);\n        if (mDisposable != null && !mDisposable.isDisposed()) {\n            mDisposable.dispose();\n            mDisposable = null;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/domain/request/InfoRequester.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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\npackage com.kunminx.puremusic.domain.request;\n\nimport android.annotation.SuppressLint;\n\nimport com.kunminx.architecture.data.response.DataResult;\nimport com.kunminx.architecture.domain.message.MutableResult;\nimport com.kunminx.architecture.domain.message.Result;\nimport com.kunminx.architecture.domain.request.Requester;\nimport com.kunminx.puremusic.data.bean.LibraryInfo;\nimport com.kunminx.puremusic.data.repository.DataRepository;\n\nimport java.util.List;\n\n/**\n * 信息列表 Request\n * <p>\n * TODO tip 1：让 UI 和业务分离，让数据总是从生产者流向消费者\n * <p>\n * UI逻辑和业务逻辑，本质区别在于，前者是数据的消费者，后者是数据的生产者，\n * \"领域层组件\" 作为数据的生产者，职责应仅限于 \"请求调度 和 结果分发\"，\n * <p>\n * 换言之，\"领域层组件\" 中应当只关注数据的生成，而不关注数据的使用，\n * 改变 UI 状态的逻辑代码，只应在表现层页面中编写、在 Observer 回调中响应数据的变化，\n * 将来升级到 Jetpack Compose 更是如此，\n * <p>\n * Activity {\n * onCreate(){\n * vm.livedata.observe { result->\n * panel.visible(result.show ? VISIBLE : GONE)\n * tvTitle.setText(result.title)\n * tvContent.setText(result.content)\n * }\n * }\n * <p>\n * TODO tip 2：Requester 通常按业务划分\n * 一个项目中通常可存在多个 Requester 类，\n * 每个页面可根据业务需要，持有多个不同 Requester 实例，\n * 通过 PublishSubject 回推一次性消息，并在表现层 Observer 中分流，\n * 对于 Event，直接执行，对于 State，使用 BehaviorSubject 通知 View 渲染和兜着状态，\n * <p>\n * Activity {\n * onCreate(){\n * request.observe {result ->\n * is Event ? -> execute one time\n * is State ? -> BehaviorSubject setValue and notify\n * }\n * }\n * <p>\n * 如这么说无体会，详见《Jetpack MVVM 分层设计解析》解析\n * https://xiaozhuanlan.com/topic/6741932805\n * <p>\n * <p>\n * Create by KunMinX at 19/11/2\n */\npublic class InfoRequester extends Requester {\n\n    private final MutableResult<DataResult<List<LibraryInfo>>> mLibraryResult = new MutableResult<>();\n\n    //TODO tip 4：应顺应 \"响应式编程\"，做好 \"单向数据流\" 开发，\n    // MutableResult 应仅限 \"鉴权中心\" 内部使用，且只暴露 immutable Result 给 UI 层，\n    // 通过 \"读写分离\" 实现数据从 \"领域层\" 到 \"表现层\" 的单向流动，\n\n    //如这么说无体会，详见《吃透 LiveData 本质，享用可靠消息鉴权机制》解析。\n    //https://xiaozhuanlan.com/topic/6017825943\n\n    public Result<DataResult<List<LibraryInfo>>> getLibraryResult() {\n        return mLibraryResult;\n    }\n\n    //TODO tip 5: requester 作为数据的生产者，职责应仅限于 \"请求调度 和 结果分发\"，\n    //\n    // 换言之，此处只关注数据的生成和回推，不关注数据的使用，\n    // 改变 UI 状态的逻辑代码，只应在表现层页面中编写，例如 Jetpack Compose 的使用，\n\n    @SuppressLint(\"CheckResult\")\n    public void requestLibraryInfo() {\n        if (mLibraryResult.getValue() == null)\n            DataRepository.getInstance().getLibraryInfo().subscribe(mLibraryResult::setValue);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/domain/request/MusicRequester.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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\npackage com.kunminx.puremusic.domain.request;\n\nimport android.annotation.SuppressLint;\n\nimport com.kunminx.architecture.data.response.DataResult;\nimport com.kunminx.architecture.domain.message.MutableResult;\nimport com.kunminx.architecture.domain.message.Result;\nimport com.kunminx.architecture.domain.request.Requester;\nimport com.kunminx.puremusic.data.bean.TestAlbum;\nimport com.kunminx.puremusic.data.repository.DataRepository;\n\n/**\n * 音乐资源  Request\n * <p>\n * TODO tip 1：让 UI 和业务分离，让数据总是从生产者流向消费者\n * <p>\n * UI逻辑和业务逻辑，本质区别在于，前者是数据的消费者，后者是数据的生产者，\n * \"领域层组件\" 作为数据的生产者，职责应仅限于 \"请求调度 和 结果分发\"，\n * <p>\n * 换言之，\"领域层组件\" 中应当只关注数据的生成，而不关注数据的使用，\n * 改变 UI 状态的逻辑代码，只应在表现层页面中编写、在 Observer 回调中响应数据的变化，\n * 将来升级到 Jetpack Compose 更是如此，\n * <p>\n * Activity {\n * onCreate(){\n * vm.livedata.observe { result->\n * panel.visible(result.show ? VISIBLE : GONE)\n * tvTitle.setText(result.title)\n * tvContent.setText(result.content)\n * }\n * }\n * <p>\n * TODO tip 2：Requester 通常按业务划分\n * 一个项目中通常可存在多个 Requester 类，\n * 每个页面可根据业务需要，持有多个不同 Requester 实例，\n * 通过 PublishSubject 回推一次性消息，并在表现层 Observer 中分流，\n * 对于 Event，直接执行，对于 State，使用 BehaviorSubject 通知 View 渲染和兜着状态，\n * <p>\n * Activity {\n * onCreate(){\n * request.observe {result ->\n * is Event ? -> execute one time\n * is State ? -> BehaviorSubject setValue and notify\n * }\n * }\n * <p>\n * 如这么说无体会，详见《Jetpack MVVM 分层设计解析》解析\n * https://xiaozhuanlan.com/topic/6741932805\n * <p>\n * <p>\n * Create by KunMinX at 19/10/29\n */\npublic class MusicRequester extends Requester {\n\n    private final MutableResult<DataResult<TestAlbum>> mFreeMusicsResult = new MutableResult<>();\n\n    //TODO tip 4：应顺应 \"响应式编程\"，做好 \"单向数据流\" 开发，\n    // MutableResult 应仅限 \"鉴权中心\" 内部使用，且只暴露 immutable Result 给 UI 层，\n    // 通过 \"读写分离\" 实现数据从 \"领域层\" 到 \"表现层\" 的单向流动，\n\n    //如这么说无体会，详见《吃透 LiveData 本质，享用可靠消息鉴权机制》解析。\n    //https://xiaozhuanlan.com/topic/6017825943\n\n    public Result<DataResult<TestAlbum>> getFreeMusicsResult() {\n        return mFreeMusicsResult;\n    }\n\n    //TODO tip 5: requester 作为数据的生产者，职责应仅限于 \"请求调度 和 结果分发\"，\n    //\n    // 换言之，此处只关注数据的生成和回推，不关注数据的使用，\n    // 改变 UI 状态的逻辑代码，只应在表现层页面中编写，例如 Jetpack Compose 的使用，\n\n    @SuppressLint(\"CheckResult\")\n    public void requestFreeMusics() {\n        DataRepository.getInstance().getFreeMusic().subscribe(mFreeMusicsResult::setValue);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/domain/usecase/CanBeStoppedUseCase.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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\npackage com.kunminx.puremusic.domain.usecase;\n\nimport androidx.annotation.NonNull;\nimport androidx.lifecycle.DefaultLifecycleObserver;\nimport androidx.lifecycle.LifecycleOwner;\n\nimport com.kunminx.architecture.data.response.DataResult;\nimport com.kunminx.architecture.domain.usecase.UseCase;\nimport com.kunminx.puremusic.data.bean.DownloadState;\n\n/**\n * UseCase 示例，实现 LifeCycle 接口，单独服务于 有 “叫停” 需求 的业务\n * <p>\n * TODO tip：\n * 同样是“下载”，我不是在数据层分别写两个方法，\n * 而是遵循开闭原则，在 ViewModel 和 数据层之间，插入一个 UseCase，来专门负责可叫停的情况，\n * 除了开闭原则，使用 UseCase 还有个考虑就是避免内存泄漏，\n * 具体缘由可详见 https://xiaozhuanlan.com/topic/6257931840 评论区 15 楼\n * 以及《这是一份 “架构模式” 自驾攻略》的解析\n * https://xiaozhuanlan.com/topic/8204519736\n * <p>\n * <p>\n * 现已更换为在 MVI-Dispatcher 中处理，具体可参见 DownloadRequest 实现\n * <p>\n * <p>\n * Create by KunMinX at 19/11/25\n */\n@Deprecated\npublic class CanBeStoppedUseCase extends UseCase<CanBeStoppedUseCase.RequestValues,\n    CanBeStoppedUseCase.ResponseValue> implements DefaultLifecycleObserver {\n\n//    private final DownloadState downloadState = new DownloadState();\n\n    //TODO tip：让 CanBeStoppedUseCase 可观察页面生命周期，\n    // 从而在页面即将退出、且下载请求尚未完成时，\n    // 及时通知数据层取消本次请求，以避免资源浪费和一系列不可预期的问题。\n\n    // 关于 Lifecycle 组件的存在意义，可详见《为你还原一个真实的 Jetpack Lifecycle》篇的解析\n    // https://xiaozhuanlan.com/topic/3684721950\n\n    @Override\n    public void onStop(@NonNull LifecycleOwner owner) {\n        if (getRequestValues() != null) {\n//            downloadState.isForgive = true;\n//            downloadState.file = null;\n//            downloadState.progress = 0;\n//            getUseCaseCallback().onError();\n        }\n    }\n\n    @Override\n    protected void executeUseCase(RequestValues requestValues) {\n\n        //访问数据层资源，在 UseCase 中处理带叫停性质的业务\n\n//        DataRepository.getInstance().downloadFile(downloadState, dataResult -> {\n//            getUseCaseCallback().onSuccess(new ResponseValue(dataResult));\n//        });\n    }\n\n    public static final class RequestValues implements UseCase.RequestValues {\n\n    }\n\n    public static final class ResponseValue implements UseCase.ResponseValue {\n\n        private final DataResult<DownloadState> mDataResult;\n\n        public ResponseValue(DataResult<DownloadState> dataResult) {\n            mDataResult = dataResult;\n        }\n\n        public DataResult<DownloadState> getDataResult() {\n            return mDataResult;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/domain/usecase/DownloadUseCase.java",
    "content": "package com.kunminx.puremusic.domain.usecase;\n\nimport com.kunminx.architecture.domain.usecase.UseCase;\nimport com.kunminx.puremusic.data.config.Const;\n\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.net.URL;\n\n/**\n * Create by KunMinX at 20/03/16\n */\npublic class DownloadUseCase extends UseCase<DownloadUseCase.RequestValues, DownloadUseCase.ResponseValue> {\n\n    @Override\n    protected void executeUseCase(RequestValues requestValues) {\n        try {\n            URL url = new URL(requestValues.url);\n            InputStream is = url.openStream();\n            File file = new File(Const.COVER_PATH, requestValues.path);\n            OutputStream os = new FileOutputStream(file);\n            byte[] buffer = new byte[1024];\n            int len = 0;\n            while ((len = is.read(buffer)) > 0) {\n                os.write(buffer, 0, len);\n            }\n            is.close();\n            os.close();\n\n            getUseCaseCallback().onSuccess(new ResponseValue(file));\n\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n    }\n\n    public static final class RequestValues implements UseCase.RequestValues {\n        private String url;\n        private String path;\n\n        public RequestValues(String url, String path) {\n            this.url = url;\n            this.path = path;\n        }\n\n        public String getUrl() {\n            return url;\n        }\n\n        public void setUrl(String url) {\n            this.url = url;\n        }\n\n        public String getPath() {\n            return path;\n        }\n\n        public void setPath(String path) {\n            this.path = path;\n        }\n    }\n\n    public static final class ResponseValue implements UseCase.ResponseValue {\n        private File mFile;\n\n        public ResponseValue(File file) {\n            mFile = file;\n        }\n\n        public File getFile() {\n            return mFile;\n        }\n\n        public void setFile(File file) {\n            mFile = file;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/bind/CommonBindingAdapter.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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\npackage com.kunminx.puremusic.ui.bind;\n\nimport android.graphics.drawable.Drawable;\nimport android.util.Pair;\nimport android.view.View;\nimport android.widget.ImageView;\nimport android.widget.TextView;\n\nimport androidx.coordinatorlayout.widget.CoordinatorLayout;\nimport androidx.databinding.BindingAdapter;\n\nimport com.bumptech.glide.Glide;\nimport com.kunminx.architecture.utils.ClickUtils;\n\n/**\n * Create by KunMinX at 19/9/18\n */\npublic class CommonBindingAdapter {\n\n    @BindingAdapter(value = {\"imageUrl\", \"placeHolder\"}, requireAll = false)\n    public static void imageUrl(ImageView view, String url, Drawable placeHolder) {\n        Glide.with(view.getContext()).load(url).placeholder(placeHolder).into(view);\n    }\n\n    @BindingAdapter(value = {\"visible\"}, requireAll = false)\n    public static void visible(View view, boolean visible) {\n        if (visible && view.getVisibility() == View.GONE) {\n            view.setVisibility(View.VISIBLE);\n        } else if (!visible && view.getVisibility() == View.VISIBLE) {\n            view.setVisibility(View.GONE);\n        }\n    }\n\n    @BindingAdapter(value = {\"invisible\"}, requireAll = false)\n    public static void invisible(View view, boolean visible) {\n        if (visible && view.getVisibility() == View.INVISIBLE) {\n            view.setVisibility(View.VISIBLE);\n        } else if (!visible && view.getVisibility() == View.VISIBLE) {\n            view.setVisibility(View.INVISIBLE);\n        }\n    }\n\n    @BindingAdapter(value = {\"size\"}, requireAll = false)\n    public static void size(View view, Pair<Integer, Integer> size) {\n        CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) view.getLayoutParams();\n        params.width = size.first;\n        params.height = size.second;\n        view.setLayoutParams(params);\n    }\n\n    @BindingAdapter(value = {\"transX\"}, requireAll = false)\n    public static void translationX(View view, float translationX) {\n        view.setTranslationX(translationX);\n    }\n\n    @BindingAdapter(value = {\"transY\"}, requireAll = false)\n    public static void translationY(View view, float translationY) {\n        view.setTranslationY(translationY);\n    }\n\n    @BindingAdapter(value = {\"x\"}, requireAll = false)\n    public static void x(View view, float x) {\n        view.setX(x);\n    }\n\n    @BindingAdapter(value = {\"y\"}, requireAll = false)\n    public static void y(View view, float y) {\n        view.setY(y);\n    }\n\n    @BindingAdapter(value = {\"alpha\"}, requireAll = false)\n    public static void alpha(View view, float alpha) {\n        view.setAlpha(alpha);\n    }\n\n    @BindingAdapter(value = {\"textColor\"}, requireAll = false)\n    public static void setTextColor(TextView textView, int textColorRes) {\n        textView.setTextColor(textView.getContext().getColor(textColorRes));\n    }\n\n    @BindingAdapter(value = {\"selected\"}, requireAll = false)\n    public static void selected(View view, boolean select) {\n        view.setSelected(select);\n    }\n\n    @BindingAdapter(value = {\"onClickWithDebouncing\"}, requireAll = false)\n    public static void onClickWithDebouncing(View view, View.OnClickListener clickListener) {\n        ClickUtils.applySingleDebouncing(view, clickListener);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/bind/DrawerBindingAdapter.java",
    "content": "package com.kunminx.puremusic.ui.bind;\n\nimport androidx.core.view.GravityCompat;\nimport androidx.databinding.BindingAdapter;\nimport androidx.drawerlayout.widget.DrawerLayout;\n\n/**\n * Create by KunMinX at 2020/3/13\n */\npublic class DrawerBindingAdapter {\n\n    @BindingAdapter(value = {\"isOpenDrawer\"}, requireAll = false)\n    public static void openDrawer(DrawerLayout drawerLayout, boolean isOpenDrawer) {\n        if (isOpenDrawer && !drawerLayout.isDrawerOpen(GravityCompat.START)) {\n            drawerLayout.openDrawer(GravityCompat.START);\n        } else {\n            drawerLayout.closeDrawer(GravityCompat.START);\n        }\n    }\n\n    @BindingAdapter(value = {\"allowDrawerOpen\"}, requireAll = false)\n    public static void allowDrawerOpen(DrawerLayout drawerLayout, boolean allowDrawerOpen) {\n        drawerLayout.setDrawerLockMode(allowDrawerOpen\n            ? DrawerLayout.LOCK_MODE_UNLOCKED\n            : DrawerLayout.LOCK_MODE_LOCKED_CLOSED);\n    }\n\n    @BindingAdapter(value = {\"bindDrawerListener\"}, requireAll = false)\n    public static void listenDrawerState(DrawerLayout drawerLayout, DrawerLayout.SimpleDrawerListener listener) {\n        drawerLayout.addDrawerListener(listener);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/bind/IconBindingAdapter.java",
    "content": "package com.kunminx.puremusic.ui.bind;\n\nimport androidx.databinding.BindingAdapter;\n\nimport com.kunminx.puremusic.ui.view.PlayPauseView;\n\nimport net.steamcrafted.materialiconlib.MaterialDrawableBuilder;\nimport net.steamcrafted.materialiconlib.MaterialIconView;\n\n/**\n * Create by KunMinX at 2020/3/13\n */\npublic class IconBindingAdapter {\n\n    @BindingAdapter(value = {\"isPlaying\"}, requireAll = false)\n    public static void isPlaying(PlayPauseView pauseView, boolean isPlaying) {\n        if (isPlaying) {\n            pauseView.play();\n        } else {\n            pauseView.pause();\n        }\n    }\n\n    @BindingAdapter(value = {\"mdIcon\"}, requireAll = false)\n    public static void setIcon(MaterialIconView view, MaterialDrawableBuilder.IconValue iconValue) {\n        view.setIcon(iconValue);\n    }\n\n    @BindingAdapter(value = {\"circleAlpha\"}, requireAll = false)\n    public static void circleAlpha(PlayPauseView pauseView, int circleAlpha) {\n        pauseView.setCircleAlpha(circleAlpha);\n    }\n\n    @BindingAdapter(value = {\"drawableColor\"}, requireAll = false)\n    public static void drawableColor(PlayPauseView pauseView, int drawableColor) {\n        pauseView.setDrawableColor(drawableColor);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/bind/TabPageBindingAdapter.java",
    "content": "package com.kunminx.puremusic.ui.bind;\n\nimport androidx.databinding.BindingAdapter;\nimport androidx.viewpager.widget.ViewPager;\n\nimport com.google.android.material.tabs.TabLayout;\nimport com.kunminx.architecture.ui.adapter.CommonViewPagerAdapter;\nimport com.kunminx.puremusic.R;\n\n/**\n * Create by KunMinX at 2020/3/13\n */\npublic class TabPageBindingAdapter {\n\n    @BindingAdapter(value = {\"initTabAndPage\"}, requireAll = false)\n    public static void initTabAndPage(ViewPager viewPager, boolean initTabAndPage) {\n        TabLayout tabLayout = (viewPager.getRootView()).findViewById(R.id.tab_layout);\n        int count = tabLayout.getTabCount();\n        String[] title = new String[count];\n        for (int i = 0; i < count; i++) {\n            TabLayout.Tab tab = tabLayout.getTabAt(i);\n            if (tab != null && tab.getText() != null) {\n                title[i] = tab.getText().toString();\n            }\n        }\n        viewPager.setAdapter(new CommonViewPagerAdapter(false, title));\n        tabLayout.setupWithViewPager(viewPager);\n    }\n\n    @BindingAdapter(value = {\"tabSelectedListener\"}, requireAll = false)\n    public static void tabSelectedListener(TabLayout tabLayout, TabLayout.OnTabSelectedListener listener) {\n        tabLayout.addOnTabSelectedListener(listener);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/bind/WebViewBindingAdapter.java",
    "content": "package com.kunminx.puremusic.ui.bind;\n\nimport android.annotation.SuppressLint;\nimport android.content.Intent;\nimport android.net.Uri;\nimport android.view.View;\nimport android.webkit.WebResourceRequest;\nimport android.webkit.WebSettings;\nimport android.webkit.WebView;\nimport android.webkit.WebViewClient;\n\nimport androidx.databinding.BindingAdapter;\n\nimport com.kunminx.architecture.utils.Utils;\n\n/**\n * Create by KunMinX at 2020/3/13\n */\npublic class WebViewBindingAdapter {\n\n    @SuppressLint(\"SetJavaScriptEnabled\")\n    @BindingAdapter(value = {\"pageAssetPath\"}, requireAll = false)\n    public static void loadAssetsPage(WebView webView, String assetPath) {\n        webView.setWebViewClient(new WebViewClient() {\n            @Override\n            public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {\n                Uri uri = request.getUrl();\n                Intent intent = new Intent(Intent.ACTION_VIEW, uri);\n                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n                Utils.getApp().startActivity(intent);\n                return true;\n            }\n        });\n        webView.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);\n        WebSettings webSettings = webView.getSettings();\n        webSettings.setJavaScriptEnabled(true);\n        webSettings.setDefaultTextEncodingName(\"UTF-8\");\n        webSettings.setSupportZoom(true);\n        webSettings.setBuiltInZoomControls(true);\n        webSettings.setDisplayZoomControls(false);\n        webSettings.setUseWideViewPort(true);\n        webSettings.setLoadWithOverviewMode(true);\n        String url = \"file:///android_asset/\" + assetPath;\n        webView.loadUrl(url);\n    }\n\n    @SuppressLint(\"SetJavaScriptEnabled\")\n    @BindingAdapter(value = {\"loadPage\"}, requireAll = false)\n    public static void loadPage(WebView webView, String loadPage) {\n        webView.setWebViewClient(new WebViewClient());\n        webView.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);\n        WebSettings webSettings = webView.getSettings();\n        webSettings.setJavaScriptEnabled(true);\n        webSettings.setDefaultTextEncodingName(\"UTF-8\");\n        webSettings.setSupportZoom(true);\n        webSettings.setBuiltInZoomControls(true);\n        webSettings.setDisplayZoomControls(false);\n        webSettings.setUseWideViewPort(true);\n        webSettings.setLoadWithOverviewMode(true);\n        webView.loadUrl(loadPage);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/page/DrawerFragment.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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\npackage com.kunminx.puremusic.ui.page;\n\nimport android.os.Bundle;\nimport android.view.View;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.kunminx.architecture.ui.page.BaseFragment;\nimport com.kunminx.architecture.ui.page.DataBindingConfig;\nimport com.kunminx.architecture.ui.page.StateHolder;\nimport com.kunminx.architecture.ui.state.State;\nimport com.kunminx.puremusic.BR;\nimport com.kunminx.puremusic.R;\nimport com.kunminx.puremusic.data.bean.LibraryInfo;\nimport com.kunminx.puremusic.data.config.Const;\nimport com.kunminx.puremusic.domain.request.InfoRequester;\nimport com.kunminx.puremusic.ui.page.adapter.DrawerAdapter;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Create by KunMinX at 19/10/29\n */\npublic class DrawerFragment extends BaseFragment {\n\n    //TODO tip 1：基于 \"单一职责原则\"，应将 ViewModel 划分为 state-ViewModel 和 result-ViewModel，\n    // state-ViewModel 职责仅限于托管、保存和恢复本页面 state，作用域仅限于本页面，\n    // result-ViewModel 职责仅限于 \"消息分发\" 场景承担 \"可信源\"，作用域依 \"数据请求\" 或 \"跨页通信\" 消息分发范围而定\n\n    // 如这么说无体会，详见 https://xiaozhuanlan.com/topic/8204519736\n\n    private DrawerStates mStates;\n    private InfoRequester mInfoRequester;\n\n    @Override\n    protected void initViewModel() {\n        mStates = getFragmentScopeViewModel(DrawerStates.class);\n        mInfoRequester = getFragmentScopeViewModel(InfoRequester.class);\n    }\n\n    @Override\n    protected DataBindingConfig getDataBindingConfig() {\n\n        //TODO tip 2: DataBinding 严格模式：\n        // 将 DataBinding 实例限制于 base 页面中，默认不向子类暴露，\n        // 通过这方式，彻底解决 View 实例 Null 安全一致性问题，\n        // 如此，View 实例 Null 安全性将和基于函数式编程思想的 Jetpack Compose 持平。\n        // 而 DataBindingConfig 就是在这样背景下，用于为 base 页面 DataBinding 提供绑定项。\n\n        // 如这么说无体会，详见 https://xiaozhuanlan.com/topic/9816742350 和 https://xiaozhuanlan.com/topic/2356748910\n\n        return new DataBindingConfig(R.layout.fragment_drawer, BR.vm, mStates)\n            .addBindingParam(BR.click, new ClickProxy())\n            .addBindingParam(BR.adapter, new DrawerAdapter(getContext()));\n    }\n\n    @Override\n    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {\n        super.onViewCreated(view, savedInstanceState);\n\n        //TODO tip 3: 从 PublishSubject 接收回推的数据，并在回调中响应数据的变化，\n        // 也即通过 BehaviorSubject（例如 ObservableField）通知控件属性重新渲染，并为其兜住最后一次状态，\n\n        //如这么说无体会，详见 https://xiaozhuanlan.com/topic/6741932805\n\n        mInfoRequester.getLibraryResult().observe(getViewLifecycleOwner(), dataResult -> {\n            if (!dataResult.getResponseStatus().isSuccess()) return;\n            if (dataResult.getResult() != null) mStates.list.set(dataResult.getResult());\n        });\n\n        mInfoRequester.requestLibraryInfo();\n    }\n\n    public class ClickProxy {\n        public void logoClick() {\n            openUrlInBrowser(Const.PROJECT_LINK);\n        }\n    }\n\n    //TODO tip 5：基于单一职责原则，抽取 Jetpack ViewModel \"状态保存和恢复\" 的能力作为 StateHolder，\n    // 并使用 ObservableField 的改良版子类 State 来承担 BehaviorSubject，用作所绑定控件的 \"可信数据源\"，\n    // 从而在收到来自 PublishSubject 的结果回推后，响应结果数据的变化，也即通知控件属性重新渲染，并为其兜住最后一次状态，\n\n    //如这么说无体会，详见 https://xiaozhuanlan.com/topic/6741932805\n\n    public static class DrawerStates extends StateHolder {\n        public final State<List<LibraryInfo>> list = new State<>(new ArrayList<>());\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/page/LoginFragment.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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\npackage com.kunminx.puremusic.ui.page;\n\nimport android.os.Bundle;\nimport android.text.TextUtils;\nimport android.view.View;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.kunminx.architecture.data.config.utils.KeyValueProvider;\nimport com.kunminx.architecture.ui.page.BaseFragment;\nimport com.kunminx.architecture.ui.page.DataBindingConfig;\nimport com.kunminx.architecture.ui.page.StateHolder;\nimport com.kunminx.architecture.ui.state.State;\nimport com.kunminx.architecture.utils.ToastUtils;\nimport com.kunminx.puremusic.BR;\nimport com.kunminx.puremusic.R;\nimport com.kunminx.puremusic.data.bean.User;\nimport com.kunminx.puremusic.data.config.Configs;\nimport com.kunminx.puremusic.domain.event.Messages;\nimport com.kunminx.puremusic.domain.message.DrawerCoordinateManager;\nimport com.kunminx.puremusic.domain.message.PageMessenger;\nimport com.kunminx.puremusic.domain.request.AccountRequester;\n\n/**\n * Create by KunMinX at 20/04/26\n */\npublic class LoginFragment extends BaseFragment {\n\n    //TODO tip 1：基于 \"单一职责原则\"，应将 ViewModel 划分为 state-ViewModel 和 result-ViewModel，\n    // state-ViewModel 职责仅限于托管、保存和恢复本页面 state，作用域仅限于本页面，\n    // result-ViewModel 职责仅限于 \"消息分发\" 场景承担 \"可信源\"，作用域依 \"数据请求\" 或 \"跨页通信\" 消息分发范围而定\n\n    // 如这么说无体会，详见 https://xiaozhuanlan.com/topic/8204519736\n\n    private LoginStates mStates;\n    private AccountRequester mAccountRequester;\n    private PageMessenger mMessenger;\n    private final Configs mConfigs = KeyValueProvider.get(Configs.class);\n\n    @Override\n    protected void initViewModel() {\n        mStates = getFragmentScopeViewModel(LoginStates.class);\n        mMessenger = getApplicationScopeViewModel(PageMessenger.class);\n        mAccountRequester = getFragmentScopeViewModel(AccountRequester.class);\n    }\n\n    @Override\n    protected DataBindingConfig getDataBindingConfig() {\n\n        //TODO tip 2: DataBinding 严格模式：\n        // 将 DataBinding 实例限制于 base 页面中，默认不向子类暴露，\n        // 通过这方式，彻底解决 View 实例 Null 安全一致性问题，\n        // 如此，View 实例 Null 安全性将和基于函数式编程思想的 Jetpack Compose 持平。\n        // 而 DataBindingConfig 就是在这样背景下，用于为 base 页面 DataBinding 提供绑定项。\n\n        // 如这么说无体会，详见 https://xiaozhuanlan.com/topic/9816742350 和 https://xiaozhuanlan.com/topic/2356748910\n\n        return new DataBindingConfig(R.layout.fragment_login, BR.vm, mStates)\n            .addBindingParam(BR.click, new ClickProxy());\n    }\n\n    @Override\n    public void onCreate(@Nullable Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n\n        getLifecycle().addObserver(DrawerCoordinateManager.getInstance());\n\n        //TODO tip 3：让 accountRequest 可观察页面生命周期，\n        // 从而在页面即将退出、且登录请求由于网络延迟尚未完成时，\n        // 及时通知数据层取消本次请求，以避免资源浪费和一系列不可预期问题。\n\n        getLifecycle().addObserver(mAccountRequester);\n    }\n\n    @Override\n    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {\n        super.onViewCreated(view, savedInstanceState);\n\n        //TODO tip 4: 从可信源 Requester 通过 immutable Result 获取请求结果的只读数据，set 给 mutable State，\n        //而非 Result、State 不分，直接在页面 set Result，\n\n        //如这么说无体会，详见《吃透 LiveData 本质，享用可靠消息鉴权机制》解析。\n        //https://xiaozhuanlan.com/topic/6017825943\n\n        mAccountRequester.getTokenResult().observe(getViewLifecycleOwner(), dataResult -> {\n            if (!dataResult.getResponseStatus().isSuccess()) {\n                mStates.loadingVisible.set(false);\n                ToastUtils.showLongToast(getString(R.string.network_state_retry));\n                return;\n            }\n\n            String s = dataResult.getResult();\n            if (TextUtils.isEmpty(s)) return;\n\n            //TODO tip：成功获取 token 后，可通过 KeyValueX 框架存储配置，\n            // 以及通过作用域为 Application 的 PageMessenger 框架通知其他页面刷新状态，\n            // 具体详见 Configs 类和 PageMessenger 类说明\n\n            mConfigs.token().set(s);\n            mStates.loadingVisible.set(false);\n            mMessenger.input(new Messages(Messages.EVENT_LOGIN_SUCCESS));\n            nav().navigateUp();\n        });\n    }\n\n    public class ClickProxy {\n\n        public void back() {\n            nav().navigateUp();\n        }\n\n        public void login() {\n\n            //TODO tip 5：通过双向绑定，使能通过 state-ViewModel 中与 xml 控件发生绑定的\"可观察数据\" 拿到控件数据，\n            // 避免直接接触控件实例而埋下 Null 安全一致性隐患。\n\n            //如这么说无体会，详见 https://xiaozhuanlan.com/topic/9816742350\n\n            if (TextUtils.isEmpty(mStates.name.get()) || TextUtils.isEmpty(mStates.password.get())) {\n                ToastUtils.showLongToast(getString(R.string.username_or_pwd_incomplete));\n                return;\n            }\n            User user = new User(mStates.name.get(), mStates.password.get());\n            mAccountRequester.requestLogin(user);\n            mStates.loadingVisible.set(true);\n        }\n    }\n\n    //TODO tip 6：基于单一职责原则，抽取 Jetpack ViewModel \"状态保存和恢复\" 的能力作为 StateHolder，\n    // 并使用 ObservableField 的改良版子类 State 来承担 BehaviorSubject，用作所绑定控件的 \"可信数据源\"，\n    // 从而在收到来自 PublishSubject 的结果回推后，响应结果数据的变化，也即通知控件属性重新渲染，并为其兜住最后一次状态，\n\n    //如这么说无体会，详见 https://xiaozhuanlan.com/topic/6741932805\n\n    public static class LoginStates extends StateHolder {\n\n        public final State<String> name = new State<>(\"\");\n\n        public final State<String> password = new State<>(\"\");\n\n        public final State<Boolean> loadingVisible = new State<>(false);\n\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/page/MainFragment.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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\npackage com.kunminx.puremusic.ui.page;\n\nimport android.annotation.SuppressLint;\nimport android.os.Bundle;\nimport android.view.View;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.kunminx.architecture.ui.page.BaseFragment;\nimport com.kunminx.architecture.ui.page.DataBindingConfig;\nimport com.kunminx.architecture.ui.page.StateHolder;\nimport com.kunminx.architecture.ui.state.State;\nimport com.kunminx.puremusic.BR;\nimport com.kunminx.puremusic.R;\nimport com.kunminx.puremusic.data.bean.TestAlbum;\nimport com.kunminx.puremusic.domain.event.Messages;\nimport com.kunminx.puremusic.domain.message.PageMessenger;\nimport com.kunminx.puremusic.domain.proxy.PlayerManager;\nimport com.kunminx.puremusic.domain.request.MusicRequester;\nimport com.kunminx.puremusic.ui.page.adapter.PlaylistAdapter;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Create by KunMinX at 19/10/29\n */\npublic class MainFragment extends BaseFragment {\n\n    //TODO tip 1：基于 \"单一职责原则\"，应将 ViewModel 划分为 state-ViewModel 和 result-ViewModel，\n    // state-ViewModel 职责仅限于托管、保存和恢复本页面 state，作用域仅限于本页面，\n    // result-ViewModel 职责仅限于 \"消息分发\" 场景承担 \"可信源\"，作用域依 \"数据请求\" 或 \"跨页通信\" 消息分发范围而定\n\n    // 如这么说无体会，详见 https://xiaozhuanlan.com/topic/8204519736\n\n    private MainStates mStates;\n    private PageMessenger mMessenger;\n    private MusicRequester mMusicRequester;\n    private PlaylistAdapter mAdapter;\n\n    @Override\n    protected void initViewModel() {\n        mStates = getFragmentScopeViewModel(MainStates.class);\n        mMessenger = getApplicationScopeViewModel(PageMessenger.class);\n        mMusicRequester = getFragmentScopeViewModel(MusicRequester.class);\n    }\n\n    @Override\n    protected DataBindingConfig getDataBindingConfig() {\n\n        mAdapter = new PlaylistAdapter(getContext());\n        mAdapter.setOnItemClickListener((viewId, item, position) -> {\n            PlayerManager.getInstance().playAudio(position);\n        });\n\n        //TODO tip 2: DataBinding 严格模式：\n        // 将 DataBinding 实例限制于 base 页面中，默认不向子类暴露，\n        // 通过这方式，彻底解决 View 实例 Null 安全一致性问题，\n        // 如此，View 实例 Null 安全性将和基于函数式编程思想的 Jetpack Compose 持平。\n        // 而 DataBindingConfig 就是在这样背景下，用于为 base 页面 DataBinding 提供绑定项。\n\n        // 如这么说无体会，详见 https://xiaozhuanlan.com/topic/9816742350 和 https://xiaozhuanlan.com/topic/2356748910\n\n        return new DataBindingConfig(R.layout.fragment_main, BR.vm, mStates)\n            .addBindingParam(BR.click, new ClickProxy())\n            .addBindingParam(BR.adapter, mAdapter);\n    }\n\n    @SuppressLint(\"NotifyDataSetChanged\")\n    @Override\n    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {\n        super.onViewCreated(view, savedInstanceState);\n\n        // TODO tip 3：所有播放状态的改变，皆来自 \"可信源\" PlayerManager 统一分发，\n        //  确保 \"消息分发可靠一致\"，避免不可预期推送和错误。\n\n        // 如这么说无体会，详见 https://xiaozhuanlan.com/topic/6017825943 & https://juejin.cn/post/7117498113983512589\n\n        PlayerManager.getInstance().getUiStates().observe(getViewLifecycleOwner(), uiStates -> {\n            mStates.musicId.set(uiStates.getMusicId(), changed -> mAdapter.notifyDataSetChanged());\n        });\n\n        //TODO tip 4:\n        // getViewLifeCycleOwner 是 2020 年新增特性，\n        // 主要为了解决 getView() 生命长度 比 fragment 短（仅存活于 onCreateView 之后和 onDestroyView 之前），\n        // 导致某些时候 fragment 其他成员还活着，但 getView() 为 null 的 生命周期安全问题，\n        // 也即，在 fragment 场景下，请使用 getViewLifeCycleOwner 作为 liveData 观察者。\n        // Activity 则不用改变。\n\n        mMusicRequester.getFreeMusicsResult().observe(getViewLifecycleOwner(), dataResult -> {\n            if (!dataResult.getResponseStatus().isSuccess()) return;\n\n            TestAlbum musicAlbum = dataResult.getResult();\n\n            // TODO tip 5：未作 UnPeek 处理的 LiveData，在视图控制器重建时会自动倒灌数据\n            // 请记得这一点，因为如果没有妥善处理，这里就可能出现预期外错误（例如收到旧数据推送），\n            // 所以，再一次，请记得它在重建时一定会倒灌。\n\n            // 如这么说无体会，详见 https://xiaozhuanlan.com/topic/6719328450\n\n            if (musicAlbum != null && musicAlbum.musics != null) {\n                mStates.list.set(musicAlbum.musics);\n                PlayerManager.getInstance().loadAlbum(musicAlbum);\n            }\n        });\n\n        mMessenger.output(this, messages -> {\n            switch (messages.eventId) {\n                case Messages.EVENT_LOGIN_SUCCESS:\n                    //TODO tip:\n                    //loginFragment 登录成功后的后续处理，例如刷新页面状态等\n                    break;\n            }\n        });\n\n        if (PlayerManager.getInstance().getAlbum() == null) mMusicRequester.requestFreeMusics();\n    }\n\n    // TODO tip 7：此处通过 DataBinding 规避 setOnClickListener 时存在的 View 实例 Null 安全一致性问题，\n\n    // 也即，有视图就绑定，无就无绑定，总之 不会因不一致性造成 View 实例 Null 安全问题。\n    // 如这么说无体会，详见 https://xiaozhuanlan.com/topic/9816742350\n\n    public class ClickProxy {\n\n        public void openMenu() {\n\n            // TODO tip 8：此处演示向 \"可信源\" 发送请求，以便实现 \"生命周期安全、消息分发可靠一致\" 的通知。\n\n            // 如这么说无体会，详见 https://xiaozhuanlan.com/topic/6017825943 & https://juejin.cn/post/7117498113983512589\n            // --------\n            // 与此同时，此处传达的另一思想是 \"最少知道原则\"，\n            // Activity 内部事情在 Activity 内部消化，不要试图在 fragment 中调用和操纵 Activity 内部东西。\n            // 因为 Activity 端的处理后续可能会改变，且可受用于更多 fragment，而不单单是本 fragment。\n\n            mMessenger.input(new Messages(Messages.EVENT_OPEN_DRAWER));\n        }\n\n        public void login() {\n            nav().navigate(R.id.action_mainFragment_to_loginFragment);\n        }\n\n        public void search() {\n            nav().navigate(R.id.action_mainFragment_to_searchFragment);\n        }\n\n    }\n\n    //TODO tip 9：每个页面都需单独准备一个 state-ViewModel，托管与 \"控件属性\" 发生绑定的 State，\n    // 此外，state-ViewModel 职责仅限于状态托管和保存恢复，不建议在此处理 UI 逻辑，\n\n    // UI 逻辑和业务逻辑，本质区别在于，前者是数据的消费者，后者是数据的生产者，\n    // 数据总是来自领域层业务逻辑的处理，并单向回推至 UI 层，在 UI 层中响应数据的变化（也即处理 UI 逻辑），\n    // 换言之，UI 逻辑只适合在 Activity/Fragment 等视图控制器中编写，将来升级到 Jetpack Compose 更是如此。\n\n    //如这么说无体会，详见 https://xiaozhuanlan.com/topic/6741932805\n\n    public static class MainStates extends StateHolder {\n\n        //TODO tip 10：此处我们使用 \"去除防抖特性\" 的 ObservableField 子类 State，用以代替 MutableLiveData，\n\n        //如这么说无体会，详见 https://xiaozhuanlan.com/topic/9816742350\n\n        public final State<String> musicId = new State<>(\"\", true);\n        public final State<Boolean> initTabAndPage = new State<>(true);\n\n        public final State<String> pageAssetPath = new State<>(\"summary.html\");\n\n        public final State<List<TestAlbum.TestMusic>> list = new State<>(new ArrayList<>());\n\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/page/PlayerFragment.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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\npackage com.kunminx.puremusic.ui.page;\n\nimport android.graphics.drawable.Drawable;\nimport android.os.Bundle;\nimport android.view.View;\nimport android.widget.SeekBar;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.kunminx.architecture.ui.page.BaseFragment;\nimport com.kunminx.architecture.ui.page.DataBindingConfig;\nimport com.kunminx.architecture.ui.page.StateHolder;\nimport com.kunminx.architecture.ui.state.State;\nimport com.kunminx.architecture.utils.Res;\nimport com.kunminx.architecture.utils.ToastUtils;\nimport com.kunminx.architecture.utils.Utils;\nimport com.kunminx.player.domain.PlayingInfoManager;\nimport com.kunminx.puremusic.BR;\nimport com.kunminx.puremusic.R;\nimport com.kunminx.puremusic.databinding.FragmentPlayerBinding;\nimport com.kunminx.puremusic.domain.event.Messages;\nimport com.kunminx.puremusic.domain.message.DrawerCoordinateManager;\nimport com.kunminx.puremusic.domain.message.PageMessenger;\nimport com.kunminx.puremusic.domain.proxy.PlayerManager;\nimport com.kunminx.puremusic.ui.page.helper.DefaultInterface;\nimport com.kunminx.puremusic.ui.view.PlayerSlideListener;\nimport com.sothree.slidinguppanel.SlidingUpPanelLayout;\n\nimport net.steamcrafted.materialiconlib.MaterialDrawableBuilder;\n\n/**\n * Create by KunMinX at 19/10/29\n */\npublic class PlayerFragment extends BaseFragment {\n\n    //TODO tip 1：基于 \"单一职责原则\"，应将 ViewModel 划分为 state-ViewModel 和 result-ViewModel，\n    // state-ViewModel 职责仅限于托管、保存和恢复本页面 state，作用域仅限于本页面，\n    // result-ViewModel 职责仅限于 \"消息分发\" 场景承担 \"可信源\"，作用域依 \"数据请求\" 或 \"跨页通信\" 消息分发范围而定\n\n    // 如这么说无体会，详见 https://xiaozhuanlan.com/topic/8204519736\n\n    private PlayerStates mStates;\n    private PlayerSlideListener.SlideAnimatorStates mAnimatorStates;\n    private PageMessenger mMessenger;\n    private PlayerSlideListener mListener;\n\n    @Override\n    protected void initViewModel() {\n        mStates = getFragmentScopeViewModel(PlayerStates.class);\n        mAnimatorStates = getFragmentScopeViewModel(PlayerSlideListener.SlideAnimatorStates.class);\n        mMessenger = getApplicationScopeViewModel(PageMessenger.class);\n    }\n\n    @Override\n    protected DataBindingConfig getDataBindingConfig() {\n\n        //TODO tip 2: DataBinding 严格模式：\n        // 将 DataBinding 实例限制于 base 页面中，默认不向子类暴露，\n        // 通过这方式，彻底解决 View 实例 Null 安全一致性问题，\n        // 如此，View 实例 Null 安全性将和基于函数式编程思想的 Jetpack Compose 持平。\n        // 而 DataBindingConfig 就是在这样背景下，用于为 base 页面 DataBinding 提供绑定项。\n\n        // 如这么说无体会，详见 https://xiaozhuanlan.com/topic/9816742350 和 https://xiaozhuanlan.com/topic/2356748910\n\n        return new DataBindingConfig(R.layout.fragment_player, BR.vm, mStates)\n            .addBindingParam(BR.panelVm, mAnimatorStates)\n            .addBindingParam(BR.click, new ClickProxy())\n            .addBindingParam(BR.listener, new ListenerHandler());\n    }\n\n    @Override\n    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {\n        super.onViewCreated(view, savedInstanceState);\n\n        //TODO tip 3: 此处演示使用 \"可信源\" MVI-Dispatcher input-output 接口完成消息收发\n\n        //如这么说无体会，详见《领域层设计》篇拆解 https://juejin.cn/post/7117498113983512589\n\n        mMessenger.output(this, messages -> {\n            switch (messages.eventId) {\n                case Messages.EVENT_ADD_SLIDE_LISTENER:\n                    if (view.getParent().getParent() instanceof SlidingUpPanelLayout) {\n                        SlidingUpPanelLayout sliding = (SlidingUpPanelLayout) view.getParent().getParent();\n\n                        //TODO tip 4: 警惕使用。非必要情况下，尽可能不在子类中拿到 binding 实例乃至获取 view 实例。使用即埋下隐患。\n                        // 目前方案是于 debug 模式，对获取实例情况给予提示。\n\n                        // 如这么说无体会，详见 https://xiaozhuanlan.com/topic/9816742350 和 https://xiaozhuanlan.com/topic/2356748910\n\n                        mListener = new PlayerSlideListener((FragmentPlayerBinding) getBinding(), mAnimatorStates, sliding);\n                        sliding.addPanelSlideListener(mListener);\n                        sliding.addPanelSlideListener(new DefaultInterface.PanelSlideListener() {\n                            @Override\n                            public void onPanelStateChanged(\n                                View view, SlidingUpPanelLayout.PanelState panelState,\n                                SlidingUpPanelLayout.PanelState panelState1) {\n                                DrawerCoordinateManager.getInstance().requestToUpdateDrawerMode(\n                                    panelState1 == SlidingUpPanelLayout.PanelState.EXPANDED,\n                                    this.getClass().getSimpleName()\n                                );\n                            }\n                        });\n                    }\n                    break;\n                case Messages.EVENT_CLOSE_SLIDE_PANEL_IF_EXPANDED:\n                    // 按下返回键，如果此时 slide 面板是展开的，那么只对面板进行 slide down\n\n                    if (view.getParent().getParent() instanceof SlidingUpPanelLayout) {\n                        SlidingUpPanelLayout sliding = (SlidingUpPanelLayout) view.getParent().getParent();\n                        if (sliding.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED) {\n                            sliding.setPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED);\n                        } else {\n\n                            // TODO tip 5：此处演示向 \"可信源\" 发送请求，以便实现 \"生命周期安全、消息分发可靠一致\" 的通知。\n\n                            // 如这么说无体会，详见 https://xiaozhuanlan.com/topic/0168753249\n                            // --------\n                            // 与此同时，此处传达的另一思想是 \"最少知道原则\"，\n                            // Activity 内部事情在 Activity 内部消化，不要试图在 fragment 中调用和操纵 Activity 内部东西。\n                            // 因为 Activity 端的处理后续可能会改变，且可受用于更多 fragment，而不单单是本 fragment。\n\n                            // TODO: yes:\n\n                            mMessenger.input(new Messages(Messages.EVENT_CLOSE_ACTIVITY_IF_ALLOWED));\n\n                            // TODO: do not:\n                            // mActivity.finish();\n                        }\n                    } else {\n                        mMessenger.input(new Messages(Messages.EVENT_CLOSE_ACTIVITY_IF_ALLOWED));\n                    }\n                    break;\n            }\n        });\n\n        // TODO tip 6：所有播放状态的改变，皆来自 getUiStates() 统一分发，\n        //  确保 \"消息分发可靠一致\"，避免不可预期推送和错误，\n\n        // 细节 1： uiStates 回调只读，此处只可通过 getter 获取只读数据，避免数据被篡改，\n        // 细节 2： uiStates 每次都是整个推送，progress 等属性会造成 uiStates 的高频回推，\n        //         故此宜对低频变化属性做防抖处理，仅当属性值变化时，通知相关控件完成一次重绘，\n\n        PlayerManager.getInstance().getUiStates().observe(getViewLifecycleOwner(), uiStates -> {\n            mStates.musicId.set(uiStates.getMusicId(), changed -> {\n                mStates.title.set(uiStates.getTitle());\n                mStates.artist.set(uiStates.getSummary());\n                mStates.coverImg.set(uiStates.getImg());\n                if (mListener != null) view.post(mListener::calculateTitleAndArtist);\n                mStates.maxSeekDuration.set(uiStates.getDuration());\n            });\n            mStates.currentSeekPosition.set(uiStates.getProgress());\n            mStates.isPlaying.set(!uiStates.isPaused());\n            mStates.repeatMode.set(uiStates.getRepeatMode(), changed -> {\n                mStates.playModeIcon.set(PlayerManager.getInstance().getModeIcon(uiStates.getRepeatMode()));\n            });\n        });\n    }\n\n    // TODO tip 7：此处通过 DataBinding 规避 setOnClickListener 时存在的 View 实例 Null 安全一致性问题，\n\n    // 也即，有视图就绑定，无就无绑定，总之 不会因不一致性造成 View 实例 Null 安全问题。\n    // 如这么说无体会，详见 https://xiaozhuanlan.com/topic/9816742350\n\n    public class ClickProxy {\n\n        public void playMode() {\n            PlayerManager.getInstance().changeMode();\n        }\n\n        public void previous() {\n            PlayerManager.getInstance().playPrevious();\n        }\n\n        public void togglePlay() {\n            PlayerManager.getInstance().togglePlay();\n        }\n\n        public void next() {\n            PlayerManager.getInstance().playNext();\n        }\n\n        public void showPlayList() {\n            ToastUtils.showShortToast(getString(R.string.unfinished));\n        }\n\n        //TODO tip: 同 tip 3\n\n        public void slideDown() {\n            mMessenger.input(new Messages(Messages.EVENT_CLOSE_SLIDE_PANEL_IF_EXPANDED));\n        }\n\n        public void more() {\n        }\n    }\n\n    public static class ListenerHandler implements DefaultInterface.OnSeekBarChangeListener {\n        @Override\n        public void onStopTrackingTouch(SeekBar seekBar) {\n            PlayerManager.getInstance().setSeek(seekBar.getProgress());\n        }\n    }\n\n    //TODO tip 8：基于单一职责原则，抽取 Jetpack ViewModel \"状态保存和恢复\" 的能力作为 StateHolder，\n    // 并使用 ObservableField 的改良版子类 State 来承担 BehaviorSubject，用作所绑定控件的 \"可信数据源\"，\n    // 从而在收到来自 PublishSubject 的结果回推后，响应结果数据的变化，也即通知控件属性重新渲染，并为其兜住最后一次状态，\n\n    //如这么说无体会，详见 https://xiaozhuanlan.com/topic/6741932805\n\n    public static class PlayerStates extends StateHolder {\n        public final State<String> musicId = new State<>(\"\", true);\n        public final State<Enum<PlayingInfoManager.RepeatMode>> repeatMode = new State<>(PlayingInfoManager.RepeatMode.LIST_CYCLE, true);\n        public final State<String> title = new State<>(Utils.getApp().getString(R.string.app_name), true);\n        public final State<String> artist = new State<>(Utils.getApp().getString(R.string.app_name), true);\n        public final State<String> coverImg = new State<>(\"\", true);\n        public final State<Drawable> placeHolder = new State<>(Res.getDrawable(R.drawable.bg_album_default), true);\n        public final State<Integer> maxSeekDuration = new State<>(0, true);\n        public final State<Integer> currentSeekPosition = new State<>(0, true);\n        public final State<Boolean> isPlaying = new State<>(false, true);\n        public final State<MaterialDrawableBuilder.IconValue> playModeIcon = new State<>(PlayerManager.getInstance().getModeIcon(), true);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/page/SearchFragment.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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\npackage com.kunminx.puremusic.ui.page;\n\nimport android.os.Bundle;\nimport android.view.View;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.kunminx.architecture.ui.page.BaseFragment;\nimport com.kunminx.architecture.ui.page.DataBindingConfig;\nimport com.kunminx.architecture.ui.page.StateHolder;\nimport com.kunminx.architecture.ui.state.State;\nimport com.kunminx.puremusic.BR;\nimport com.kunminx.puremusic.R;\nimport com.kunminx.puremusic.data.bean.DownloadState;\nimport com.kunminx.puremusic.data.config.Const;\nimport com.kunminx.puremusic.domain.event.DownloadEvent;\nimport com.kunminx.puremusic.domain.message.DrawerCoordinateManager;\nimport com.kunminx.puremusic.domain.request.DownloadRequester;\n\n/**\n * Create by KunMinX at 19/10/29\n */\npublic class SearchFragment extends BaseFragment {\n\n    //TODO tip 1：基于 \"单一职责原则\"，应将 ViewModel 划分为 state-ViewModel 和 result-ViewModel，\n    // state-ViewModel 职责仅限于托管、保存和恢复本页面 state，作用域仅限于本页面，\n    // result-ViewModel 职责仅限于 \"消息分发\" 场景承担 \"可信源\"，作用域依 \"数据请求\" 或 \"跨页通信\" 消息分发范围而定\n\n    // 如这么说无体会，详见 https://xiaozhuanlan.com/topic/8204519736\n\n    private SearchStates mStates;\n    private DownloadRequester mDownloadRequester;\n    private DownloadRequester mGlobalDownloadRequester;\n\n    @Override\n    protected void initViewModel() {\n        mStates = getFragmentScopeViewModel(SearchStates.class);\n        mDownloadRequester = getFragmentScopeViewModel(DownloadRequester.class);\n        mGlobalDownloadRequester = getActivityScopeViewModel(DownloadRequester.class);\n    }\n\n    @Override\n    protected DataBindingConfig getDataBindingConfig() {\n\n        //TODO tip 2: DataBinding 严格模式：\n        // 将 DataBinding 实例限制于 base 页面中，默认不向子类暴露，\n        // 通过这方式，彻底解决 View 实例 Null 安全一致性问题，\n        // 如此，View 实例 Null 安全性将和基于函数式编程思想的 Jetpack Compose 持平。\n        // 而 DataBindingConfig 就是在这样背景下，用于为 base 页面 DataBinding 提供绑定项。\n\n        // 如这么说无体会，详见 https://xiaozhuanlan.com/topic/9816742350 和 https://xiaozhuanlan.com/topic/2356748910\n\n        return new DataBindingConfig(R.layout.fragment_search, BR.vm, mStates)\n            .addBindingParam(BR.click, new ClickProxy());\n    }\n\n    @Override\n    public void onCreate(@Nullable Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n\n        getLifecycle().addObserver(DrawerCoordinateManager.getInstance());\n\n        //TODO tip 3：绑定跟随视图控制器生命周期、可叫停、单独放在 UseCase 中处理的业务\n        getLifecycle().addObserver(mDownloadRequester);\n    }\n\n    @Override\n    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {\n        super.onViewCreated(view, savedInstanceState);\n\n        //TODO tip 8: 此处演示使用 MVI-Dispatcher input-output 接口完成数据请求响应\n\n        //如这么说无体会，详见《领域层设计》篇拆解 https://juejin.cn/post/7117498113983512589\n\n        mDownloadRequester.output(this, downloadEvent -> {\n            if (downloadEvent.eventId == DownloadEvent.EVENT_DOWNLOAD) {\n                DownloadState state = downloadEvent.downloadState;\n                mStates.progress_cancelable.set(state.progress);\n                mStates.enableDownload.set(state.progress == 100 || state.progress == 0);\n            }\n        });\n\n        //TODO tip 9: 此处演示 \"同一 Result-ViewModel 类，在不同作用域下实例化，造成的不同结果\"\n\n        mGlobalDownloadRequester.output(this, downloadEvent -> {\n            if (downloadEvent.eventId == DownloadEvent.EVENT_DOWNLOAD_GLOBAL) {\n                DownloadState state = downloadEvent.downloadState;\n                mStates.progress.set(state.progress);\n                mStates.enableGlobalDownload.set(state.progress == 100 || state.progress == 0);\n            }\n        });\n    }\n\n    // TODO tip 4：此处通过 DataBinding 规避 setOnClickListener 时存在的 View 实例 Null 安全一致性问题，\n\n    // 也即，有视图就绑定，无就无绑定，总之 不会因不一致性造成 View 实例 Null 安全问题。\n    // 如这么说无体会，详见 https://xiaozhuanlan.com/topic/9816742350\n\n    public class ClickProxy {\n\n        public void back() {\n            nav().navigateUp();\n        }\n\n        public void testNav() {\n            openUrlInBrowser(Const.COLUMN_LINK);\n        }\n\n        public void subscribe() {\n            openUrlInBrowser(Const.COLUMN_LINK);\n        }\n\n        //TODO tip: 同 tip 8\n\n        public void testDownload() {\n            mGlobalDownloadRequester.input(new DownloadEvent(DownloadEvent.EVENT_DOWNLOAD_GLOBAL));\n        }\n\n        //TODO tip 5: 在 UseCase 中 执行可跟随生命周期中止的下载任务\n\n        public void testLifecycleDownload() {\n            mDownloadRequester.input(new DownloadEvent(DownloadEvent.EVENT_DOWNLOAD));\n        }\n    }\n\n    //TODO tip 6：基于单一职责原则，抽取 Jetpack ViewModel \"状态保存和恢复\" 的能力作为 StateHolder，\n    // 并使用 ObservableField 的改良版子类 State 来承担 BehaviorSubject，用作所绑定控件的 \"可信数据源\"，\n    // 从而在收到来自 PublishSubject 的结果回推后，响应结果数据的变化，也即通知控件属性重新渲染，并为其兜住最后一次状态，\n\n    //如这么说无体会，详见 https://xiaozhuanlan.com/topic/6741932805\n\n    public static class SearchStates extends StateHolder {\n\n        public final State<Integer> progress = new State<>(1);\n\n        public final State<Integer> progress_cancelable = new State<>(1);\n\n        public final State<Boolean> enableDownload = new State<>(true);\n\n        public final State<Boolean> enableGlobalDownload = new State<>(true);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/page/adapter/DiffUtils.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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\npackage com.kunminx.puremusic.ui.page.adapter;\n\nimport androidx.annotation.NonNull;\nimport androidx.recyclerview.widget.DiffUtil;\n\nimport com.kunminx.puremusic.data.bean.LibraryInfo;\nimport com.kunminx.puremusic.data.bean.TestAlbum;\n\n/**\n * Create by KunMinX at 2020/7/19\n */\npublic class DiffUtils {\n\n    private DiffUtil.ItemCallback<LibraryInfo> mLibraryInfoItemCallback;\n\n    private DiffUtil.ItemCallback<TestAlbum.TestMusic> mTestMusicItemCallback;\n\n    private DiffUtils() {\n    }\n\n    private static final DiffUtils S_DIFF_UTILS = new DiffUtils();\n\n    public static DiffUtils getInstance() {\n        return S_DIFF_UTILS;\n    }\n\n    public DiffUtil.ItemCallback<LibraryInfo> getLibraryInfoItemCallback() {\n        if (mLibraryInfoItemCallback == null) {\n            mLibraryInfoItemCallback = new DiffUtil.ItemCallback<LibraryInfo>() {\n                @Override\n                public boolean areItemsTheSame(@NonNull LibraryInfo oldItem, @NonNull LibraryInfo newItem) {\n                    return oldItem.equals(newItem);\n                }\n\n                @Override\n                public boolean areContentsTheSame(@NonNull LibraryInfo oldItem, @NonNull LibraryInfo newItem) {\n                    return oldItem.getTitle().equals(newItem.getTitle());\n                }\n            };\n        }\n        return mLibraryInfoItemCallback;\n    }\n\n    public DiffUtil.ItemCallback<TestAlbum.TestMusic> getTestMusicItemCallback() {\n        if (mTestMusicItemCallback == null) {\n            mTestMusicItemCallback = new DiffUtil.ItemCallback<TestAlbum.TestMusic>() {\n                @Override\n                public boolean areItemsTheSame(@NonNull TestAlbum.TestMusic oldItem, @NonNull TestAlbum.TestMusic newItem) {\n                    return oldItem.equals(newItem);\n                }\n\n                @Override\n                public boolean areContentsTheSame(@NonNull TestAlbum.TestMusic oldItem, @NonNull TestAlbum.TestMusic newItem) {\n                    return oldItem.musicId.equals(newItem.musicId);\n                }\n            };\n        }\n        return mTestMusicItemCallback;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/page/adapter/DrawerAdapter.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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\npackage com.kunminx.puremusic.ui.page.adapter;\n\nimport android.content.Context;\nimport android.content.Intent;\nimport android.net.Uri;\n\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.kunminx.binding_recyclerview.adapter.SimpleDataBindingAdapter;\nimport com.kunminx.puremusic.R;\nimport com.kunminx.puremusic.data.bean.LibraryInfo;\nimport com.kunminx.puremusic.databinding.AdapterLibraryBinding;\n\n/**\n * Create by KunMinX at 20/4/19\n */\npublic class DrawerAdapter extends SimpleDataBindingAdapter<LibraryInfo, AdapterLibraryBinding> {\n\n    public DrawerAdapter(Context context) {\n        super(context, R.layout.adapter_library, DiffUtils.getInstance().getLibraryInfoItemCallback());\n\n        //TODO item click 回调可以在 adapter 中实现，也可以在外部实现\n        setOnItemClickListener((viewId, item, position) -> {\n            Uri uri = Uri.parse(item.getUrl());\n            Intent intent = new Intent(Intent.ACTION_VIEW, uri);\n            mContext.startActivity(intent);\n        });\n    }\n\n    @Override\n    protected void onBindItem(AdapterLibraryBinding binding, LibraryInfo item, RecyclerView.ViewHolder holder) {\n        binding.setInfo(item);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/page/adapter/PlaylistAdapter.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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\npackage com.kunminx.puremusic.ui.page.adapter;\n\nimport android.content.Context;\nimport android.graphics.Color;\n\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.kunminx.binding_recyclerview.adapter.SimpleDataBindingAdapter;\nimport com.kunminx.puremusic.R;\nimport com.kunminx.puremusic.data.bean.TestAlbum;\nimport com.kunminx.puremusic.databinding.AdapterPlayItemBinding;\nimport com.kunminx.puremusic.domain.proxy.PlayerManager;\n\n/**\n * Create by KunMinX at 20/4/19\n */\npublic class PlaylistAdapter extends SimpleDataBindingAdapter<TestAlbum.TestMusic, AdapterPlayItemBinding> {\n\n    public PlaylistAdapter(Context context) {\n        super(context, R.layout.adapter_play_item, DiffUtils.getInstance().getTestMusicItemCallback());\n    }\n\n    @Override\n    protected void onBindItem(AdapterPlayItemBinding binding, TestAlbum.TestMusic item, RecyclerView.ViewHolder holder) {\n        binding.setAlbum(item);\n        int currentIndex = PlayerManager.getInstance().getAlbumIndex();\n        binding.ivPlayStatus.setColor(currentIndex == holder.getAbsoluteAdapterPosition()\n            ? binding.getRoot().getContext().getColor(R.color.gray) : Color.TRANSPARENT);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/page/helper/DefaultInterface.java",
    "content": "/*\n *\n *  * Copyright 2018-present KunMinX\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 */\n\npackage com.kunminx.puremusic.ui.page.helper;\n\nimport android.view.View;\nimport android.widget.SeekBar;\n\nimport com.sothree.slidinguppanel.SlidingUpPanelLayout;\n\n/**\n * Create by KunMinX at 2020/12/3\n */\npublic class DefaultInterface {\n\n    public interface OnSeekBarChangeListener extends SeekBar.OnSeekBarChangeListener {\n        @Override\n        default void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {\n        }\n\n        @Override\n        default void onStartTrackingTouch(SeekBar seekBar) {\n        }\n\n        @Override\n        default void onStopTrackingTouch(SeekBar seekBar) {\n        }\n    }\n\n    public interface PanelSlideListener extends SlidingUpPanelLayout.PanelSlideListener {\n        @Override\n        default void onPanelSlide(View panel, float slideOffset) {\n        }\n\n        @Override\n        default void onPanelStateChanged(View panel,\n                                         SlidingUpPanelLayout.PanelState previousState,\n                                         SlidingUpPanelLayout.PanelState newState) {\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/view/PlayPauseDrawable.java",
    "content": "package com.kunminx.puremusic.ui.view;\n\nimport android.animation.Animator;\nimport android.animation.AnimatorListenerAdapter;\nimport android.animation.ObjectAnimator;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.ColorFilter;\nimport android.graphics.Paint;\nimport android.graphics.Path;\nimport android.graphics.PixelFormat;\nimport android.graphics.Rect;\nimport android.graphics.RectF;\nimport android.graphics.drawable.Drawable;\nimport android.util.Property;\n\nimport androidx.annotation.ColorInt;\n\npublic class PlayPauseDrawable extends Drawable {\n\n    private static final Property<PlayPauseDrawable, Float> PROGRESS = new Property<PlayPauseDrawable, Float>(Float.class, \"progress\") {\n        @Override\n        public Float get(PlayPauseDrawable d) {\n            return d.getProgress();\n        }\n\n        @Override\n        public void set(PlayPauseDrawable d, Float value) {\n            d.setProgress(value);\n        }\n    };\n\n    private final Path mLeftPauseBar = new Path();\n    private final Path mRightPauseBar = new Path();\n    private final Paint mPaint = new Paint();\n    private final RectF mBounds = new RectF();\n    private float mPauseBarWidth;\n    private float mPauseBarHeight;\n    private float mPauseBarDistance;\n\n    private float mWidth;\n    private float mHeight;\n\n    private float mProgress;\n    private boolean mIsPlay;\n\n    public PlayPauseDrawable() {\n        mPaint.setAntiAlias(true);\n        mPaint.setStyle(Paint.Style.FILL);\n        mPaint.setColor(Color.BLACK);\n    }\n\n    public PlayPauseDrawable(@ColorInt int color) {\n        mPaint.setAntiAlias(true);\n        mPaint.setStyle(Paint.Style.FILL);\n        mPaint.setColor(color);\n    }\n\n    private static float interpolate(float a, float b, float t) {\n        return a + (b - a) * t;\n    }\n\n    public void setIsPlay(boolean isPlay) {\n        this.mIsPlay = isPlay;\n    }\n\n    @Override\n    protected void onBoundsChange(Rect bounds) {\n        super.onBoundsChange(bounds);\n        mBounds.set(bounds);\n        mWidth = mBounds.width();\n        mHeight = mBounds.height();\n\n        mPauseBarWidth = mWidth / 8;\n        mPauseBarHeight = mHeight * 0.40f;\n        mPauseBarDistance = mPauseBarWidth;\n\n    }\n\n    @Override\n    public void draw(Canvas canvas) {\n        mLeftPauseBar.rewind();\n        mRightPauseBar.rewind();\n\n        final float barDist = interpolate(mPauseBarDistance, 0, mProgress);\n        final float barWidth = interpolate(mPauseBarWidth, mPauseBarHeight / 2f, mProgress);\n        final float firstBarTopLeft = interpolate(0, barWidth, mProgress);\n        final float secondBarTopRight = interpolate(2 * barWidth + barDist, barWidth + barDist, mProgress);\n\n        mLeftPauseBar.moveTo(0, 0);\n        mLeftPauseBar.lineTo(firstBarTopLeft, -mPauseBarHeight);\n        mLeftPauseBar.lineTo(barWidth, -mPauseBarHeight);\n        mLeftPauseBar.lineTo(barWidth, 0);\n        mLeftPauseBar.close();\n\n        mRightPauseBar.moveTo(barWidth + barDist, 0);\n        mRightPauseBar.lineTo(barWidth + barDist, -mPauseBarHeight);\n        mRightPauseBar.lineTo(secondBarTopRight, -mPauseBarHeight);\n        mRightPauseBar.lineTo(2 * barWidth + barDist, 0);\n        mRightPauseBar.close();\n\n        canvas.save();\n\n        canvas.translate(interpolate(0, mPauseBarHeight / 8f, mProgress), 0);\n\n        final float rotationProgress = mIsPlay ? 1 - mProgress : mProgress;\n        final float startingRotation = mIsPlay ? 90 : 0;\n        canvas.rotate(interpolate(startingRotation, startingRotation + 90, rotationProgress), mWidth / 2f, mHeight / 2f);\n\n        canvas.translate(mWidth / 2f - ((2 * barWidth + barDist) / 2f), mHeight / 2f + (mPauseBarHeight / 2f));\n\n        canvas.drawPath(mLeftPauseBar, mPaint);\n        canvas.drawPath(mRightPauseBar, mPaint);\n\n        canvas.restore();\n    }\n\n    public Animator getPausePlayAnimator() {\n        final Animator anim = ObjectAnimator.ofFloat(this, PROGRESS, mIsPlay ? 1 : 0, mIsPlay ? 0 : 1);\n        anim.addListener(new AnimatorListenerAdapter() {\n            @Override\n            public void onAnimationEnd(Animator animation) {\n                mIsPlay = !mIsPlay;\n            }\n        });\n        return anim;\n    }\n\n    public boolean isPlay() {\n        return mIsPlay;\n    }\n\n    private float getProgress() {\n        return mProgress;\n    }\n\n    private void setProgress(float progress) {\n        mProgress = progress;\n        invalidateSelf();\n    }\n\n    @Override\n    public void setAlpha(int alpha) {\n        mPaint.setAlpha(alpha);\n        invalidateSelf();\n    }\n\n    public void setDrawableColor(@ColorInt int color) {\n        mPaint.setColor(color);\n        invalidateSelf();\n    }\n\n    @Override\n    public void setColorFilter(ColorFilter cf) {\n        mPaint.setColorFilter(cf);\n        invalidateSelf();\n    }\n\n    @Override\n    public int getOpacity() {\n        return PixelFormat.TRANSLUCENT;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/view/PlayPauseView.java",
    "content": "package com.kunminx.puremusic.ui.view;\n\nimport android.animation.Animator;\nimport android.animation.AnimatorSet;\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.Outline;\nimport android.graphics.Paint;\nimport android.graphics.drawable.Drawable;\nimport android.util.AttributeSet;\nimport android.view.View;\nimport android.view.ViewOutlineProvider;\nimport android.view.animation.DecelerateInterpolator;\nimport android.widget.FrameLayout;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.NonNull;\n\nimport com.kunminx.puremusic.R;\n\npublic class PlayPauseView extends FrameLayout {\n\n    private static final long PLAY_PAUSE_ANIMATION_DURATION = 200;\n    public final boolean isDrawCircle;\n    private final PlayPauseDrawable mDrawable;\n    private final Paint mPaint = new Paint();\n    public int circleAlpha;\n    private int mDrawableColor;\n    private AnimatorSet mAnimatorSet;\n    private int mBackgroundColor;\n    private int mWidth;\n    private int mHeight;\n    private boolean mIsPlay;\n\n    public PlayPauseView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        setWillNotDraw(false);\n\n        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.PlayPauseView);\n        isDrawCircle = typedArray.getBoolean(R.styleable.PlayPauseView_isCircleDraw, true);\n        circleAlpha = typedArray.getInt(R.styleable.PlayPauseView_circleAlpha, 255);\n        mDrawableColor = typedArray.getInt(R.styleable.PlayPauseView_drawableColor, Color.WHITE);\n        typedArray.recycle();\n\n        mPaint.setAntiAlias(true);\n        mPaint.setStyle(Paint.Style.FILL);\n        mPaint.setAlpha(circleAlpha);\n        mPaint.setColor(mBackgroundColor);\n        mDrawable = new PlayPauseDrawable(mDrawableColor);\n        mDrawable.setCallback(this);\n\n    }\n\n    @Override\n    protected void onSizeChanged(final int w, final int h, int oldw, int oldh) {\n        super.onSizeChanged(w, h, oldw, oldh);\n        mDrawable.setBounds(0, 0, w, h);\n        mWidth = w;\n        mHeight = h;\n\n        setOutlineProvider(new ViewOutlineProvider() {\n            @Override\n            public void getOutline(View view, Outline outline) {\n                outline.setOval(0, 0, view.getWidth(), view.getHeight());\n            }\n        });\n        setClipToOutline(true);\n    }\n\n    public void setCircleAlpha(int alpah) {\n        circleAlpha = alpah;\n        invalidate();\n    }\n\n    private int getCircleColor() {\n        return mBackgroundColor;\n    }\n\n    public void setCircleColor(@ColorInt int color) {\n        mBackgroundColor = color;\n        invalidate();\n    }\n\n    public int getDrawableColor() {\n        return mDrawableColor;\n    }\n\n    public void setDrawableColor(@ColorInt int color) {\n        mDrawableColor = color;\n        mDrawable.setDrawableColor(color);\n        invalidate();\n    }\n\n    @Override\n    protected boolean verifyDrawable(@NonNull Drawable who) {\n        return who == mDrawable || super.verifyDrawable(who);\n    }\n\n    @Override\n    public boolean hasOverlappingRendering() {\n        return false;\n    }\n\n    @Override\n    protected void onDraw(Canvas canvas) {\n        super.onDraw(canvas);\n        mPaint.setColor(mBackgroundColor);\n        final float radius = Math.min(mWidth, mHeight) / 2f;\n        if (isDrawCircle) {\n            mPaint.setColor(mBackgroundColor);\n            mPaint.setAlpha(circleAlpha);\n            canvas.drawCircle(mWidth / 2f, mHeight / 2f, radius, mPaint);\n        }\n        mDrawable.draw(canvas);\n    }\n\n    public boolean isPlay() {\n        return mIsPlay;\n    }\n\n    public void play() {\n        if (mAnimatorSet != null) {\n            mAnimatorSet.cancel();\n        }\n        mAnimatorSet = new AnimatorSet();\n        mDrawable.setIsPlay(mIsPlay = true);\n        final Animator pausePlayAnim = mDrawable.getPausePlayAnimator();\n        mAnimatorSet.setInterpolator(new DecelerateInterpolator());\n        mAnimatorSet.setDuration(PLAY_PAUSE_ANIMATION_DURATION);\n        pausePlayAnim.start();\n    }\n\n    public void pause() {\n        if (mAnimatorSet != null) {\n            mAnimatorSet.cancel();\n        }\n        mAnimatorSet = new AnimatorSet();\n        mDrawable.setIsPlay(mIsPlay = false);\n        final Animator pausePlayAnim = mDrawable.getPausePlayAnimator();\n        mAnimatorSet.setInterpolator(new DecelerateInterpolator());\n        mAnimatorSet.setDuration(PLAY_PAUSE_ANIMATION_DURATION);\n        pausePlayAnim.start();\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/view/PlayerSlideListener.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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\npackage com.kunminx.puremusic.ui.view;\n\nimport android.animation.ArgbEvaluator;\nimport android.animation.FloatEvaluator;\nimport android.animation.IntEvaluator;\nimport android.graphics.Color;\nimport android.graphics.Rect;\nimport android.util.Pair;\nimport android.view.View;\nimport android.widget.TextView;\n\nimport com.kunminx.architecture.ui.page.StateHolder;\nimport com.kunminx.architecture.ui.state.State;\nimport com.kunminx.architecture.utils.DisplayUtils;\nimport com.kunminx.architecture.utils.ScreenUtils;\nimport com.kunminx.puremusic.databinding.FragmentPlayerBinding;\nimport com.sothree.slidinguppanel.SlidingUpPanelLayout;\n\n/**\n * Create by KunMinX at 19/10/29\n */\npublic class PlayerSlideListener implements SlidingUpPanelLayout.PanelSlideListener {\n\n    private final FragmentPlayerBinding mBinding;\n    private final SlidingUpPanelLayout mSlidingUpPanelLayout;\n    private final SlideAnimatorStates mStates;\n\n    private int mTitleEndTranslationX;\n    private int mArtistEndTranslationX;\n    private int mArtistNormalEndTranslationY;\n    private int mContentNormalEndTranslationY;\n\n    private final int mModeStartX;\n    private final int mPreviousStartX;\n    private final int mPlayPauseStartX;\n    private final int mNextStartX;\n    private final int mPlayQueueStartX;\n    private final int mPlayPauseEndX;\n    private final int mPreviousEndX;\n    private final int mModeEndX;\n    private final int mNextEndX;\n    private final int mPlayQueueEndX;\n    private final int mIconContainerStartY;\n    private final int mIconContainerEndY;\n\n    private final int SCREEN_WIDTH;\n    private final int SCREEN_HEIGHT;\n\n    private final IntEvaluator INT_EVALUATOR = new IntEvaluator();\n    private final FloatEvaluator FLOAT_EVALUATOR = new FloatEvaluator();\n    private final ArgbEvaluator COLOR_EVALUATOR = new ArgbEvaluator();\n\n    private final int NOW_PLAYING_CARD_COLOR;\n    private final int PLAY_PAUSE_DRAWABLE_COLOR;\n    private Status mStatus = Status.COLLAPSED;\n\n    public enum Status {\n        EXPANDED,\n        COLLAPSED,\n    }\n\n    public PlayerSlideListener(FragmentPlayerBinding binding, SlideAnimatorStates states, SlidingUpPanelLayout slidingUpPanelLayout) {\n        mBinding = binding;\n        mStates = states;\n        mSlidingUpPanelLayout = slidingUpPanelLayout;\n        SCREEN_WIDTH = ScreenUtils.getScreenWidth();\n        SCREEN_HEIGHT = ScreenUtils.getScreenHeight();\n        PLAY_PAUSE_DRAWABLE_COLOR = Color.BLACK;\n        NOW_PLAYING_CARD_COLOR = Color.WHITE;\n        calculateTitleAndArtist();\n        mModeStartX = binding.mode != null ? binding.mode.getLeft() : 0;\n        mPreviousStartX = binding.previous.getLeft();\n        mPlayPauseStartX = binding.playPause.getLeft();\n        mNextStartX = binding.next.getLeft();\n        mPlayQueueStartX = binding.icPlayList != null ? binding.icPlayList.getLeft() : 0;\n        int size = DisplayUtils.dp2px(36);\n        int gap = (SCREEN_WIDTH - 5 * (size)) / 6;\n        mPlayPauseEndX = (SCREEN_WIDTH / 2) - (size / 2);\n        mPreviousEndX = mPlayPauseEndX - gap - size;\n        mModeEndX = mPreviousEndX - gap - size;\n        mNextEndX = mPlayPauseEndX + gap + size;\n        mPlayQueueEndX = mNextEndX + gap + size;\n        mIconContainerStartY = binding.iconContainer.getTop();\n        int tempImgSize = DisplayUtils.dp2px(55);\n        mStates.albumArtSize.set(new Pair<>(tempImgSize, tempImgSize));\n        mIconContainerEndY = SCREEN_HEIGHT - 3 * binding.iconContainer.getHeight() - binding.seekBottom.getHeight();\n        mStates.playPauseDrawableColor.set(PLAY_PAUSE_DRAWABLE_COLOR);\n        mStates.playCircleAlpha.set(INT_EVALUATOR.evaluate(0, 0, 255));\n        mStates.nextX.set(mNextStartX);\n        mStates.modeX.set(0);\n        mStates.previousX.set(0);\n        mStates.playPauseX.set(mPlayPauseStartX);\n        mStates.iconContainerY.set(mIconContainerStartY);\n        mBinding.executePendingBindings();\n    }\n\n    @Override\n    public void onPanelSlide(View panel, float slideOffset) {\n        calculateTitleAndArtist();\n        int tempImgSize = INT_EVALUATOR.evaluate(slideOffset, DisplayUtils.dp2px(55), SCREEN_WIDTH);\n        mStates.albumArtSize.set(new Pair<>(tempImgSize, tempImgSize));\n        mStates.titleTranslationX.set(FLOAT_EVALUATOR.evaluate(slideOffset, 0, mTitleEndTranslationX));\n        mStates.artistTranslationX.set(FLOAT_EVALUATOR.evaluate(slideOffset, 0, mArtistEndTranslationX));\n        mStates.artistTranslationY.set(FLOAT_EVALUATOR.evaluate(slideOffset, 0, mArtistNormalEndTranslationY));\n        mStates.summaryTranslationY.set(FLOAT_EVALUATOR.evaluate(slideOffset, 0, mContentNormalEndTranslationY));\n        mStates.playPauseX.set(INT_EVALUATOR.evaluate(slideOffset, mPlayPauseStartX, mPlayPauseEndX));\n        mStates.playCircleAlpha.set(INT_EVALUATOR.evaluate(slideOffset, 0, 255));\n        mStates.playPauseDrawableColor.set((int) COLOR_EVALUATOR.evaluate(slideOffset, PLAY_PAUSE_DRAWABLE_COLOR, NOW_PLAYING_CARD_COLOR));\n        mStates.previousX.set(INT_EVALUATOR.evaluate(slideOffset, mPreviousStartX, mPreviousEndX));\n        mStates.modeX.set(INT_EVALUATOR.evaluate(slideOffset, mModeStartX, mModeEndX));\n        mStates.nextX.set(INT_EVALUATOR.evaluate(slideOffset, mNextStartX, mNextEndX));\n        mStates.icPlayListX.set(INT_EVALUATOR.evaluate(slideOffset, mPlayQueueStartX, mPlayQueueEndX));\n        mStates.modeAlpha.set(FLOAT_EVALUATOR.evaluate(slideOffset, 0, 1));\n        mStates.previousAlpha.set(FLOAT_EVALUATOR.evaluate(slideOffset, 0, 1));\n        mStates.iconContainerY.set(INT_EVALUATOR.evaluate(slideOffset, mIconContainerStartY, mIconContainerEndY));\n        mBinding.executePendingBindings();\n    }\n\n    @Override\n    public void onPanelStateChanged(View panel, SlidingUpPanelLayout.PanelState previousState, SlidingUpPanelLayout.PanelState newState) {\n        if (previousState == SlidingUpPanelLayout.PanelState.COLLAPSED) {\n            mStates.songProgressNormalVisibility.set(false);\n            mStates.modeVisibility.set(true);\n            mStates.previousVisibility.set(true);\n        }\n        if (newState == SlidingUpPanelLayout.PanelState.EXPANDED) {\n            mStatus = Status.EXPANDED;\n            mStates.customToolbarVisibility.set(true);\n        } else if (newState == SlidingUpPanelLayout.PanelState.COLLAPSED) {\n            mStatus = Status.COLLAPSED;\n            mStates.songProgressNormalVisibility.set(true);\n            mStates.modeVisibility.set(false);\n            mStates.previousVisibility.set(false);\n            mBinding.topContainer.setOnClickListener(v -> {\n                if (mSlidingUpPanelLayout.isTouchEnabled())\n                    mSlidingUpPanelLayout.setPanelState(SlidingUpPanelLayout.PanelState.EXPANDED);\n            });\n        } else if (newState == SlidingUpPanelLayout.PanelState.DRAGGING) {\n            mStates.customToolbarVisibility.set(false);\n        }\n    }\n\n    public void calculateTitleAndArtist() {\n        int titleWidth = getTextWidth(mBinding.title != null ? mBinding.title : null);\n        int artistWidth = getTextWidth(mBinding.artist != null ? mBinding.artist : null);\n        mTitleEndTranslationX = (SCREEN_WIDTH / 2) - (titleWidth / 2) - DisplayUtils.dp2px(67);\n        mArtistEndTranslationX = (SCREEN_WIDTH / 2) - (artistWidth / 2) - DisplayUtils.dp2px(67);\n        mArtistNormalEndTranslationY = DisplayUtils.dp2px(12);\n        mContentNormalEndTranslationY = SCREEN_WIDTH + DisplayUtils.dp2px(32);\n        mStates.titleTranslationX.set(mStatus == Status.COLLAPSED ? 0f : mTitleEndTranslationX);\n        mStates.artistTranslationX.set(mStatus == Status.COLLAPSED ? 0f : mArtistEndTranslationX);\n    }\n\n    private int getTextWidth(TextView textView) {\n        if (textView == null) return 0;\n        Rect artistBounds = new Rect();\n        textView.getPaint().getTextBounds(textView.getText().toString(), 0, textView.getText().length(), artistBounds);\n        return artistBounds.width();\n    }\n\n    /**\n     * TODO tip：使用 ObservableField 绑定，尽可能减少 View 实例 Null 安全一致性问题\n     * <p>\n     *  如这么说无体会，详见 https://xiaozhuanlan.com/topic/9816742350 和 https://xiaozhuanlan.com/topic/2356748910\n     */\n    public static class SlideAnimatorStates extends StateHolder {\n        public final State<Float> titleTranslationX = new State<>(0f);\n        public final State<Float> artistTranslationX = new State<>(0f);\n        public final State<Float> artistTranslationY = new State<>(0f);\n        public final State<Float> summaryTranslationY = new State<>(0f);\n        public final State<Integer> playPauseX = new State<>(0);\n        public final State<Integer> playCircleAlpha = new State<>(0);\n        public final State<Integer> playPauseDrawableColor = new State<>(0);\n        public final State<Integer> previousX = new State<>(0);\n        public final State<Integer> modeX = new State<>(0);\n        public final State<Integer> nextX = new State<>(0);\n        public final State<Integer> icPlayListX = new State<>(0);\n        public final State<Float> modeAlpha = new State<>(0f);\n        public final State<Float> previousAlpha = new State<>(0f);\n        public final State<Integer> iconContainerY = new State<>(0);\n        public final State<Boolean> songProgressNormalVisibility = new State<>(false);\n        public final State<Boolean> modeVisibility = new State<>(false);\n        public final State<Boolean> previousVisibility = new State<>(false);\n        public final State<Boolean> customToolbarVisibility = new State<>(false);\n        public final State<Pair<Integer, Integer>> albumArtSize = new State<>(new Pair<>(0, 0));\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/widget/PlayerService.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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\npackage com.kunminx.puremusic.ui.widget;\n\nimport android.annotation.SuppressLint;\nimport android.app.Notification;\nimport android.app.NotificationChannel;\nimport android.app.NotificationChannelGroup;\nimport android.app.NotificationManager;\nimport android.app.PendingIntent;\nimport android.app.Service;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.graphics.Bitmap;\nimport android.os.Build;\nimport android.os.IBinder;\nimport android.view.View;\nimport android.widget.RemoteViews;\n\nimport androidx.annotation.Nullable;\nimport androidx.core.app.NotificationCompat;\n\nimport com.kunminx.architecture.domain.usecase.UseCaseHandler;\nimport com.kunminx.architecture.utils.ImageUtils;\nimport com.kunminx.puremusic.MainActivity;\nimport com.kunminx.puremusic.R;\nimport com.kunminx.puremusic.data.bean.TestAlbum;\nimport com.kunminx.puremusic.data.config.Const;\nimport com.kunminx.puremusic.domain.proxy.PlayerManager;\nimport com.kunminx.puremusic.domain.usecase.DownloadUseCase;\n\nimport java.io.File;\n\n/**\n * Create by KunMinX at 19/7/17\n */\npublic class PlayerService extends Service {\n\n    public static final String NOTIFY_PREVIOUS = \"pure_music.kunminx.previous\";\n    public static final String NOTIFY_CLOSE = \"pure_music.kunminx.close\";\n    public static final String NOTIFY_PAUSE = \"pure_music.kunminx.pause\";\n    public static final String NOTIFY_PLAY = \"pure_music.kunminx.play\";\n    public static final String NOTIFY_NEXT = \"pure_music.kunminx.next\";\n    private static final String GROUP_ID = \"group_001\";\n    private static final String CHANNEL_ID = \"channel_001\";\n    private DownloadUseCase mDownloadUseCase;\n\n    @Override\n    public int onStartCommand(Intent intent, int flags, int startId) {\n        TestAlbum.TestMusic results = PlayerManager.getInstance().getCurrentPlayingMusic();\n        if (results == null) {\n            stopSelf();\n            return START_NOT_STICKY;\n        }\n\n        createNotification(results);\n        return START_NOT_STICKY;\n    }\n\n    private void createNotification(TestAlbum.TestMusic testMusic) {\n        try {\n            String title = testMusic.title;\n            TestAlbum album = PlayerManager.getInstance().getAlbum();\n            String summary = album.summary;\n\n            RemoteViews simpleContentView = new RemoteViews(\n                getApplicationContext().getPackageName(), R.layout.notify_player_small);\n\n            RemoteViews expandedView;\n            expandedView = new RemoteViews(\n                getApplicationContext().getPackageName(), R.layout.notify_player_big);\n\n            Intent intent = new Intent(getApplicationContext(), MainActivity.class);\n            intent.setAction(\"showPlayer\");\n\n            PendingIntent contentIntent = PendingIntent.getActivity(this, 0, intent,\n                Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ? PendingIntent.FLAG_MUTABLE : 0);\n\n            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {\n                NotificationManager notificationManager = (NotificationManager)\n                    getSystemService(Context.NOTIFICATION_SERVICE);\n\n                NotificationChannelGroup playGroup = new NotificationChannelGroup(GROUP_ID, getString(R.string.play));\n                notificationManager.createNotificationChannelGroup(playGroup);\n\n                NotificationChannel playChannel = new NotificationChannel(CHANNEL_ID,\n                    getString(R.string.notify_of_play), NotificationManager.IMPORTANCE_DEFAULT);\n                playChannel.setGroup(GROUP_ID);\n                notificationManager.createNotificationChannel(playChannel);\n            }\n\n            Notification notification = new NotificationCompat.Builder(\n                getApplicationContext(), CHANNEL_ID)\n                .setSmallIcon(R.drawable.ic_player)\n                .setContentIntent(contentIntent)\n                .setOnlyAlertOnce(true)\n                .setContentTitle(title).build();\n\n            notification.contentView = simpleContentView;\n            notification.bigContentView = expandedView;\n\n            setListeners(simpleContentView);\n            setListeners(expandedView);\n\n            notification.contentView.setViewVisibility(R.id.player_progress_bar, View.GONE);\n            notification.contentView.setViewVisibility(R.id.player_next, View.VISIBLE);\n            notification.contentView.setViewVisibility(R.id.player_previous, View.VISIBLE);\n            notification.bigContentView.setViewVisibility(R.id.player_next, View.VISIBLE);\n            notification.bigContentView.setViewVisibility(R.id.player_previous, View.VISIBLE);\n            notification.bigContentView.setViewVisibility(R.id.player_progress_bar, View.GONE);\n\n            boolean isPaused = PlayerManager.getInstance().isPaused();\n            notification.contentView.setViewVisibility(R.id.player_pause, isPaused ? View.GONE : View.VISIBLE);\n            notification.contentView.setViewVisibility(R.id.player_play, isPaused ? View.VISIBLE : View.GONE);\n            notification.bigContentView.setViewVisibility(R.id.player_pause, isPaused ? View.GONE : View.VISIBLE);\n            notification.bigContentView.setViewVisibility(R.id.player_play, isPaused ? View.VISIBLE : View.GONE);\n\n            notification.contentView.setTextViewText(R.id.player_song_name, title);\n            notification.contentView.setTextViewText(R.id.player_author_name, summary);\n            notification.bigContentView.setTextViewText(R.id.player_song_name, title);\n            notification.bigContentView.setTextViewText(R.id.player_author_name, summary);\n            notification.flags |= Notification.FLAG_ONGOING_EVENT;\n\n            String coverPath = Const.COVER_PATH + File.separator + testMusic.musicId + \".jpg\";\n            Bitmap bitmap = ImageUtils.getBitmap(coverPath);\n\n            if (bitmap != null) {\n                notification.contentView.setImageViewBitmap(R.id.player_album_art, bitmap);\n                notification.bigContentView.setImageViewBitmap(R.id.player_album_art, bitmap);\n            } else {\n                requestAlbumCover(testMusic.coverImg, testMusic.musicId);\n                notification.contentView.setImageViewResource(R.id.player_album_art, R.drawable.bg_album_default);\n                notification.bigContentView.setImageViewResource(R.id.player_album_art, R.drawable.bg_album_default);\n            }\n\n            startForeground(5, notification);\n\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n    }\n\n    @SuppressLint(\"UnspecifiedImmutableFlag\")\n    public void setListeners(RemoteViews view) {\n        int flags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S\n            ? PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE\n            : PendingIntent.FLAG_UPDATE_CURRENT;\n        try {\n            PendingIntent pendingIntent = PendingIntent.getBroadcast(getApplicationContext(),\n                0, new Intent(NOTIFY_PREVIOUS).setPackage(getPackageName()), flags);\n            view.setOnClickPendingIntent(R.id.player_previous, pendingIntent);\n            pendingIntent = PendingIntent.getBroadcast(getApplicationContext(),\n                0, new Intent(NOTIFY_CLOSE).setPackage(getPackageName()), flags);\n            view.setOnClickPendingIntent(R.id.player_close, pendingIntent);\n            pendingIntent = PendingIntent.getBroadcast(getApplicationContext(),\n                0, new Intent(NOTIFY_PAUSE).setPackage(getPackageName()), flags);\n            view.setOnClickPendingIntent(R.id.player_pause, pendingIntent);\n            pendingIntent = PendingIntent.getBroadcast(getApplicationContext(),\n                0, new Intent(NOTIFY_NEXT).setPackage(getPackageName()), flags);\n            view.setOnClickPendingIntent(R.id.player_next, pendingIntent);\n            pendingIntent = PendingIntent.getBroadcast(getApplicationContext(),\n                0, new Intent(NOTIFY_PLAY).setPackage(getPackageName()), flags);\n            view.setOnClickPendingIntent(R.id.player_play, pendingIntent);\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n    }\n\n    private void requestAlbumCover(String coverUrl, String musicId) {\n        if (mDownloadUseCase == null) {\n            mDownloadUseCase = new DownloadUseCase();\n        }\n\n        UseCaseHandler.getInstance().execute(mDownloadUseCase,\n            new DownloadUseCase.RequestValues(coverUrl, musicId + \".jpg\"),\n            response -> startService(new Intent(getApplicationContext(), PlayerService.class)));\n    }\n\n    @Override\n    public void onDestroy() {\n        super.onDestroy();\n    }\n\n    @Nullable\n    @Override\n    public IBinder onBind(Intent intent) {\n        return null;\n    }\n\n\n}\n"
  },
  {
    "path": "app/src/main/res/anim/h_fragment_enter.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:duration=\"150\">\n    <translate\n        android:fromXDelta=\"10%p\"\n        android:interpolator=\"@android:anim/decelerate_interpolator\"\n        android:toXDelta=\"0\" />\n\n    <alpha\n        android:fromAlpha=\"0\"\n        android:toAlpha=\"1.0\" />\n</set>"
  },
  {
    "path": "app/src/main/res/anim/h_fragment_exit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:duration=\"200\">\n    <translate\n        android:fromXDelta=\"0\"\n        android:interpolator=\"@android:anim/accelerate_interpolator\"\n        android:toXDelta=\"-10%p\" />\n</set>"
  },
  {
    "path": "app/src/main/res/anim/h_fragment_pop_enter.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:duration=\"200\">\n    <translate\n        android:fromXDelta=\"-10%p\"\n        android:toXDelta=\"0\" />\n\n</set>"
  },
  {
    "path": "app/src/main/res/anim/h_fragment_pop_exit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:duration=\"200\">\n    <translate\n        android:fromXDelta=\"0\"\n        android:toXDelta=\"10%p\" />\n\n    <alpha\n        android:fromAlpha=\"1.0\"\n        android:toAlpha=\"0\" />\n</set>"
  },
  {
    "path": "app/src/main/res/drawable/bar_selector_white.xml",
    "content": "<!--\n  ~ Copyright 2018-present KunMinX\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\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:state_pressed=\"true\">\n        <shape android:shape=\"rectangle\">\n            <solid android:color=\"#40ffffff\" />\n        </shape>\n    </item>\n    <item android:state_focused=\"true\">\n        <shape android:shape=\"rectangle\">\n            <solid android:color=\"#40ffffff\" />\n        </shape>\n    </item>\n    <item android:state_selected=\"true\">\n        <shape android:shape=\"rectangle\">\n            <solid android:color=\"#40ffffff\" />\n        </shape>\n    </item>\n    <item android:drawable=\"@color/transparent\" />\n</selector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_menu_black_48dp.xml",
    "content": "<vector android:height=\"48dp\"\n    android:tint=\"#666666\"\n    android:viewportHeight=\"24.0\"\n    android:viewportWidth=\"24.0\"\n    android:width=\"48dp\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_music_note_black_48dp.xml",
    "content": "<vector android:height=\"48dp\"\n    android:tint=\"#666666\"\n    android:viewportHeight=\"24.0\"\n    android:viewportWidth=\"24.0\"\n    android:width=\"48dp\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M12,3v10.55c-0.59,-0.34 -1.27,-0.55 -2,-0.55 -2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4V7h4V3h-6z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_search_black_48dp.xml",
    "content": "<vector android:height=\"48dp\"\n    android:tint=\"#666666\"\n    android:viewportHeight=\"24.0\"\n    android:viewportWidth=\"24.0\"\n    android:width=\"48dp\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/loading_animation.xml",
    "content": "<rotate xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:drawable=\"@drawable/ic_progress\"\n    android:fromDegrees=\"0\"\n    android:pivotX=\"50%\"\n    android:pivotY=\"50%\"\n    android:toDegrees=\"360\" />"
  },
  {
    "path": "app/src/main/res/drawable/progressbar_color.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:id=\"@+id/progress\">\n        <scale\n            android:scaleWidth=\"100%\"\n            android:scaleGravity=\"left\">\n            <shape>\n                <corners android:radius=\"0.0dip\" />\n                <gradient\n                    android:angle=\"270.0\"\n                    android:centerColor=\"#ffaaaaaa\"\n                    android:centerY=\"0.75\"\n                    android:endColor=\"#ffaaaaaa\"\n                    android:startColor=\"#ffaaaaaa\" />\n            </shape>\n        </scale>\n    </item>\n    <item>\n        <scale\n            android:scaleWidth=\"100%\"\n            android:scaleGravity=\"fill_horizontal\">\n            <shape>\n                <corners android:radius=\"0.0dip\" />\n                <gradient\n                    android:angle=\"270.0\"\n                    android:centerColor=\"#10000000\"\n                    android:centerY=\"0.75\"\n                    android:endColor=\"#10000000\"\n                    android:startColor=\"#10000000\" />\n            </shape>\n        </scale>\n    </item>\n\n</layer-list>"
  },
  {
    "path": "app/src/main/res/layout/activity_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:sothree=\"http://schemas.android.com/apk/res-auto\">\n\n    <data>\n\n        <variable\n            name=\"vm\"\n            type=\"com.kunminx.puremusic.MainActivity.MainActivityStates\" />\n\n        <variable\n            name=\"listener\"\n            type=\"com.kunminx.puremusic.MainActivity.ListenerHandler\" />\n\n    </data>\n\n    <androidx.drawerlayout.widget.DrawerLayout\n        android:id=\"@+id/dl\"\n        allowDrawerOpen=\"@{vm.allowDrawerOpen}\"\n        bindDrawerListener=\"@{listener}\"\n        isOpenDrawer=\"@{vm.openDrawer}\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <com.sothree.slidinguppanel.SlidingUpPanelLayout\n            android:id=\"@+id/sliding_layout\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:gravity=\"bottom\"\n            sothree:umanoDragView=\"@+id/slide_fragment_host\"\n            sothree:umanoOverlay=\"false\"\n            sothree:umanoPanelHeight=\"@dimen/sliding_up_header\"\n            sothree:umanoShadowHeight=\"5dp\">\n\n            <fragment\n                android:id=\"@+id/main_fragment_host\"\n                android:name=\"androidx.navigation.fragment.NavHostFragment\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:fitsSystemWindows=\"true\"\n                app:defaultNavHost=\"true\"\n                app:navGraph=\"@navigation/nav_main\" />\n\n            <fragment\n                android:id=\"@+id/slide_fragment_host\"\n                android:name=\"androidx.navigation.fragment.NavHostFragment\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:fitsSystemWindows=\"true\"\n                app:defaultNavHost=\"true\"\n                app:navGraph=\"@navigation/nav_slide\" />\n\n        </com.sothree.slidinguppanel.SlidingUpPanelLayout>\n\n        <fragment\n            android:id=\"@+id/drawer_fragment_host\"\n            android:name=\"androidx.navigation.fragment.NavHostFragment\"\n            android:layout_width=\"330dp\"\n            android:layout_height=\"match_parent\"\n            android:layout_gravity=\"start\"\n            android:fitsSystemWindows=\"true\"\n            app:defaultNavHost=\"true\"\n            app:navGraph=\"@navigation/nav_drawer\" />\n\n    </androidx.drawerlayout.widget.DrawerLayout>\n\n</layout>\n"
  },
  {
    "path": "app/src/main/res/layout/adapter_library.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018-present KunMinX\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\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <data>\n\n        <variable\n            name=\"info\"\n            type=\"com.kunminx.puremusic.data.bean.LibraryInfo\" />\n\n    </data>\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"?attr/selectableItemBackground\"\n        android:orientation=\"vertical\">\n\n        <TextView\n            android:id=\"@+id/tv_title\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginStart=\"16dp\"\n            android:layout_marginTop=\"12dp\"\n            android:text=\"@{info.title}\"\n            android:textColor=\"@color/black\"\n            android:textSize=\"18sp\"\n            android:textStyle=\"bold\"\n            tools:text=\"@string/project_title\" />\n\n        <TextView\n            android:id=\"@+id/tv_summary\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginStart=\"16dp\"\n            android:layout_marginTop=\"4dp\"\n            android:layout_marginEnd=\"16dp\"\n            android:layout_marginBottom=\"12dp\"\n            android:text=\"@{info.summary}\"\n            android:textColor=\"@color/light_gray\"\n            android:textSize=\"13sp\"\n            tools:text=\"@string/app_summary\" />\n\n    </LinearLayout>\n</layout>"
  },
  {
    "path": "app/src/main/res/layout/adapter_play_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018-present KunMinX\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\n<layout 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\n    <data>\n\n        <variable\n            name=\"album\"\n            type=\"com.kunminx.puremusic.data.bean.TestAlbum.TestMusic\" />\n\n    </data>\n\n    <androidx.constraintlayout.widget.ConstraintLayout\n        android:id=\"@+id/root_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"72dp\"\n        android:orientation=\"vertical\"\n        tools:background=\"@color/light_gray\">\n\n        <androidx.appcompat.widget.AppCompatImageView\n            android:id=\"@+id/iv_cover\"\n            imageUrl=\"@{album.coverImg}\"\n            android:layout_width=\"56dp\"\n            android:layout_height=\"56dp\"\n            android:layout_marginStart=\"12dp\"\n            android:scaleType=\"centerCrop\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\"\n            tools:src=\"@drawable/bg_home\" />\n\n        <LinearLayout\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"vertical\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            app:layout_constraintLeft_toRightOf=\"@+id/iv_cover\"\n            app:layout_constraintTop_toTopOf=\"parent\">\n\n            <TextView\n                android:id=\"@+id/tv_title\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginStart=\"12dp\"\n                android:text=\"@{album.title}\"\n                android:textSize=\"18sp\"\n                tools:text=\"title\" />\n\n            <TextView\n                android:id=\"@+id/tv_artist\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginStart=\"12dp\"\n                android:layout_marginTop=\"4dp\"\n                android:text=\"@{album.artist.name}\"\n                android:textSize=\"14sp\"\n                tools:text=\"summary\" />\n\n        </LinearLayout>\n\n        <net.steamcrafted.materialiconlib.MaterialIconView\n            android:id=\"@+id/iv_play_status\"\n            android:layout_width=\"36dp\"\n            android:layout_height=\"36dp\"\n            android:layout_marginEnd=\"12dp\"\n            android:background=\"?attr/selectableItemBackgroundBorderless\"\n            android:scaleType=\"center\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\"\n            app:materialIcon=\"music_note\"\n            app:materialIconColor=\"@color/gray\"\n            app:materialIconSize=\"28dp\" />\n\n    </androidx.constraintlayout.widget.ConstraintLayout>\n</layout>"
  },
  {
    "path": "app/src/main/res/layout/fragment_drawer.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018-present KunMinX\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\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <data>\n\n        <variable\n            name=\"vm\"\n            type=\"com.kunminx.puremusic.ui.page.DrawerFragment.DrawerStates\" />\n\n        <variable\n            name=\"click\"\n            type=\"com.kunminx.puremusic.ui.page.DrawerFragment.ClickProxy\" />\n\n        <variable\n            name=\"adapter\"\n            type=\"androidx.recyclerview.widget.RecyclerView.Adapter\" />\n    </data>\n\n    <androidx.constraintlayout.widget.ConstraintLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:background=\"@color/white\">\n\n        <androidx.appcompat.widget.AppCompatImageView\n            android:id=\"@+id/iv_logo\"\n            android:layout_width=\"100dp\"\n            android:layout_height=\"100dp\"\n            android:layout_marginTop=\"40dp\"\n            android:onClick=\"@{()->click.logoClick()}\"\n            android:src=\"@drawable/ic_launcher\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\" />\n\n        <TextView\n            android:id=\"@+id/tv_app\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"16dp\"\n            android:background=\"?attr/selectableItemBackground\"\n            android:onClick=\"@{()->click.logoClick()}\"\n            android:text=\"@string/app_name\"\n            android:textColor=\"@color/black\"\n            android:textSize=\"20sp\"\n            android:textStyle=\"bold\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@+id/iv_logo\" />\n\n        <TextView\n            android:id=\"@+id/tv_summary\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"16dp\"\n            android:background=\"?attr/selectableItemBackground\"\n            android:onClick=\"@{()->click.logoClick()}\"\n            android:text=\"@string/app_summary\"\n            android:textColor=\"@color/light_gray\"\n            android:textSize=\"12sp\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@+id/tv_app\" />\n\n        <!-- TODO 以下 adapter 和 sumbitList 属性皆乃 BindingAdapter 中定义的属性，\n            旨在将 ViewModel 中的数据绑定到 BindingAdapter，\n            以便可以间接通知布局中存在的视图实例，避免空指针安全问题，\n            如果这样说还不理解的话，可参考 DataBinding 篇的解析\n            https://xiaozhuanlan.com/topic/9816742350 -->\n\n        <!-- TODO 该 BindingAdapter 现已抽到 Strict-DataBinding 开源库中独立维护\n        可在本项目中搜索 RecyclerViewBindingAdapter 找到-->\n\n        <androidx.recyclerview.widget.RecyclerView\n            android:id=\"@+id/rv\"\n            adapter=\"@{adapter}\"\n            submitList=\"@{vm.list}\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"0dp\"\n            android:layout_marginTop=\"24dp\"\n            app:layoutManager=\"com.kunminx.binding_recyclerview.layout_manager.WrapContentLinearLayoutManager\"\n            app:layout_constraintBottom_toTopOf=\"@+id/tv_copyright\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@+id/tv_summary\" />\n\n        <TextView\n            android:id=\"@+id/tv_copyright\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"48dp\"\n            android:background=\"?attr/selectableItemBackground\"\n            android:gravity=\"center\"\n            android:onClick=\"@{()->click.logoClick()}\"\n            android:text=\"@string/Copyright\"\n            android:textColor=\"@color/light_gray\"\n            android:textSize=\"12sp\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@+id/rv\" />\n\n    </androidx.constraintlayout.widget.ConstraintLayout>\n</layout>\n"
  },
  {
    "path": "app/src/main/res/layout/fragment_login.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout 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    tools:ignore=\"RtlSymmetry\">\n\n    <data>\n\n        <variable\n            name=\"vm\"\n            type=\"com.kunminx.puremusic.ui.page.LoginFragment.LoginStates\" />\n\n        <variable\n            name=\"click\"\n            type=\"com.kunminx.puremusic.ui.page.LoginFragment.ClickProxy\" />\n\n    </data>\n\n    <androidx.constraintlayout.widget.ConstraintLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:background=\"@color/white\">\n\n        <net.steamcrafted.materialiconlib.MaterialIconView\n            android:id=\"@+id/btn_back\"\n            android:layout_width=\"24dp\"\n            android:layout_height=\"24dp\"\n            android:layout_marginStart=\"16dp\"\n            android:layout_marginTop=\"48dp\"\n            android:background=\"?attr/selectableItemBackgroundBorderless\"\n            android:onClick=\"@{()->click.back()}\"\n            android:scaleType=\"center\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\"\n            app:materialIcon=\"arrow_left\"\n            app:materialIconColor=\"@color/gray\"\n            app:materialIconSize=\"28dp\" />\n\n        <TextView\n            android:id=\"@+id/tv_title\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginStart=\"12dp\"\n            android:layout_marginTop=\"120dp\"\n            android:gravity=\"center\"\n            android:text=\"@string/login_title\"\n            android:textColor=\"@color/black\"\n            android:textSize=\"20sp\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\" />\n\n        <TextView\n            android:id=\"@+id/tv_content\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginStart=\"12dp\"\n            android:layout_marginTop=\"8dp\"\n            android:gravity=\"center\"\n            android:text=\"@string/login_content\"\n            android:textColor=\"@color/black\"\n            android:textSize=\"12sp\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@+id/tv_title\" />\n\n        <androidx.appcompat.widget.AppCompatEditText\n            android:id=\"@+id/et_name\"\n            drawable_radius=\"@{12}\"\n            drawable_strokeColor=\"@{0xffeeeeee}\"\n            drawable_strokeWidth=\"@{1}\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginStart=\"24dp\"\n            android:layout_marginTop=\"56dp\"\n            android:layout_marginEnd=\"24dp\"\n            android:hint=\"@string/user_name\"\n            android:inputType=\"text\"\n            android:paddingStart=\"12dp\"\n            android:singleLine=\"true\"\n            android:text=\"@={vm.name}\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@+id/tv_content\" />\n\n        <androidx.appcompat.widget.AppCompatEditText\n            android:id=\"@+id/et_pwd\"\n            drawable_radius=\"@{12}\"\n            drawable_strokeColor=\"@{0xffeeeeee}\"\n            drawable_strokeWidth=\"@{1}\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginStart=\"24dp\"\n            android:layout_marginTop=\"24dp\"\n            android:layout_marginEnd=\"24dp\"\n            android:hint=\"@string/user_password\"\n            android:inputType=\"textPassword\"\n            android:paddingStart=\"12dp\"\n            android:singleLine=\"true\"\n            android:text=\"@={vm.password}\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@+id/et_name\" />\n\n        <ProgressBar\n            android:id=\"@+id/progress\"\n            style=\"@style/Widget.AppCompat.ProgressBar.Horizontal\"\n            visible=\"@{vm.loadingVisible}\"\n            android:layout_width=\"160dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_margin=\"24dp\"\n            android:indeterminate=\"true\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@+id/et_pwd\" />\n\n        <Button\n            android:id=\"@+id/btn_login\"\n            drawable_radius=\"@{25}\"\n            drawable_solidColor=\"@{0xffFF7055}\"\n            android:layout_width=\"200dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"24dp\"\n            android:onClick=\"@{()->click.login()}\"\n            android:text=\"@string/login\"\n            android:textColor=\"@color/white\"\n            android:textSize=\"18sp\"\n            android:textStyle=\"bold\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@+id/progress\"\n            app:layout_goneMarginTop=\"72dp\" />\n\n    </androidx.constraintlayout.widget.ConstraintLayout>\n</layout>\n"
  },
  {
    "path": "app/src/main/res/layout/fragment_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018-present KunMinX\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\n<layout 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\n    <data>\n\n        <variable\n            name=\"click\"\n            type=\"com.kunminx.puremusic.ui.page.MainFragment.ClickProxy\" />\n\n        <variable\n            name=\"vm\"\n            type=\"com.kunminx.puremusic.ui.page.MainFragment.MainStates\" />\n\n        <variable\n            name=\"adapter\"\n            type=\"androidx.recyclerview.widget.RecyclerView.Adapter\" />\n\n    </data>\n\n    <androidx.coordinatorlayout.widget.CoordinatorLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:overScrollMode=\"never\">\n\n        <com.google.android.material.appbar.AppBarLayout\n            android:id=\"@+id/appbar_layout\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:fitsSystemWindows=\"true\"\n            android:theme=\"@style/AppTheme\">\n\n            <com.google.android.material.appbar.CollapsingToolbarLayout\n                android:id=\"@+id/collapse_layout\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"275dp\"\n                android:fitsSystemWindows=\"true\"\n                app:contentScrim=\"@color/transparent\"\n                app:layout_scrollFlags=\"scroll|exitUntilCollapsed\">\n\n                <androidx.appcompat.widget.AppCompatImageView\n                    android:id=\"@+id/iv_bg\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"match_parent\"\n                    android:fitsSystemWindows=\"true\"\n                    android:scaleType=\"centerCrop\"\n                    android:src=\"@drawable/bg_home\"\n                    app:layout_collapseMode=\"parallax\" />\n\n                <androidx.constraintlayout.widget.ConstraintLayout\n                    android:id=\"@+id/toolbar\"\n                    drawable_radius=\"@{8}\"\n                    drawable_solidColor=\"@{0x88ffffff}\"\n                    drawable_strokeColor=\"@{0x33666666}\"\n                    drawable_strokeWidth=\"@{1}\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"48dp\"\n                    android:layout_marginStart=\"12dp\"\n                    android:layout_marginTop=\"37dp\"\n                    android:layout_marginEnd=\"12dp\"\n                    android:layout_marginBottom=\"12dp\">\n\n                    <androidx.appcompat.widget.AppCompatImageView\n                        android:id=\"@+id/iv_menu\"\n                        android:layout_width=\"24dp\"\n                        android:layout_height=\"24dp\"\n                        android:layout_marginStart=\"12dp\"\n                        android:background=\"?attr/selectableItemBackgroundBorderless\"\n                        android:onClick=\"@{()->click.openMenu()}\"\n                        android:scaleType=\"centerInside\"\n                        android:src=\"@drawable/ic_menu_black_48dp\"\n                        app:layout_constraintBottom_toBottomOf=\"parent\"\n                        app:layout_constraintLeft_toLeftOf=\"parent\"\n                        app:layout_constraintTop_toTopOf=\"parent\" />\n\n                    <androidx.appcompat.widget.AppCompatImageView\n                        android:id=\"@+id/iv_icon\"\n                        onClickWithDebouncing=\"@{()->click.login()}\"\n                        android:layout_width=\"24dp\"\n                        android:layout_height=\"24dp\"\n                        android:background=\"?attr/selectableItemBackgroundBorderless\"\n                        android:scaleType=\"centerInside\"\n                        android:src=\"@drawable/ic_music_note_black_48dp\"\n                        app:layout_constraintBottom_toBottomOf=\"parent\"\n                        app:layout_constraintRight_toLeftOf=\"@+id/tv_app\"\n                        app:layout_constraintTop_toTopOf=\"parent\" />\n\n                    <TextView\n                        android:id=\"@+id/tv_app\"\n                        onClickWithDebouncing=\"@{()->click.login()}\"\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        android:text=\"@string/app_name\"\n                        android:textSize=\"18sp\"\n                        android:textStyle=\"bold\"\n                        app:layout_constraintBottom_toBottomOf=\"parent\"\n                        app:layout_constraintLeft_toLeftOf=\"parent\"\n                        app:layout_constraintRight_toRightOf=\"parent\"\n                        app:layout_constraintTop_toTopOf=\"parent\" />\n\n                    <androidx.appcompat.widget.AppCompatImageView\n                        android:id=\"@+id/iv_search\"\n                        onClickWithDebouncing=\"@{()->click.search()}\"\n                        android:layout_width=\"24dp\"\n                        android:layout_height=\"24dp\"\n                        android:layout_marginEnd=\"12dp\"\n                        android:background=\"?attr/selectableItemBackgroundBorderless\"\n                        android:scaleType=\"centerInside\"\n                        android:src=\"@drawable/ic_search_black_48dp\"\n                        app:layout_constraintBottom_toBottomOf=\"parent\"\n                        app:layout_constraintRight_toRightOf=\"parent\"\n                        app:layout_constraintTop_toTopOf=\"parent\" />\n\n                </androidx.constraintlayout.widget.ConstraintLayout>\n\n            </com.google.android.material.appbar.CollapsingToolbarLayout>\n\n            <!-- TODO 建议不要使用如下 TabLayout 和 ViewPager 的嵌套式语法糖\n            此处只是为了便于展示 BindingAdapter 的业务能力，而使用的语法糖，\n            在实际开发中，BindingAdapter 的通知时机和 TabLayout 的某些\n            机制并不完美配合，导致可能出现 TabLayout 和 ViewPager 联动失效、\n            ViewPager child 不显示内容等问题 -->\n\n            <com.google.android.material.tabs.TabLayout\n                android:id=\"@+id/tab_layout\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                app:tabBackground=\"@color/white\"\n                app:tabIndicatorColor=\"@color/gray\"\n                app:tabIndicatorHeight=\"4dp\"\n                app:tabSelectedTextColor=\"@color/gray\"\n                app:tabTextColor=\"@color/light_gray\">\n\n                <com.google.android.material.tabs.TabItem\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"@string/recently\" />\n\n                <com.google.android.material.tabs.TabItem\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"@string/best_practice\" />\n\n            </com.google.android.material.tabs.TabLayout>\n\n        </com.google.android.material.appbar.AppBarLayout>\n\n        <!-- TODO 建议不要使用如下 TabLayout 和 ViewPager 的嵌套式语法糖\n           此处只是为了便于展示 BindingAdapter 的业务能力，而使用的语法糖，\n           在实际开发中，BindingAdapter 的通知时机和 TabLayout 的某些\n           机制并不完美配合，导致可能出现 TabLayout 和 ViewPager 联动失效、\n           ViewPager child 不显示内容等问题 -->\n\n        <androidx.viewpager.widget.ViewPager\n            android:id=\"@+id/view_pager\"\n            initTabAndPage=\"@{true}\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:overScrollMode=\"never\"\n            app:layout_behavior=\"@string/appbar_scrolling_view_behavior\">\n\n            <!-- TODO 以下 adapter 和 sumbitList 属性皆乃 BindingAdapter 中定义的属性，\n            旨在将 ViewModel 中的数据绑定到 BindingAdapter，\n            以便可以间接通知布局中存在的视图实例，避免空指针安全问题，\n            如果这样说还不理解的话，可参考 DataBinding 篇的解析\n            https://xiaozhuanlan.com/topic/9816742350 -->\n\n            <!-- TODO 该 BindingAdapter 现已抽到 Strict-DataBinding 开源库中独立维护\n            可在本项目中搜索 RecyclerViewBindingAdapter 找到-->\n\n            <androidx.recyclerview.widget.RecyclerView\n                android:id=\"@+id/rv\"\n                adapter=\"@{adapter}\"\n                submitList=\"@{vm.list}\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:clipToPadding=\"false\"\n                app:layoutManager=\"com.kunminx.binding_recyclerview.layout_manager.WrapContentLinearLayoutManager\"\n                tools:listitem=\"@layout/adapter_play_item\" />\n\n            <androidx.core.widget.NestedScrollView\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\">\n\n                <WebView\n                    android:id=\"@+id/web_view\"\n                    pageAssetPath=\"@{vm.pageAssetPath}\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"match_parent\"\n                    android:clipToPadding=\"false\" />\n\n            </androidx.core.widget.NestedScrollView>\n\n        </androidx.viewpager.widget.ViewPager>\n\n    </androidx.coordinatorlayout.widget.CoordinatorLayout>\n</layout>\n"
  },
  {
    "path": "app/src/main/res/layout/fragment_player.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018-present KunMinX\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\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:playpauseview=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <data>\n\n        <variable\n            name=\"click\"\n            type=\"com.kunminx.puremusic.ui.page.PlayerFragment.ClickProxy\" />\n\n        <variable\n            name=\"listener\"\n            type=\"com.kunminx.puremusic.ui.page.PlayerFragment.ListenerHandler\" />\n\n        <variable\n            name=\"vm\"\n            type=\"com.kunminx.puremusic.ui.page.PlayerFragment.PlayerStates\" />\n\n        <variable\n            name=\"panelVm\"\n            type=\"com.kunminx.puremusic.ui.view.PlayerSlideListener.SlideAnimatorStates\" />\n\n  </data>\n\n    <androidx.coordinatorlayout.widget.CoordinatorLayout\n        android:id=\"@+id/topContainer\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:layout_gravity=\"top\">\n\n        <androidx.appcompat.widget.AppCompatImageView\n            android:id=\"@+id/album_art\"\n            imageUrl=\"@{vm.coverImg}\"\n            placeHolder=\"@{vm.placeHolder}\"\n            size=\"@{panelVm.albumArtSize}\"\n            android:layout_width=\"@dimen/sliding_up_header\"\n            android:layout_height=\"@dimen/sliding_up_header\"\n            android:scaleType=\"centerCrop\"\n            android:src=\"@drawable/bg_album_default\" />\n\n        <RelativeLayout\n            android:id=\"@+id/custom_toolbar\"\n            invisible=\"@{panelVm.customToolbarVisibility}\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            android:layout_gravity=\"top\"\n            android:layout_marginTop=\"37dp\"\n            android:orientation=\"horizontal\">\n\n            <net.steamcrafted.materialiconlib.MaterialIconView\n                android:id=\"@+id/btn_close\"\n                android:layout_width=\"36dp\"\n                android:layout_height=\"36dp\"\n                android:layout_alignParentStart=\"true\"\n                android:layout_centerVertical=\"true\"\n                android:layout_marginStart=\"16dp\"\n                android:background=\"?attr/selectableItemBackgroundBorderless\"\n                android:onClick=\"@{()->click.slideDown()}\"\n                android:scaleType=\"center\"\n                app:materialIcon=\"chevron_down\"\n                app:materialIconColor=\"@color/white\"\n                app:materialIconSize=\"28dp\" />\n\n            <net.steamcrafted.materialiconlib.MaterialIconView\n                android:id=\"@+id/popup_menu\"\n                android:layout_width=\"36dp\"\n                android:layout_height=\"36dp\"\n                android:layout_alignParentEnd=\"true\"\n                android:layout_centerVertical=\"true\"\n                android:layout_marginEnd=\"16dp\"\n                android:background=\"?attr/selectableItemBackgroundBorderless\"\n                android:onClick=\"@{()->click.more()}\"\n                android:scaleType=\"center\"\n                app:materialIcon=\"dots_vertical\"\n                app:materialIconColor=\"@color/white\"\n                app:materialIconSize=\"28dp\" />\n\n        </RelativeLayout>\n\n        <LinearLayout\n            android:id=\"@+id/summary\"\n            transY=\"@{panelVm.summaryTranslationY}\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"@dimen/sliding_up_header\"\n            android:layout_gravity=\"top\"\n            android:layout_marginStart=\"@dimen/sliding_up_header\"\n            android:orientation=\"vertical\">\n\n      <ProgressBar\n        android:id=\"@+id/song_progress_normal\"\n        style=\"@style/Widget.AppCompat.ProgressBar.Horizontal\"\n        invisible=\"@{panelVm.songProgressNormalVisibility}\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"top\"\n        android:max=\"@{vm.maxSeekDuration}\"\n        android:minHeight=\"4dp\"\n        android:progress=\"@{vm.currentSeekPosition}\"\n        android:progressDrawable=\"@drawable/progressbar_color\"\n        android:progressTint=\"@color/transparent\" />\n\n            <TextView\n                android:id=\"@+id/title\"\n                style=\"@style/TextAppearance.AppCompat.Body1\"\n                transX=\"@{panelVm.titleTranslationX}\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginStart=\"12dp\"\n                android:layout_marginEnd=\"42dp\"\n                android:ellipsize=\"end\"\n                android:maxLines=\"1\"\n                android:text=\"@{vm.title}\"\n                android:textSize=\"16sp\"\n                tools:text=\"title\" />\n\n            <TextView\n                android:id=\"@+id/artist\"\n                style=\"@style/TextAppearance.AppCompat.Widget.ActionMode.Subtitle\"\n                transX=\"@{panelVm.artistTranslationX}\"\n                transY=\"@{panelVm.artistTranslationY}\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginStart=\"12dp\"\n                android:ellipsize=\"end\"\n                android:maxLength=\"20\"\n                android:maxLines=\"1\"\n                android:text=\"@{vm.artist}\"\n                android:textColor=\"?android:textColorSecondary\"\n                android:textSize=\"14sp\"\n                tools:text=\"artist\" />\n\n        </LinearLayout>\n\n        <RelativeLayout\n            android:id=\"@+id/icon_container\"\n            y=\"@{panelVm.iconContainerY}\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"top\"\n            android:layout_marginTop=\"10dp\">\n\n            <net.steamcrafted.materialiconlib.MaterialIconView\n                android:id=\"@+id/next\"\n                x=\"@{panelVm.nextX}\"\n                android:layout_width=\"36dp\"\n                android:layout_height=\"36dp\"\n                android:layout_alignParentEnd=\"true\"\n                android:layout_marginEnd=\"16dp\"\n                android:background=\"?attr/selectableItemBackgroundBorderless\"\n                android:onClick=\"@{()->click.next()}\"\n                android:scaleType=\"center\"\n                app:materialIcon=\"skip_next\"\n                app:materialIconColor=\"@android:color/black\"\n                app:materialIconSize=\"28dp\" />\n\n            <com.kunminx.puremusic.ui.view.PlayPauseView\n                android:id=\"@+id/play_pause\"\n                isPlaying=\"@{vm.isPlaying}\"\n                onClickWithDebouncing=\"@{()->click.togglePlay()}\"\n                x=\"@{panelVm.playPauseX}\"\n                android:layout_width=\"36dp\"\n                android:layout_height=\"36dp\"\n                android:layout_marginEnd=\"16dp\"\n                android:layout_toStartOf=\"@id/next\"\n                android:clickable=\"true\"\n                android:focusable=\"true\"\n                android:foreground=\"?attr/selectableItemBackground\"\n                app:circleAlpha=\"@{panelVm.playCircleAlpha}\"\n                app:drawableColor=\"@{panelVm.playPauseDrawableColor}\"\n                playpauseview:circleAlpha=\"0\"\n                playpauseview:isCircleDraw=\"true\" />\n\n            <net.steamcrafted.materialiconlib.MaterialIconView\n                android:id=\"@+id/previous\"\n                alpha=\"@{panelVm.previousAlpha}\"\n                visible=\"@{panelVm.previousVisibility}\"\n                x=\"@{panelVm.previousX}\"\n                android:layout_width=\"36dp\"\n                android:layout_height=\"36dp\"\n                android:layout_alignParentTop=\"true\"\n                android:layout_marginEnd=\"16dp\"\n                android:layout_toStartOf=\"@+id/play_pause\"\n                android:alpha=\"0\"\n                android:background=\"?attr/selectableItemBackgroundBorderless\"\n                android:onClick=\"@{()->click.previous()}\"\n                android:scaleType=\"center\"\n                app:materialIcon=\"skip_previous\"\n                app:materialIconColor=\"@android:color/black\"\n                app:materialIconSize=\"28dp\" />\n\n            <net.steamcrafted.materialiconlib.MaterialIconView\n                android:id=\"@+id/mode\"\n                alpha=\"@{panelVm.modeAlpha}\"\n                mdIcon=\"@{vm.playModeIcon}\"\n                visible=\"@{panelVm.modeVisibility}\"\n                x=\"@{panelVm.modeX}\"\n                android:layout_width=\"36dp\"\n                android:layout_height=\"36dp\"\n                android:layout_marginEnd=\"16dp\"\n                android:layout_toStartOf=\"@id/previous\"\n                android:alpha=\"0\"\n                android:background=\"?attr/selectableItemBackgroundBorderless\"\n                android:onClick=\"@{()->click.playMode()}\"\n                android:scaleType=\"center\"\n                app:materialIconColor=\"@android:color/black\"\n                app:materialIconSize=\"28dp\" />\n\n            <net.steamcrafted.materialiconlib.MaterialIconView\n                android:id=\"@+id/ic_play_list\"\n                x=\"@{panelVm.icPlayListX}\"\n                android:layout_width=\"36dp\"\n                android:layout_height=\"36dp\"\n                android:layout_marginStart=\"16dp\"\n                android:layout_toEndOf=\"@id/next\"\n                android:background=\"?attr/selectableItemBackgroundBorderless\"\n                android:onClick=\"@{()->click.showPlayList()}\"\n                android:scaleType=\"center\"\n                app:materialIcon=\"playlist_play\"\n                app:materialIconColor=\"@android:color/black\"\n                app:materialIconSize=\"28dp\" />\n\n        </RelativeLayout>\n\n    <SeekBar\n      android:id=\"@+id/seek_bottom\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"wrap_content\"\n      android:layout_gravity=\"bottom\"\n      android:background=\"@color/transparent\"\n      android:clickable=\"true\"\n      android:focusable=\"true\"\n      android:max=\"@{vm.maxSeekDuration}\"\n      android:minHeight=\"6dp\"\n      android:paddingTop=\"24dp\"\n      android:progress=\"@{vm.currentSeekPosition}\"\n      android:progressDrawable=\"@drawable/progressbar_color\"\n      android:thumb=\"@null\"\n      app:onSeekBarChangeListener=\"@{listener}\" />\n\n    </androidx.coordinatorlayout.widget.CoordinatorLayout>\n</layout>\n"
  },
  {
    "path": "app/src/main/res/layout/fragment_search.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018-present KunMinX\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\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <data>\n\n        <variable\n            name=\"click\"\n            type=\"com.kunminx.puremusic.ui.page.SearchFragment.ClickProxy\" />\n\n        <variable\n            name=\"vm\"\n            type=\"com.kunminx.puremusic.ui.page.SearchFragment.SearchStates\" />\n    </data>\n\n    <androidx.constraintlayout.widget.ConstraintLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:background=\"@color/white\">\n\n        <net.steamcrafted.materialiconlib.MaterialIconView\n            android:id=\"@+id/btn_back\"\n            android:layout_width=\"36dp\"\n            android:layout_height=\"36dp\"\n            android:layout_marginStart=\"12dp\"\n            android:layout_marginTop=\"37dp\"\n            android:background=\"?attr/selectableItemBackgroundBorderless\"\n            android:onClick=\"@{()->click.back()}\"\n            android:scaleType=\"center\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\"\n            app:materialIcon=\"arrow_left\"\n            app:materialIconColor=\"@color/gray\"\n            app:materialIconSize=\"28dp\" />\n\n        <TextView\n            android:id=\"@+id/tv_title\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginStart=\"12dp\"\n            android:layout_marginTop=\"37dp\"\n            android:background=\"?attr/selectableItemBackgroundBorderless\"\n            android:text=\"@string/relearn_android\"\n            android:textSize=\"18sp\"\n            android:textStyle=\"bold\"\n            app:layout_constraintBottom_toTopOf=\"@+id/tv_content\"\n            app:layout_constraintLeft_toRightOf=\"@+id/btn_back\"\n            app:layout_constraintTop_toTopOf=\"parent\" />\n\n        <TextView\n            android:id=\"@+id/tv_content\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginStart=\"18dp\"\n            android:layout_marginBottom=\"4dp\"\n            android:background=\"?attr/selectableItemBackgroundBorderless\"\n            android:text=\"@string/learn_more_friends\"\n            android:textColor=\"@color/light_gray\"\n            android:textSize=\"12sp\"\n            app:layout_constraintLeft_toRightOf=\"@+id/btn_back\"\n            app:layout_constraintTop_toBottomOf=\"@+id/tv_title\" />\n\n        <TextView\n            android:id=\"@+id/btn_subsribe\"\n            drawable_radius=\"@{25}\"\n            drawable_solidColor=\"@{0xffFF7055}\"\n            android:layout_width=\"100dp\"\n            android:layout_height=\"32dp\"\n            android:layout_marginTop=\"37dp\"\n            android:layout_marginEnd=\"20dp\"\n            android:background=\"?attr/selectableItemBackgroundBorderless\"\n            android:gravity=\"center\"\n            android:onClick=\"@{()->click.subscribe()}\"\n            android:text=\"@string/learn_more\"\n            android:textColor=\"@color/white\"\n            android:textSize=\"13sp\"\n            android:textStyle=\"bold\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\" />\n\n        <net.steamcrafted.materialiconlib.MaterialIconView\n            android:id=\"@+id/ic\"\n            android:layout_width=\"242dp\"\n            android:layout_height=\"242dp\"\n            android:layout_marginTop=\"24dp\"\n            android:background=\"?attr/selectableItemBackgroundBorderless\"\n            android:onClick=\"@{()->click.testNav()}\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@+id/tv_content\"\n            app:materialIcon=\"magnify\"\n            app:materialIconColor=\"@color/light_gray\"\n            app:materialIconSize=\"28dp\" />\n\n        <TextView\n            android:id=\"@+id/tv_tip\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:background=\"?attr/selectableItemBackground\"\n            android:onClick=\"@{()->click.testNav()}\"\n            android:text=\"@string/search_page_tip\"\n            android:textSize=\"16sp\"\n            android:visibility=\"gone\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@+id/ic\" />\n\n        <TextView\n            android:id=\"@+id/tv_test_download\"\n            drawable_enabled=\"@{0xffFF7055}\"\n            drawable_solidColor=\"@{0xffFFA07A}\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"32dp\"\n            android:background=\"?attr/selectableItemBackground\"\n            android:enabled=\"@{vm.enableGlobalDownload}\"\n            android:gravity=\"center\"\n            android:onClick=\"@{()->click.testDownload()}\"\n            android:paddingLeft=\"12dp\"\n            android:paddingRight=\"12dp\"\n            android:text=\"@string/test_download\"\n            android:textColor=\"@color/white\"\n            android:textSize=\"13sp\"\n            android:textStyle=\"bold\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@+id/tv_tip\" />\n\n        <TextView\n            android:id=\"@+id/tv_test_lifecycle_download\"\n            drawable_enabled=\"@{0xffFF7055}\"\n            drawable_solidColor=\"@{0xffFFA07A}\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"32dp\"\n            android:layout_marginTop=\"12dp\"\n            android:background=\"?attr/selectableItemBackground\"\n            android:enabled=\"@{vm.enableDownload}\"\n            android:gravity=\"center\"\n            android:onClick=\"@{()->click.testLifecycleDownload()}\"\n            android:paddingLeft=\"12dp\"\n            android:paddingRight=\"12dp\"\n            android:text=\"@string/test_lifecycle_download\"\n            android:textColor=\"@color/white\"\n            android:textSize=\"13sp\"\n            android:textStyle=\"bold\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@+id/tv_test_download\" />\n\n        <SeekBar\n            android:id=\"@+id/pb\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"20dp\"\n            android:layout_marginLeft=\"16dp\"\n            android:layout_marginTop=\"16dp\"\n            android:layout_marginRight=\"16dp\"\n            android:background=\"@color/transparent\"\n            android:max=\"20\"\n            android:progress=\"@{vm.progress}\"\n            android:progressDrawable=\"@drawable/progressbar_color\"\n            android:thumb=\"@color/transparent\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@+id/tv_test_lifecycle_download\" />\n\n        <SeekBar\n            android:id=\"@+id/pb_cancelable\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"20dp\"\n            android:layout_marginLeft=\"16dp\"\n            android:layout_marginTop=\"16dp\"\n            android:layout_marginRight=\"16dp\"\n            android:background=\"@color/transparent\"\n            android:max=\"20\"\n            android:progress=\"@{vm.progress_cancelable}\"\n            android:progressDrawable=\"@drawable/progressbar_color\"\n            android:thumb=\"@color/transparent\"\n            app:layout_constraintLeft_toLeftOf=\"parent\"\n            app:layout_constraintRight_toRightOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@+id/pb\" />\n\n    </androidx.constraintlayout.widget.ConstraintLayout>\n</layout>\n"
  },
  {
    "path": "app/src/main/res/layout/notify_player_big.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018-present KunMinX\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\n<LinearLayout 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=\"128dp\"\n    android:background=\"@color/light_gray\"\n    android:baselineAligned=\"false\"\n    android:orientation=\"horizontal\">\n\n    <RelativeLayout\n        android:layout_width=\"128dp\"\n        android:layout_height=\"128dp\">\n\n        <ImageView\n            android:id=\"@+id/player_album_art\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:background=\"#ffc0c0c0\"\n            android:scaleType=\"fitXY\"\n            tools:ignore=\"ContentDescription\" />\n\n        <ProgressBar\n            android:id=\"@+id/player_progress_bar\"\n            android:layout_width=\"30dp\"\n            android:layout_height=\"30dp\"\n            android:layout_centerInParent=\"true\"\n            android:indeterminateDrawable=\"@drawable/loading_animation\"\n            android:indeterminateDuration=\"1500\" />\n    </RelativeLayout>\n\n    <FrameLayout\n        android:layout_width=\"0dp\"\n        android:layout_height=\"match_parent\"\n        android:layout_weight=\"1\">\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginEnd=\"44dp\"\n            android:orientation=\"vertical\">\n\n            <TextView\n                android:id=\"@+id/player_song_name\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginStart=\"12dp\"\n                android:layout_marginTop=\"4dp\"\n                android:gravity=\"top|start\"\n                android:singleLine=\"true\"\n                android:text=\"\"\n                android:textColor=\"#ffffffff\"\n                android:textSize=\"14sp\" />\n\n            <TextView\n                android:id=\"@+id/player_author_name\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginStart=\"12dp\"\n                android:layout_marginTop=\"1dp\"\n                android:singleLine=\"true\"\n                android:text=\"\"\n                android:textColor=\"@android:color/white\"\n                android:textSize=\"14sp\" />\n\n            <TextView\n                android:id=\"@+id/player_albumname\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginStart=\"12dp\"\n                android:layout_marginTop=\"1dp\"\n                android:singleLine=\"true\"\n                android:text=\"\"\n                android:textColor=\"@android:color/white\"\n                android:textSize=\"14sp\" />\n\n        </LinearLayout>\n\n        <ImageView\n            android:id=\"@+id/player_close\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"end\"\n            android:background=\"@drawable/bar_selector_white\"\n            android:padding=\"10dp\"\n            android:scaleType=\"center\"\n            android:src=\"@drawable/ic_close_white\"\n            tools:ignore=\"ContentDescription\" />\n\n        <FrameLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"0.5dp\"\n            android:layout_gravity=\"bottom\"\n            android:layout_marginBottom=\"48dp\"\n            android:background=\"@android:color/darker_gray\" />\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"48dp\"\n            android:layout_gravity=\"bottom\"\n            android:gravity=\"center\"\n            android:orientation=\"horizontal\">\n\n            <ImageView\n                android:id=\"@+id/player_previous\"\n                android:layout_width=\"40dp\"\n                android:layout_height=\"40dp\"\n                android:layout_marginLeft=\"10dp\"\n                android:layout_marginRight=\"10dp\"\n                android:background=\"@drawable/bar_selector_white\"\n                android:scaleType=\"center\"\n                android:src=\"@drawable/ic_next_dark\"\n                tools:ignore=\"ContentDescription\" />\n\n            <ImageView\n                android:id=\"@+id/player_pause\"\n                android:layout_width=\"40dp\"\n                android:layout_height=\"40dp\"\n                android:layout_marginLeft=\"10dp\"\n                android:layout_marginRight=\"10dp\"\n                android:background=\"@drawable/bar_selector_white\"\n                android:scaleType=\"center\"\n                android:src=\"@drawable/ic_action_pause\"\n                tools:ignore=\"ContentDescription\" />\n\n            <ImageView\n                android:id=\"@+id/player_play\"\n                android:layout_width=\"40dp\"\n                android:layout_height=\"40dp\"\n                android:layout_marginLeft=\"10dp\"\n                android:layout_marginRight=\"10dp\"\n                android:background=\"@drawable/bar_selector_white\"\n                android:scaleType=\"center\"\n                android:src=\"@drawable/ic_action_play\"\n                android:visibility=\"gone\"\n                tools:ignore=\"ContentDescription\" />\n\n            <ImageView\n                android:id=\"@+id/player_next\"\n                android:layout_width=\"40dp\"\n                android:layout_height=\"40dp\"\n                android:layout_marginLeft=\"10dp\"\n                android:layout_marginRight=\"10dp\"\n                android:background=\"@drawable/bar_selector_white\"\n                android:scaleType=\"center\"\n                android:src=\"@drawable/ic_previous_dark\"\n                tools:ignore=\"ContentDescription\" />\n        </LinearLayout>\n    </FrameLayout>\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/notify_player_small.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018-present KunMinX\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\n<RelativeLayout 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=\"64dp\"\n    android:background=\"@color/light_gray\">\n\n    <ImageView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:src=\"@color/transparent\"\n        tools:ignore=\"ContentDescription\" />\n\n    <ImageView\n        android:id=\"@+id/player_album_art\"\n        android:layout_width=\"40dp\"\n        android:layout_height=\"40dp\"\n        android:layout_centerVertical=\"true\"\n        android:layout_marginStart=\"12dp\"\n        android:scaleType=\"fitXY\"\n        tools:ignore=\"ContentDescription\" />\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_centerVertical=\"true\"\n        android:layout_marginEnd=\"180dp\"\n        android:orientation=\"vertical\">\n\n        <TextView\n            android:id=\"@+id/player_song_name\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginStart=\"64dp\"\n            android:ellipsize=\"end\"\n            android:gravity=\"top|start\"\n            android:singleLine=\"true\"\n            android:textColor=\"#ffffffff\"\n            android:textSize=\"16sp\" />\n\n        <TextView\n            android:id=\"@+id/player_author_name\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginStart=\"64dp\"\n            android:layout_marginTop=\"2dp\"\n            android:ellipsize=\"end\"\n            android:singleLine=\"true\"\n            android:textColor=\"@android:color/white\"\n            android:textSize=\"13sp\" />\n\n    </LinearLayout>\n\n    <ImageView\n        android:id=\"@+id/player_close\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_alignParentEnd=\"true\"\n        android:background=\"@drawable/bar_selector_white\"\n        android:padding=\"8dp\"\n        android:scaleType=\"center\"\n        android:src=\"@drawable/ic_close_white\"\n        tools:ignore=\"ContentDescription\" />\n\n    <LinearLayout\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"48dp\"\n        android:layout_alignParentEnd=\"true\"\n        android:layout_centerVertical=\"true\"\n        android:layout_marginEnd=\"40dp\"\n        android:gravity=\"center\"\n        android:orientation=\"horizontal\">\n\n        <ProgressBar\n            android:id=\"@+id/player_progress_bar\"\n            android:layout_width=\"30dp\"\n            android:layout_height=\"30dp\"\n            android:layout_gravity=\"center\"\n            android:layout_marginEnd=\"56dp\"\n            android:indeterminateDrawable=\"@drawable/loading_animation\"\n            android:indeterminateDuration=\"1500\" />\n\n        <ImageView\n            android:id=\"@+id/player_previous\"\n            android:layout_width=\"32dp\"\n            android:layout_height=\"32dp\"\n            android:layout_marginLeft=\"8dp\"\n            android:layout_marginRight=\"8dp\"\n            android:background=\"@drawable/bar_selector_white\"\n            android:scaleType=\"center\"\n            android:src=\"@drawable/ic_next_dark\"\n            tools:ignore=\"ContentDescription\" />\n\n        <ImageView\n            android:id=\"@+id/player_pause\"\n            android:layout_width=\"36dp\"\n            android:layout_height=\"36dp\"\n            android:layout_marginLeft=\"8dp\"\n            android:layout_marginRight=\"8dp\"\n            android:background=\"@drawable/bar_selector_white\"\n            android:scaleType=\"center\"\n            android:src=\"@drawable/ic_action_pause\"\n            tools:ignore=\"ContentDescription\" />\n\n        <ImageView\n            android:id=\"@+id/player_play\"\n            android:layout_width=\"36dp\"\n            android:layout_height=\"36dp\"\n            android:layout_marginLeft=\"8dp\"\n            android:layout_marginRight=\"8dp\"\n            android:background=\"@drawable/bar_selector_white\"\n            android:scaleType=\"center\"\n            android:src=\"@drawable/ic_action_play\"\n            android:visibility=\"gone\"\n            tools:ignore=\"ContentDescription\" />\n\n        <ImageView\n            android:id=\"@+id/player_next\"\n            android:layout_width=\"32dp\"\n            android:layout_height=\"32dp\"\n            android:layout_marginLeft=\"8dp\"\n            android:layout_marginRight=\"8dp\"\n            android:background=\"@drawable/bar_selector_white\"\n            android:scaleType=\"center\"\n            android:src=\"@drawable/ic_previous_dark\"\n            tools:ignore=\"ContentDescription\" />\n    </LinearLayout>\n\n</RelativeLayout>"
  },
  {
    "path": "app/src/main/res/layout-land/activity_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018-present KunMinX\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\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <data>\n\n        <variable\n            name=\"vm\"\n            type=\"com.kunminx.puremusic.MainActivity.MainActivityStates\" />\n\n    </data>\n\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:baselineAligned=\"false\"\n        android:orientation=\"horizontal\">\n\n        <fragment\n            android:id=\"@+id/main_fragment_host\"\n            android:name=\"androidx.navigation.fragment.NavHostFragment\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"match_parent\"\n            android:layout_marginTop=\"-25dp\"\n            android:layout_weight=\"1\"\n            android:fitsSystemWindows=\"true\"\n            app:defaultNavHost=\"true\"\n            app:navGraph=\"@navigation/nav_main\" />\n\n        <fragment\n            android:id=\"@+id/slide_fragment_host\"\n            android:name=\"androidx.navigation.fragment.NavHostFragment\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"match_parent\"\n            android:layout_weight=\"1\"\n            android:fitsSystemWindows=\"true\"\n            app:defaultNavHost=\"true\"\n            app:navGraph=\"@navigation/nav_slide\" />\n\n    </LinearLayout>\n</layout>\n"
  },
  {
    "path": "app/src/main/res/layout-land/fragment_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018-present KunMinX\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\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <data>\n\n        <variable\n            name=\"click\"\n            type=\"com.kunminx.puremusic.ui.page.MainFragment.ClickProxy\" />\n\n        <variable\n            name=\"vm\"\n            type=\"com.kunminx.puremusic.ui.page.MainFragment.MainStates\" />\n\n        <variable\n            name=\"adapter\"\n            type=\"androidx.recyclerview.widget.RecyclerView.Adapter\" />\n\n    </data>\n\n    <androidx.coordinatorlayout.widget.CoordinatorLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:fitsSystemWindows=\"true\"\n        android:overScrollMode=\"never\">\n\n        <com.google.android.material.appbar.AppBarLayout\n            android:id=\"@+id/appbar_layout\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:fitsSystemWindows=\"true\"\n            android:theme=\"@style/AppTheme\">\n\n            <!-- TODO 建议不要使用如下 TabLayout 和 ViewPager 的嵌套式语法糖\n           此处只是为了便于展示 BindingAdapter 的业务能力，而使用的语法糖，\n           在实际开发中，BindingAdapter 的通知时机和 TabLayout 的某些\n           机制并不完美配合，导致可能出现 TabLayout 和 ViewPager 联动失效、\n           ViewPager child 不显示内容等问题 -->\n\n            <com.google.android.material.tabs.TabLayout\n                android:id=\"@+id/tab_layout\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"48dp\"\n                android:background=\"@color/white\"\n                app:tabBackground=\"@color/white\"\n                app:tabIndicatorColor=\"@color/gray\"\n                app:tabIndicatorFullWidth=\"true\"\n                app:tabIndicatorHeight=\"4dp\"\n                app:tabSelectedTextColor=\"@color/gray\"\n                app:tabTextColor=\"@color/light_gray\">\n\n                <com.google.android.material.tabs.TabItem\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"@string/recently\" />\n\n                <com.google.android.material.tabs.TabItem\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"@string/best_practice\" />\n\n            </com.google.android.material.tabs.TabLayout>\n\n        </com.google.android.material.appbar.AppBarLayout>\n\n        <!-- TODO 建议不要使用如下 TabLayout 和 ViewPager 的嵌套式语法糖\n           此处只是为了便于展示 BindingAdapter 的业务能力，而使用的语法糖，\n           在实际开发中，BindingAdapter 的通知时机和 TabLayout 的某些\n           机制并不完美配合，导致可能出现 TabLayout 和 ViewPager 联动失效、\n           ViewPager child 不显示内容等问题 -->\n\n        <androidx.viewpager.widget.ViewPager\n            android:id=\"@+id/view_pager\"\n            initTabAndPage=\"@{true}\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            app:layout_behavior=\"@string/appbar_scrolling_view_behavior\">\n\n            <!-- TODO 以下 adapter 和 sumbitList 属性皆乃 BindingAdapter 中定义的属性，\n            旨在将 ViewModel 中的数据绑定到 BindingAdapter，\n            以便可以间接通知布局中存在的视图实例，避免空指针安全问题，\n            如果这样说还不理解的话，可参考 DataBinding 篇的解析\n            https://xiaozhuanlan.com/topic/9816742350 -->\n\n            <!-- TODO 该 BindingAdapter 现已抽到 Strict-DataBinding 开源库中独立维护\n            可在本项目中搜索 RecyclerViewBindingAdapter 找到-->\n\n            <androidx.recyclerview.widget.RecyclerView\n                android:id=\"@+id/rv\"\n                adapter=\"@{adapter}\"\n                submitList=\"@{vm.list}\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:clipToPadding=\"false\"\n                app:layoutManager=\"com.kunminx.binding_recyclerview.layout_manager.WrapContentLinearLayoutManager\" />\n\n            <WebView\n                android:id=\"@+id/web_view\"\n                pageAssetPath=\"@{vm.pageAssetPath}\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:clipToPadding=\"false\" />\n\n        </androidx.viewpager.widget.ViewPager>\n\n    </androidx.coordinatorlayout.widget.CoordinatorLayout>\n</layout>\n"
  },
  {
    "path": "app/src/main/res/layout-land/fragment_player.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018-present KunMinX\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\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:playpauseview=\"http://schemas.android.com/apk/res-auto\">\n\n    <data>\n\n        <variable\n            name=\"click\"\n            type=\"com.kunminx.puremusic.ui.page.PlayerFragment.ClickProxy\" />\n\n        <variable\n            name=\"listener\"\n            type=\"com.kunminx.puremusic.ui.page.PlayerFragment.ListenerHandler\" />\n\n        <variable\n            name=\"vm\"\n            type=\"com.kunminx.puremusic.ui.page.PlayerFragment.PlayerStates\" />\n\n  </data>\n\n    <androidx.coordinatorlayout.widget.CoordinatorLayout\n        android:id=\"@+id/topContainer\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:layout_gravity=\"top\">\n\n        <androidx.appcompat.widget.AppCompatImageView\n            android:id=\"@+id/album_art\"\n            imageUrl=\"@{vm.coverImg}\"\n            placeHolder=\"@{vm.placeHolder}\"\n            android:layout_width=\"@dimen/sliding_up_header_land\"\n            android:layout_height=\"@dimen/sliding_up_header_land\"\n            android:layout_gravity=\"center_horizontal\"\n            android:layout_marginTop=\"48dp\"\n            android:scaleType=\"centerCrop\"\n            android:src=\"@drawable/bg_album_default\" />\n\n        <androidx.constraintlayout.widget.ConstraintLayout\n            android:id=\"@+id/icon_container\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"bottom\"\n            android:layout_marginTop=\"10dp\"\n            android:layout_marginBottom=\"48dp\"\n            android:orientation=\"horizontal\">\n\n            <net.steamcrafted.materialiconlib.MaterialIconView\n                android:id=\"@+id/previous\"\n                android:layout_width=\"36dp\"\n                android:layout_height=\"36dp\"\n                android:layout_marginEnd=\"24dp\"\n                android:background=\"?attr/selectableItemBackgroundBorderless\"\n                android:onClick=\"@{()->click.previous()}\"\n                android:scaleType=\"center\"\n                app:layout_constraintBottom_toBottomOf=\"parent\"\n                app:layout_constraintRight_toLeftOf=\"@+id/play_pause\"\n                app:layout_constraintTop_toTopOf=\"parent\"\n                app:materialIcon=\"skip_previous\"\n                app:materialIconColor=\"@android:color/black\"\n                app:materialIconSize=\"28dp\" />\n\n            <com.kunminx.puremusic.ui.view.PlayPauseView\n                android:id=\"@+id/play_pause\"\n                isPlaying=\"@{vm.isPlaying}\"\n                onClickWithDebouncing=\"@{()->click.togglePlay()}\"\n                android:layout_width=\"36dp\"\n                android:layout_height=\"36dp\"\n                android:clickable=\"true\"\n                android:focusable=\"true\"\n                android:foreground=\"?attr/selectableItemBackground\"\n                app:layout_constraintBottom_toBottomOf=\"parent\"\n                app:layout_constraintLeft_toLeftOf=\"parent\"\n                app:layout_constraintRight_toRightOf=\"parent\"\n                app:layout_constraintTop_toTopOf=\"parent\"\n                playpauseview:drawableColor=\"@color/white\"\n                playpauseview:isCircleDraw=\"true\" />\n\n            <net.steamcrafted.materialiconlib.MaterialIconView\n                android:id=\"@+id/next\"\n                android:layout_width=\"36dp\"\n                android:layout_height=\"36dp\"\n                android:layout_marginStart=\"24dp\"\n                android:background=\"?attr/selectableItemBackgroundBorderless\"\n                android:onClick=\"@{()->click.next()}\"\n                android:scaleType=\"center\"\n                app:layout_constraintBottom_toBottomOf=\"parent\"\n                app:layout_constraintLeft_toRightOf=\"@+id/play_pause\"\n                app:layout_constraintTop_toTopOf=\"parent\"\n                app:materialIcon=\"skip_next\"\n                app:materialIconColor=\"@android:color/black\"\n                app:materialIconSize=\"28dp\" />\n\n        </androidx.constraintlayout.widget.ConstraintLayout>\n\n    <SeekBar\n      android:id=\"@+id/seek_bottom\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"wrap_content\"\n      android:layout_gravity=\"bottom\"\n      android:background=\"@color/transparent\"\n      android:clickable=\"true\"\n      android:focusable=\"true\"\n      android:max=\"@{vm.maxSeekDuration}\"\n      android:minHeight=\"6dp\"\n      android:paddingTop=\"24dp\"\n      android:progress=\"@{vm.currentSeekPosition}\"\n      android:progressDrawable=\"@drawable/progressbar_color\"\n      android:thumb=\"@null\"\n      android:visibility=\"visible\"\n      app:onSeekBarChangeListener=\"@{listener}\" />\n\n    </androidx.coordinatorlayout.widget.CoordinatorLayout>\n</layout>\n"
  },
  {
    "path": "app/src/main/res/navigation/nav_drawer.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018-present KunMinX\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\n<navigation 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:id=\"@+id/nav_drawer\"\n    app:startDestination=\"@id/drawerFragment\">\n\n    <fragment\n        android:id=\"@+id/drawerFragment\"\n        android:name=\"com.kunminx.puremusic.ui.page.DrawerFragment\"\n        android:label=\"fragment_drawer\"\n        tools:layout=\"@layout/fragment_drawer\" />\n</navigation>"
  },
  {
    "path": "app/src/main/res/navigation/nav_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018-present KunMinX\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\n<navigation 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:id=\"@+id/nav_main\"\n    app:startDestination=\"@id/mainFragment\">\n\n    <fragment\n        android:id=\"@+id/mainFragment\"\n        android:name=\"com.kunminx.puremusic.ui.page.MainFragment\"\n        android:label=\"fragment_main\"\n        tools:layout=\"@layout/fragment_main\">\n\n        <action\n            android:id=\"@+id/action_mainFragment_to_searchFragment\"\n            app:destination=\"@id/searchFragment\"\n            app:enterAnim=\"@anim/h_fragment_enter\"\n            app:exitAnim=\"@anim/h_fragment_exit\"\n            app:popEnterAnim=\"@anim/h_fragment_pop_enter\"\n            app:popExitAnim=\"@anim/h_fragment_pop_exit\" />\n\n        <action\n            android:id=\"@+id/action_mainFragment_to_loginFragment\"\n            app:destination=\"@id/loginFragment\"\n            app:enterAnim=\"@anim/h_fragment_enter\"\n            app:exitAnim=\"@anim/h_fragment_exit\"\n            app:popEnterAnim=\"@anim/h_fragment_pop_enter\"\n            app:popExitAnim=\"@anim/h_fragment_pop_exit\" />\n\n    </fragment>\n\n    <fragment\n        android:id=\"@+id/searchFragment\"\n        android:name=\"com.kunminx.puremusic.ui.page.SearchFragment\"\n        android:label=\"fragment_search\"\n        tools:layout=\"@layout/fragment_search\">\n\n    </fragment>\n\n    <fragment\n        android:id=\"@+id/loginFragment\"\n        android:name=\"com.kunminx.puremusic.ui.page.LoginFragment\"\n        android:label=\"LoginFragment\"\n        tools:layout=\"@layout/fragment_login\" />\n\n</navigation>\n"
  },
  {
    "path": "app/src/main/res/navigation/nav_slide.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018-present KunMinX\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\n<navigation 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:id=\"@+id/nav_slide\"\n    app:startDestination=\"@id/playerFragment\">\n\n    <fragment\n        android:id=\"@+id/playerFragment\"\n        android:name=\"com.kunminx.puremusic.ui.page.PlayerFragment\"\n        android:label=\"fragment_player\"\n        tools:layout=\"@layout/fragment_player\" />\n</navigation>"
  },
  {
    "path": "app/src/main/res/values/attrs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018-present KunMinX\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\n<resources>\n\n    <declare-styleable name=\"PlayPauseView\">\n        <attr name=\"isCircleDraw\" format=\"boolean\" />\n        <attr name=\"circleAlpha\" format=\"integer\" />\n        <attr name=\"backgroundColor\" format=\"reference|color\" />\n        <attr name=\"drawableColor\" format=\"reference|color\" />\n    </declare-styleable>\n\n</resources>"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"colorPrimary\">#008577</color>\n    <color name=\"colorPrimaryDark\">#00574B</color>\n    <color name=\"colorAccent\">#D81B60</color>\n\n    <color name=\"white\">#fff</color>\n    <color name=\"black\">#000</color>\n    <color name=\"gray\">#666</color>\n    <color name=\"light_gray\">#999</color>\n    <color name=\"transparent\">#00000000</color>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/dimen.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018-present KunMinX\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\n<resources>\n    <dimen name=\"sliding_up_header\">55dp</dimen>\n    <dimen name=\"sliding_up_header_land\">200dp</dimen>\n\n\n</resources>"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">PureMusic</string>\n    <string name=\"app_name_debug\">PureMusic-Debug</string>\n    <string name=\"welcome_back\">欢迎回来</string>\n\n    <string name=\"free_music_json\">\n        \"{\\\"albumId\\\":\\\"001\\\",\\\"title\\\":\\\"Cute\\\",\\\"summary\\\":\\\"BenSound\\\",\\\"artist\\\":{\\\"name\\\":\\\"UnKnown\\\"},\\\"coverImg\\\":\\\"https://upload-images.jianshu.io/upload_images/57036-570ed96eb055ef17.png\\\",\\\"musics\\\":[{\\\"musicId\\\":\\\"001\\\",\\\"title\\\":\\\"Tomorrow\\\",\\\"artist\\\":{\\\"name\\\":\\\"UnKnown\\\"},\\\"coverImg\\\":\\\"https://upload-images.jianshu.io/upload_images/57036-570ed96eb055ef17.png\\\",\\\"url\\\":\\\"bensound-sunny.mp3\\\"},{\\\"musicId\\\":\\\"002\\\",\\\"title\\\":\\\"Sunny\\\",\\\"artist\\\":{\\\"name\\\":\\\"UnKnown\\\"},\\\"coverImg\\\":\\\"https://upload-images.jianshu.io/upload_images/57036-8a7d311f2a758d4c.png\\\",\\\"url\\\":\\\"bensound-sunny.mp3\\\"},{\\\"musicId\\\":\\\"003\\\",\\\"title\\\":\\\"Energy\\\",\\\"artist\\\":{\\\"name\\\":\\\"UnKnown\\\"},\\\"coverImg\\\":\\\"https://upload-images.jianshu.io/upload_images/57036-9f034d4886c8fe77.png\\\",\\\"url\\\":\\\"bensound-sunny.mp3\\\"},{\\\"musicId\\\":\\\"004\\\",\\\"title\\\":\\\"Epic\\\",\\\"artist\\\":{\\\"name\\\":\\\"UnKnown\\\"},\\\"coverImg\\\":\\\"https://upload-images.jianshu.io/upload_images/57036-00b8c58771bdc34d.png\\\",\\\"url\\\":\\\"bensound-sunny.mp3\\\"},{\\\"musicId\\\":\\\"005\\\",\\\"title\\\":\\\"Slow motion\\\",\\\"artist\\\":{\\\"name\\\":\\\"UnKnown\\\"},\\\"coverImg\\\":\\\"https://upload-images.jianshu.io/upload_images/57036-2ebb4f068282a46e.png\\\",\\\"url\\\":\\\"bensound-sunny.mp3\\\"},{\\\"musicId\\\":\\\"006\\\",\\\"title\\\":\\\"Cute\\\",\\\"artist\\\":{\\\"name\\\":\\\"UnKnown\\\"},\\\"coverImg\\\":\\\"https://upload-images.jianshu.io/upload_images/57036-bc522cc201c4dfef.png\\\",\\\"url\\\":\\\"bensound-sunny.mp3\\\"},{\\\"musicId\\\":\\\"007\\\",\\\"title\\\":\\\"Beach Party\\\",\\\"artist\\\":{\\\"name\\\":\\\"UnKnown\\\"},\\\"coverImg\\\":\\\"https://upload-images.jianshu.io/upload_images/57036-07ab1f9932cdad6a.png\\\",\\\"url\\\":\\\"bensound-sunny.mp3\\\"},{\\\"musicId\\\":\\\"008\\\",\\\"title\\\":\\\"Actionable\\\",\\\"artist\\\":{\\\"name\\\":\\\"UnKnown\\\"},\\\"coverImg\\\":\\\"https://upload-images.jianshu.io/upload_images/57036-fc92ea6f8f164947.png\\\",\\\"url\\\":\\\"bensound-sunny.mp3\\\"},{\\\"musicId\\\":\\\"009\\\",\\\"title\\\":\\\"Smile\\\",\\\"artist\\\":{\\\"name\\\":\\\"UnKnown\\\"},\\\"coverImg\\\":\\\"https://upload-images.jianshu.io/upload_images/57036-805e2bbbbde2a3a6.png\\\",\\\"url\\\":\\\"bensound-sunny.mp3\\\"},{\\\"musicId\\\":\\\"010\\\",\\\"title\\\":\\\"Forever\\\",\\\"artist\\\":{\\\"name\\\":\\\"UnKnown\\\"},\\\"coverImg\\\":\\\"https://upload-images.jianshu.io/upload_images/57036-0af32fdf88b1ee83.png\\\",\\\"url\\\":\\\"bensound-sunny.mp3\\\"},{\\\"musicId\\\":\\\"011\\\",\\\"title\\\":\\\"Yestoday\\\",\\\"artist\\\":{\\\"name\\\":\\\"UnKnown\\\"},\\\"coverImg\\\":\\\"https://upload-images.jianshu.io/upload_images/57036-570ed96eb055ef17.png\\\",\\\"url\\\":\\\"bensound-sunny.mp3\\\"},{\\\"musicId\\\":\\\"012\\\",\\\"title\\\":\\\"MonDay\\\",\\\"artist\\\":{\\\"name\\\":\\\"UnKnown\\\"},\\\"coverImg\\\":\\\"https://upload-images.jianshu.io/upload_images/57036-8a7d311f2a758d4c.png\\\",\\\"url\\\":\\\"bensound-sunny.mp3\\\"},{\\\"musicId\\\":\\\"013\\\",\\\"title\\\":\\\"Tuesday\\\",\\\"artist\\\":{\\\"name\\\":\\\"UnKnown\\\"},\\\"coverImg\\\":\\\"https://upload-images.jianshu.io/upload_images/57036-9f034d4886c8fe77.png\\\",\\\"url\\\":\\\"bensound-sunny.mp3\\\"},{\\\"musicId\\\":\\\"014\\\",\\\"title\\\":\\\"Wednesday\\\",\\\"artist\\\":{\\\"name\\\":\\\"UnKnown\\\"},\\\"coverImg\\\":\\\"https://upload-images.jianshu.io/upload_images/57036-00b8c58771bdc34d.png\\\",\\\"url\\\":\\\"bensound-sunny.mp3\\\"},{\\\"musicId\\\":\\\"015\\\",\\\"title\\\":\\\"Thursday\\\",\\\"artist\\\":{\\\"name\\\":\\\"UnKnown\\\"},\\\"coverImg\\\":\\\"https://upload-images.jianshu.io/upload_images/57036-2ebb4f068282a46e.png\\\",\\\"url\\\":\\\"bensound-sunny.mp3\\\"},{\\\"musicId\\\":\\\"016\\\",\\\"title\\\":\\\"Friday\\\",\\\"artist\\\":{\\\"name\\\":\\\"UnKnown\\\"},\\\"coverImg\\\":\\\"https://upload-images.jianshu.io/upload_images/57036-bc522cc201c4dfef.png\\\",\\\"url\\\":\\\"bensound-sunny.mp3\\\"},{\\\"musicId\\\":\\\"017\\\",\\\"title\\\":\\\"Saturday\\\",\\\"artist\\\":{\\\"name\\\":\\\"UnKnown\\\"},\\\"coverImg\\\":\\\"https://upload-images.jianshu.io/upload_images/57036-07ab1f9932cdad6a.png\\\",\\\"url\\\":\\\"bensound-sunny.mp3\\\"},{\\\"musicId\\\":\\\"018\\\",\\\"title\\\":\\\"Sunday\\\",\\\"artist\\\":{\\\"name\\\":\\\"UnKnown\\\"},\\\"coverImg\\\":\\\"https://upload-images.jianshu.io/upload_images/57036-fc92ea6f8f164947.png\\\",\\\"url\\\":\\\"bensound-sunny.mp3\\\"},{\\\"musicId\\\":\\\"019\\\",\\\"title\\\":\\\"HaHa\\\",\\\"artist\\\":{\\\"name\\\":\\\"UnKnown\\\"},\\\"coverImg\\\":\\\"https://upload-images.jianshu.io/upload_images/57036-805e2bbbbde2a3a6.png\\\",\\\"url\\\":\\\"bensound-sunny.mp3\\\"},{\\\"musicId\\\":\\\"020\\\",\\\"title\\\":\\\"Good\\\",\\\"artist\\\":{\\\"name\\\":\\\"UnKnown\\\"},\\\"coverImg\\\":\\\"https://upload-images.jianshu.io/upload_images/57036-0af32fdf88b1ee83.png\\\",\\\"url\\\":\\\"bensound-sunny.mp3\\\"}]}\"\n    </string>\n\n    <string name=\"library_json\">\n        \"[{\\\"title\\\":\\\"Lifecycle\\\",\\\"summary\\\":\\\"主要用于在软件工程的背景下，解决生命周期管理的一致性问题\\\",\\\"url\\\":\\\"https://xiaozhuanlan.com/topic/3684721950\\\"},{\\\"title\\\":\\\"LiveData\\\",\\\"summary\\\":\\\"主要用于配合 “可信源” 实现消息的读写分离，从而确保消息分发的可靠一致，避免收到不可预期的推送或脏数据\\\",\\\"url\\\":\\\"https://xiaozhuanlan.com/topic/0168753249\\\"},{\\\"title\\\":\\\"ViewModel\\\",\\\"summary\\\":\\\"主要用于托管页面状态、分治视图控制器重建时的状态恢复，从而提升状态恢复的效率和节省不必要的流量、电量开销\\\",\\\"url\\\":\\\"https://xiaozhuanlan.com/topic/6257931840\\\"},{\\\"title\\\":\\\"DataBinding\\\",\\\"summary\\\":\\\"主要用于在软件工程的背景下，解决 “视图实例 null 安全” 的一致性问题\\\",\\\"url\\\":\\\"https://xiaozhuanlan.com/topic/9816742350\\\"},{\\\"title\\\":\\\"Navigation\\\",\\\"summary\\\":\\\"通过声明式编程来解决 “应用内路由导航” 和 “初值传参” 的一致性问题\\\",\\\"url\\\":\\\"https://xiaozhuanlan.com/topic/5860149732\\\"}]\"\n    </string>\n    <string name=\"play\">播放</string>\n    <string name=\"notify_of_play\">播放时的通知栏展示</string>\n    <string name=\"network_unconnected\">网络断开了</string>\n\n    <string name=\"play_repeat\">列表循环</string>\n    <string name=\"play_repeat_once\">单曲循环</string>\n    <string name=\"play_shuffle\">随机播放</string>\n\n    <string name=\"unfinished\">这是个前排吃瓜的按钮！ ^_^</string>\n    <string name=\"recently\">最近播放</string>\n    <string name=\"best_practice\">最佳实践</string>\n\n\n    <string name=\"app_summary\">是难得一见的 Jetpack MVVM 最佳实践！</string>\n    <string name=\"project_title\">本项目中用到的技术点包括：</string>\n    <string name=\"Copyright\">Product with love by KunMinX @2019</string>\n    <string name=\"search_page_tip\">本页面用于测试 Navigation 路由导航</string>\n    <string name=\"test_download\">点击测试下载，返回页面仍有效</string>\n    <string name=\"test_lifecycle_download\">点击测试下载，离开页面即中止</string>\n    <string name=\"search_back\">右滑关闭本页面</string>\n\n    <string name=\"relearn_android\">《重学安卓》</string>\n    <string name=\"learn_more_friends\">付费读者加微信进群 myatejx</string>\n    <string name=\"learn_more\">Learn More</string>\n\n    <string name=\"login_title\">欢迎来到 Jetpack MVVM 的世界</string>\n    <string name=\"login_content\">Welcome to the world of Jetpack MVVM best practice !</string>\n    <string name=\"user_name\">请输入用户名</string>\n    <string name=\"user_password\">请输入密码</string>\n    <string name=\"login\">登录</string>\n    <string name=\"username_or_pwd_incomplete\">用户名或密码不完整</string>\n    <string name=\"network_state_retry\">网络状态不佳，请重试</string>\n    <string name=\"article_navigation\">https://xiaozhuanlan.com/topic/5860149732</string>\n    <string name=\"github_project\">https://github.com/KunMinX/Jetpack-MVVM-Best-Practice</string>\n\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.NoActionBar\">\n        <!-- Customize your theme here. -->\n        <item name=\"colorPrimary\">@color/colorPrimary</item>\n        <item name=\"colorPrimaryDark\">@color/white</item>\n        <item name=\"colorAccent\">@color/colorAccent</item>\n    </style>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/xml/network_security_config.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<network-security-config xmlns:tools=\"http://schemas.android.com/tools\">\n    <base-config\n        cleartextTrafficPermitted=\"true\"\n        tools:ignore=\"InsecureBaseConfiguration\" />\n</network-security-config>\n"
  },
  {
    "path": "app/src/test/java/com/kunminx/puremusic/ExampleUnitTest.java",
    "content": "package com.kunminx.puremusic;\n\nimport static org.junit.Assert.assertEquals;\n\nimport org.junit.Test;\n\n/**\n * Example local unit test, which will execute on the development machine (host).\n *\n * @see <a href=\"http://d.android.com/tools/testing\">Testing documentation</a>\n */\npublic class ExampleUnitTest {\n    @Test\n    public void addition_isCorrect() {\n        assertEquals(4, 2 + 2);\n    }\n}\n"
  },
  {
    "path": "architecture/build.gradle",
    "content": "/*\n * Copyright 2018-present KunMinX\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\napply plugin: \"com.android.library\"\n\nandroid {\n    compileSdk appTargetSdk\n    defaultConfig {\n        minSdk appMinSdk\n        targetSdk appTargetSdk\n\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildFeatures {\n        dataBinding true\n    }\n}\n\n\ndependencies {\n    api fileTree(dir: \"libs\", include: [\"*.jar\", \"*.aar\"])\n\n    testImplementation \"junit:junit:4.13.2\"\n    androidTestImplementation \"androidx.test.ext:junit:1.1.5\"\n    androidTestImplementation \"androidx.test.espresso:espresso-core:3.5.1\"\n\n    //常用基础组件\n\n    api \"androidx.appcompat:appcompat:1.6.1\"\n    api \"org.jetbrains:annotations:24.0.1\"\n    api \"androidx.navigation:navigation-runtime:2.5.3\"\n\n    api \"com.google.android.material:material:1.9.0\"\n    api \"androidx.constraintlayout:constraintlayout:2.1.4\"\n    api \"androidx.recyclerview:recyclerview:1.3.1\"\n\n    //常用架构组件，已按功能提取分割为多个独立库，可按需选配\n\n    api 'com.github.KunMinX:MVI-Dispatcher:7.6.0'\n    api 'com.github.KunMinX:UnPeek-LiveData:7.8.0'\n    api 'com.github.KunMinX:Smooth-Navigation:v4.0.0'\n    api 'com.github.KunMinX.Strict-DataBinding:binding_state:6.2.0'\n    api 'com.github.KunMinX.Strict-DataBinding:strict_databinding:6.2.0'\n    api 'com.github.KunMinX.Strict-DataBinding:binding_recyclerview:6.2.0'\n\n    //常用数据、媒体组件\n\n    api \"com.github.bumptech.glide:glide:4.16.0\"\n\n    api \"com.google.code.gson:gson:2.10.1\"\n    api \"com.squareup.retrofit2:retrofit:2.9.0\"\n    api \"com.squareup.retrofit2:converter-gson:2.9.0\"\n    api \"com.squareup.okhttp3:logging-interceptor:4.11.0\"\n    api \"com.squareup.okhttp3:okhttp:4.11.0\"\n\n    api 'io.reactivex.rxjava2:rxandroid:2.1.1'\n    api 'io.reactivex.rxjava2:rxjava:2.2.21'\n}\n"
  },
  {
    "path": "architecture/src/androidTest/java/com/kunminx/architecture/ExampleInstrumentedTest.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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\npackage com.kunminx.architecture;\n\nimport static org.junit.Assert.assertEquals;\n\nimport android.content.Context;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport androidx.test.platform.app.InstrumentationRegistry;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/**\n * Instrumented test, which will execute on an Android device.\n *\n * @see <a href=\"http://d.android.com/tools/testing\">Testing documentation</a>\n */\n@RunWith(AndroidJUnit4.class)\npublic class ExampleInstrumentedTest {\n    @Test\n    public void useAppContext() {\n        // Context of the app under test.\n        Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();\n\n        assertEquals(\"com.kunminx.architecture.test\", appContext.getPackageName());\n    }\n}\n"
  },
  {
    "path": "architecture/src/main/AndroidManifest.xml",
    "content": "<!--\n  ~ Copyright 2018-present KunMinX\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\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.kunminx.architecture\">\n\n    <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n\n    <application>\n\n        <provider\n            android:name=\"androidx.core.content.FileProvider\"\n            android:authorities=\"${applicationId}.fileprovider\"\n            android:exported=\"false\"\n            android:grantUriPermissions=\"true\">\n            <meta-data\n                android:name=\"android.support.FILE_PROVIDER_PATHS\"\n                android:resource=\"@xml/file_paths\" />\n        </provider>\n\n    </application>\n\n</manifest>\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/data/response/DataResult.java",
    "content": "/*\n *\n * Copyright 2018-present KunMinX\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 */\n\npackage com.kunminx.architecture.data.response;\n\n/**\n * TODO: 专用于数据层返回结果至 domain 层或 ViewModel，原因如下：\n * <p>\n * liveData 专用于页面开发、解决生命周期安全问题，\n * 有时数据并非通过 liveData 分发给页面，也可是通过其他方式通知非页面组件，\n * 这时 repo 方法中内定通过 liveData 分发便不合适，不如一开始就规定不在数据层通过 liveData 返回结果。\n * <p>\n * 如这么说无体会，详见《这是一份 “架构模式” 自驾攻略》解析\n * https://xiaozhuanlan.com/topic/8204519736\n * <p>\n * Create by KunMinX at 2020/7/20\n */\npublic class DataResult<T> {\n\n    private final T mEntity;\n    private final ResponseStatus mResponseStatus;\n\n    public DataResult(T entity, ResponseStatus responseStatus) {\n        mEntity = entity;\n        mResponseStatus = responseStatus;\n    }\n\n    public DataResult(T entity) {\n        mEntity = entity;\n        mResponseStatus = new ResponseStatus();\n    }\n\n    public T getResult() {\n        return mEntity;\n    }\n\n    public ResponseStatus getResponseStatus() {\n        return mResponseStatus;\n    }\n\n    public interface Result<T> {\n        void onResult(DataResult<T> dataResult);\n    }\n}\n\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/data/response/ResponseStatus.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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\npackage com.kunminx.architecture.data.response;\n\n/**\n * TODO：本类仅用作示例参考，请根据 \"实际项目需求\" 配置自定义的 \"响应状态元信息\"\n * <p>\n * Create by KunMinX at 19/10/11\n */\npublic class ResponseStatus {\n\n    private String responseCode = \"\";\n    private boolean success = true;\n    private Enum<ResultSource> source = ResultSource.NETWORK;\n\n    public ResponseStatus() {\n    }\n\n    public ResponseStatus(String responseCode, boolean success) {\n        this.responseCode = responseCode;\n        this.success = success;\n    }\n\n    public ResponseStatus(String responseCode, boolean success, Enum<ResultSource> source) {\n        this(responseCode, success);\n        this.source = source;\n    }\n\n    public String getResponseCode() {\n        return responseCode;\n    }\n\n    public boolean isSuccess() {\n        return success;\n    }\n\n    public Enum<ResultSource> getSource() {\n        return source;\n    }\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/data/response/ResultSource.java",
    "content": "package com.kunminx.architecture.data.response;\n\n/**\n * Create by KunMinX at 2020/11/30\n */\npublic enum ResultSource {\n    NETWORK, DATABASE, LOCAL_FILE\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/data/response/manager/NetworkStateManager.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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\npackage com.kunminx.architecture.data.response.manager;\n\nimport android.content.IntentFilter;\nimport android.net.ConnectivityManager;\n\nimport androidx.annotation.NonNull;\nimport androidx.lifecycle.DefaultLifecycleObserver;\nimport androidx.lifecycle.LifecycleOwner;\n\nimport com.kunminx.architecture.utils.Utils;\n\n/**\n * Create by KunMinX at 19/10/11\n */\npublic class NetworkStateManager implements DefaultLifecycleObserver {\n\n    private static final NetworkStateManager S_MANAGER = new NetworkStateManager();\n    private final NetworkStateReceive mNetworkStateReceive = new NetworkStateReceive();\n\n    private NetworkStateManager() {\n    }\n\n    public static NetworkStateManager getInstance() {\n        return S_MANAGER;\n    }\n\n    //TODO tip：让 NetworkStateManager 可观察页面生命周期，\n    // 从而在页面失去焦点时，\n    // 及时断开本页面对网络状态的监测，以避免重复回调和一系列不可预期的问题。\n\n    // 关于 Lifecycle 组件的存在意义，可详见《为你还原一个真实的 Jetpack Lifecycle》篇的解析\n    // https://xiaozhuanlan.com/topic/3684721950\n\n    @Override\n    public void onResume(@NonNull LifecycleOwner owner) {\n        IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);\n        Utils.getApp().getApplicationContext().registerReceiver(mNetworkStateReceive, filter);\n    }\n\n    @Override\n    public void onPause(@NonNull LifecycleOwner owner) {\n        Utils.getApp().getApplicationContext().unregisterReceiver(mNetworkStateReceive);\n    }\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/data/response/manager/NetworkStateReceive.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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\npackage com.kunminx.architecture.data.response.manager;\n\nimport android.content.BroadcastReceiver;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.net.ConnectivityManager;\nimport android.widget.Toast;\n\nimport com.kunminx.architecture.R;\nimport com.kunminx.architecture.utils.NetworkUtils;\n\nimport java.util.Objects;\n\n/**\n * Create by KunMinX at 19/8/5\n */\npublic class NetworkStateReceive extends BroadcastReceiver {\n\n    @Override\n    public void onReceive(Context context, Intent intent) {\n        if (Objects.equals(intent.getAction(), ConnectivityManager.CONNECTIVITY_ACTION)) {\n            if (!NetworkUtils.isConnected()) {\n                Toast.makeText(context, context.getString(R.string.network_not_good), Toast.LENGTH_SHORT).show();\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/domain/request/AsyncTask.java",
    "content": "package com.kunminx.architecture.domain.request;\n\nimport android.annotation.SuppressLint;\n\nimport io.reactivex.Observable;\nimport io.reactivex.ObservableEmitter;\nimport io.reactivex.ObservableOnSubscribe;\nimport io.reactivex.android.schedulers.AndroidSchedulers;\nimport io.reactivex.annotations.NonNull;\nimport io.reactivex.disposables.Disposable;\nimport io.reactivex.schedulers.Schedulers;\n\n/**\n * Create by KunMinX at 2022/6/14\n */\npublic class AsyncTask {\n\n  @SuppressLint(\"CheckResult\")\n  public static <T> Observable<T> doIO(Action<T> start) {\n    return Observable.create(start::onEmit)\n            .subscribeOn(Schedulers.io())\n            .observeOn(AndroidSchedulers.mainThread());\n  }\n\n  @SuppressLint(\"CheckResult\")\n  public static <T> Observable<T> doCalculate(Action<T> start) {\n    return Observable.create(start::onEmit)\n            .subscribeOn(Schedulers.computation())\n            .observeOn(AndroidSchedulers.mainThread());\n  }\n\n  public interface Action<T> {\n    void onEmit(ObservableEmitter<T> emitter);\n  }\n\n  public interface Observer<T> extends io.reactivex.Observer<T> {\n    default void onSubscribe(@NonNull Disposable d) {\n    }\n\n    void onNext(@NonNull T t);\n\n    default void onError(@NonNull Throwable e) {\n    }\n\n    default void onComplete() {\n    }\n  }\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/domain/request/Requester.java",
    "content": "package com.kunminx.architecture.domain.request;\n\nimport androidx.lifecycle.ViewModel;\n/**\n * TODO tip 1：\n * 基于单一职责原则，抽取 Jetpack ViewModel \"作用域管理\" 的能力作为 \"领域层组件\"，\n *\n * TODO tip 2：让 UI 和业务分离，让数据总是从生产者流向消费者\n *\n * UI逻辑和业务逻辑，本质区别在于，前者是数据的消费者，后者是数据的生产者，\n * \"领域层组件\" 作为数据的生产者，职责应仅限于 \"请求调度 和 结果分发\"，\n *\n * 换言之，\"领域层组件\" 中应当只关注数据的生成，而不关注数据的使用，\n * 改变 UI 状态的逻辑代码，只应在表现层页面中编写、在 Observer 回调中响应数据的变化，\n * 将来升级到 Jetpack Compose 更是如此，\n *\n * Activity {\n *  onCreate(){\n *   vm.livedata.observe { result->\n *     if(result.show)\n *       panel.visible(VISIBLE)\n *     else\n *       panel.visible(GONE)\n *     tvTitle.setText(result.title)\n *     tvContent.setText(result.content)\n *   }\n * }\n *\n * TODO tip 3：Requester 通常按业务划分\n * 一个项目中通常可存在多个 Requester 类，\n * 每个页面可根据业务需要，持有多个不同 Requester 实例，\n * 通过 PublishSubject 回推一次性消息，并在表现层 Observer 中分流，\n * 对于 Event，直接执行，对于 State，使用 BehaviorSubject 通知 View 渲染和兜着状态，\n *\n * Activity {\n *  onCreate(){\n *   request.observe {result ->\n *     is Event ? -> execute one time\n *     is State ? -> BehaviorSubject setValue and notify\n *   }\n * }\n *\n * 如这么说无体会，详见《Jetpack MVVM 分层设计解析》解析\n * https://xiaozhuanlan.com/topic/6741932805\n *\n * Create by KunMinX at 2023/6/5\n */\npublic class Requester extends ViewModel {\n\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/domain/usecase/UseCase.java",
    "content": "package com.kunminx.architecture.domain.usecase;\n\n/**\n * Use cases are the entry points to the domain layer.\n *\n * @param <Q> the request type\n * @param <P> the response type\n */\npublic abstract class UseCase<Q extends UseCase.RequestValues, P extends UseCase.ResponseValue> {\n\n    private Q mRequestValues;\n\n    private UseCaseCallback<P> mUseCaseCallback;\n\n    public Q getRequestValues() {\n        return mRequestValues;\n    }\n\n    public void setRequestValues(Q requestValues) {\n        mRequestValues = requestValues;\n    }\n\n    public UseCaseCallback<P> getUseCaseCallback() {\n        return mUseCaseCallback;\n    }\n\n    public void setUseCaseCallback(UseCaseCallback<P> useCaseCallback) {\n        mUseCaseCallback = useCaseCallback;\n    }\n\n    void run() {\n        executeUseCase(mRequestValues);\n    }\n\n    protected abstract void executeUseCase(Q requestValues);\n\n    /**\n     * Data passed to a request.\n     */\n    public interface RequestValues {\n    }\n\n    /**\n     * Data received from a request.\n     */\n    public interface ResponseValue {\n    }\n\n    public interface UseCaseCallback<R> {\n        void onSuccess(R response);\n\n        default void onError() {\n        }\n    }\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/domain/usecase/UseCaseHandler.java",
    "content": "package com.kunminx.architecture.domain.usecase;\n\n/**\n * Runs {@link UseCase}s using a {@link UseCaseScheduler}.\n */\npublic class UseCaseHandler {\n\n    private static UseCaseHandler INSTANCE;\n\n    private final UseCaseScheduler mUseCaseScheduler;\n\n    public UseCaseHandler(UseCaseScheduler useCaseScheduler) {\n        mUseCaseScheduler = useCaseScheduler;\n    }\n\n    public static UseCaseHandler getInstance() {\n        if (INSTANCE == null) {\n            INSTANCE = new UseCaseHandler(new UseCaseThreadPoolScheduler());\n        }\n        return INSTANCE;\n    }\n\n    public <T extends UseCase.RequestValues, R extends UseCase.ResponseValue> void execute(\n        final UseCase<T, R> useCase, T values, UseCase.UseCaseCallback<R> callback) {\n        useCase.setRequestValues(values);\n        //noinspection unchecked\n        useCase.setUseCaseCallback(new UiCallbackWrapper(callback, this));\n\n        // The network request might be handled in a different thread so make sure\n        // Espresso knows\n        // that the app is busy until the response is handled.\n\n        // This callback may be called twice, once for the cache and once for loading\n        // the data from the server API, so we check before decrementing, otherwise\n        // it throws \"Counter has been corrupted!\" exception.\n        mUseCaseScheduler.execute(useCase::run);\n    }\n\n    private <V extends UseCase.ResponseValue> void notifyResponse(final V response,\n                                                                  final UseCase.UseCaseCallback<V> useCaseCallback) {\n        mUseCaseScheduler.notifyResponse(response, useCaseCallback);\n    }\n\n    private <V extends UseCase.ResponseValue> void notifyError(\n        final UseCase.UseCaseCallback<V> useCaseCallback) {\n        mUseCaseScheduler.onError(useCaseCallback);\n    }\n\n    private static final class UiCallbackWrapper<V extends UseCase.ResponseValue> implements\n        UseCase.UseCaseCallback<V> {\n        private final UseCase.UseCaseCallback<V> mCallback;\n        private final UseCaseHandler mUseCaseHandler;\n\n        public UiCallbackWrapper(UseCase.UseCaseCallback<V> callback,\n                                 UseCaseHandler useCaseHandler) {\n            mCallback = callback;\n            mUseCaseHandler = useCaseHandler;\n        }\n\n        @Override\n        public void onSuccess(V response) {\n            mUseCaseHandler.notifyResponse(response, mCallback);\n        }\n\n        @Override\n        public void onError() {\n            mUseCaseHandler.notifyError(mCallback);\n        }\n    }\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/domain/usecase/UseCaseScheduler.java",
    "content": "package com.kunminx.architecture.domain.usecase;\n\n/**\n * Interface for schedulers, see {@link UseCaseThreadPoolScheduler}.\n */\npublic interface UseCaseScheduler {\n\n    void execute(Runnable runnable);\n\n    <V extends UseCase.ResponseValue> void notifyResponse(final V response,\n                                                          final UseCase.UseCaseCallback<V> useCaseCallback);\n\n    <V extends UseCase.ResponseValue> void onError(\n        final UseCase.UseCaseCallback<V> useCaseCallback);\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/domain/usecase/UseCaseThreadPoolScheduler.java",
    "content": "package com.kunminx.architecture.domain.usecase;\n\nimport android.os.Handler;\n\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.LinkedBlockingQueue;\nimport java.util.concurrent.ThreadPoolExecutor;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * Executes asynchronous tasks using a {@link ThreadPoolExecutor}.\n * <p>\n * See also {@link Executors} for a list of factory methods to create common\n * {@link java.util.concurrent.ExecutorService}s for different scenarios.\n */\npublic class UseCaseThreadPoolScheduler implements UseCaseScheduler {\n\n    public static final int POOL_SIZE = 2;\n    public static final int MAX_POOL_SIZE = 4 * 2;\n    public static final int FIXED_POOL_SIZE = 4;\n    public static final int TIMEOUT = 30;\n    final ThreadPoolExecutor mThreadPoolExecutor;\n    private final Handler mHandler = new Handler();\n\n    /**\n     * 固定线程数的无界线程池\n     */\n    public UseCaseThreadPoolScheduler() {\n        mThreadPoolExecutor = new ThreadPoolExecutor(FIXED_POOL_SIZE, FIXED_POOL_SIZE, TIMEOUT,\n            TimeUnit.SECONDS, new LinkedBlockingQueue<>());\n    }\n\n    @Override\n    public void execute(Runnable runnable) {\n        mThreadPoolExecutor.execute(runnable);\n    }\n\n    @Override\n    public <V extends UseCase.ResponseValue> void notifyResponse(final V response,\n                                                                 final UseCase.UseCaseCallback<V> useCaseCallback) {\n        mHandler.post(() -> {\n            if (null != useCaseCallback) {\n                useCaseCallback.onSuccess(response);\n            }\n        });\n    }\n\n    @Override\n    public <V extends UseCase.ResponseValue> void onError(\n        final UseCase.UseCaseCallback<V> useCaseCallback) {\n        mHandler.post(useCaseCallback::onError);\n    }\n\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/ui/adapter/CommonViewPagerAdapter.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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\npackage com.kunminx.architecture.ui.adapter;\n\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.viewpager.widget.PagerAdapter;\n\n/**\n * Create by KunMinX at 19/6/15\n */\npublic class CommonViewPagerAdapter extends PagerAdapter {\n\n    private final int count;\n    private final boolean enableDestroyItem;\n    private final String[] title;\n\n    public CommonViewPagerAdapter(boolean enableDestroyItem, String[] title) {\n        this.count = title.length;\n        this.enableDestroyItem = enableDestroyItem;\n        this.title = title;\n    }\n\n    @Override\n    public int getCount() {\n        return count;\n    }\n\n    @Override\n    public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {\n        return view == object;\n    }\n\n    @NonNull\n    @Override\n    public Object instantiateItem(@NonNull ViewGroup container, int position) {\n        return container.getChildAt(position);\n    }\n\n    @Override\n    public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {\n        if (enableDestroyItem) {\n            container.removeView((View) object);\n        }\n    }\n\n    @Nullable\n    @Override\n    public CharSequence getPageTitle(int position) {\n        return title[position];\n    }\n}\n\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/ui/bind/DrawablesBindingAdapter.java",
    "content": "package com.kunminx.architecture.ui.bind;\n\nimport static java.lang.annotation.ElementType.FIELD;\nimport static java.lang.annotation.ElementType.PARAMETER;\n\nimport android.content.res.Resources;\nimport android.graphics.drawable.Drawable;\nimport android.graphics.drawable.GradientDrawable;\nimport android.graphics.drawable.InsetDrawable;\nimport android.graphics.drawable.StateListDrawable;\nimport android.view.View;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.IntDef;\nimport androidx.databinding.BindingAdapter;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\nimport java.lang.reflect.Field;\n\n/**\n * Created by linjiang on 2018/9/5.\n */\n\npublic class DrawablesBindingAdapter {\n    private static final String TAG = \"Drawables\";\n\n    private static final int INVALID = 0;\n    private static final int[] TMP_PADDING = new int[4];\n\n    // normal, checked, checkable, enabled, focused, pressed, selected\n    @BindingAdapter(value = {\n        \"drawable_shapeMode\",\n        \"drawable_solidColor\",\n        \"drawable_strokeColor\",\n        \"drawable_strokeWidth\",\n        \"drawable_strokeDash\",\n        \"drawable_strokeDashGap\",\n        \"drawable_radius\",\n        \"drawable_radiusLT\",\n        \"drawable_radiusLB\",\n        \"drawable_radiusRT\",\n        \"drawable_radiusRB\",\n        \"drawable_startColor\",\n        \"drawable_centerColor\",\n        \"drawable_endColor\",\n        \"drawable_orientation\",\n        \"drawable_gradientType\",\n        \"drawable_radialCenterX\",\n        \"drawable_radialCenterY\",\n        \"drawable_radialRadius\",\n        \"drawable_width\",\n        \"drawable_height\",\n        \"drawable_marginLeft\",\n        \"drawable_marginTop\",\n        \"drawable_marginRight\",\n        \"drawable_marginBottom\",\n        \"drawable_ringThickness\",\n        \"drawable_ringThicknessRatio\",\n        \"drawable_ringInnerRadius\",\n        \"drawable_ringInnerRadiusRatio\",\n\n        \"drawable_checked_shapeMode\",\n        \"drawable_checked_solidColor\",\n        \"drawable_checked_strokeColor\",\n        \"drawable_checked_strokeWidth\",\n        \"drawable_checked_strokeDash\",\n        \"drawable_checked_strokeDashGap\",\n        \"drawable_checked_radius\",\n        \"drawable_checked_radiusLT\",\n        \"drawable_checked_radiusLB\",\n        \"drawable_checked_radiusRT\",\n        \"drawable_checked_radiusRB\",\n        \"drawable_checked_startColor\",\n        \"drawable_checked_centerColor\",\n        \"drawable_checked_endColor\",\n        \"drawable_checked_orientation\",\n        \"drawable_checked_gradientType\",\n        \"drawable_checked_radialCenterX\",\n        \"drawable_checked_radialCenterY\",\n        \"drawable_checked_radialRadius\",\n        \"drawable_checked_width\",\n        \"drawable_checked_height\",\n        \"drawable_checked_marginLeft\",\n        \"drawable_checked_marginTop\",\n        \"drawable_checked_marginRight\",\n        \"drawable_checked_marginBottom\",\n        \"drawable_checked_ringThickness\",\n        \"drawable_checked_ringThicknessRatio\",\n        \"drawable_checked_ringInnerRadius\",\n        \"drawable_checked_ringInnerRadiusRatio\",\n\n        \"drawable_checkable_shapeMode\",\n        \"drawable_checkable_solidColor\",\n        \"drawable_checkable_strokeColor\",\n        \"drawable_checkable_strokeWidth\",\n        \"drawable_checkable_strokeDash\",\n        \"drawable_checkable_strokeDashGap\",\n        \"drawable_checkable_radius\",\n        \"drawable_checkable_radiusLT\",\n        \"drawable_checkable_radiusLB\",\n        \"drawable_checkable_radiusRT\",\n        \"drawable_checkable_radiusRB\",\n        \"drawable_checkable_startColor\",\n        \"drawable_checkable_centerColor\",\n        \"drawable_checkable_endColor\",\n        \"drawable_checkable_orientation\",\n        \"drawable_checkable_gradientType\",\n        \"drawable_checkable_radialCenterX\",\n        \"drawable_checkable_radialCenterY\",\n        \"drawable_checkable_radialRadius\",\n        \"drawable_checkable_width\",\n        \"drawable_checkable_height\",\n        \"drawable_checkable_marginLeft\",\n        \"drawable_checkable_marginTop\",\n        \"drawable_checkable_marginRight\",\n        \"drawable_checkable_marginBottom\",\n        \"drawable_checkable_ringThickness\",\n        \"drawable_checkable_ringThicknessRatio\",\n        \"drawable_checkable_ringInnerRadius\",\n        \"drawable_checkable_ringInnerRadiusRatio\",\n\n        \"drawable_enabled_shapeMode\",\n        \"drawable_enabled_solidColor\",\n        \"drawable_enabled_strokeColor\",\n        \"drawable_enabled_strokeWidth\",\n        \"drawable_enabled_strokeDash\",\n        \"drawable_enabled_strokeDashGap\",\n        \"drawable_enabled_radius\",\n        \"drawable_enabled_radiusLT\",\n        \"drawable_enabled_radiusLB\",\n        \"drawable_enabled_radiusRT\",\n        \"drawable_enabled_radiusRB\",\n        \"drawable_enabled_startColor\",\n        \"drawable_enabled_centerColor\",\n        \"drawable_enabled_endColor\",\n        \"drawable_enabled_orientation\",\n        \"drawable_enabled_gradientType\",\n        \"drawable_enabled_radialCenterX\",\n        \"drawable_enabled_radialCenterY\",\n        \"drawable_enabled_radialRadius\",\n        \"drawable_enabled_width\",\n        \"drawable_enabled_height\",\n        \"drawable_enabled_marginLeft\",\n        \"drawable_enabled_marginTop\",\n        \"drawable_enabled_marginRight\",\n        \"drawable_enabled_marginBottom\",\n        \"drawable_enabled_ringThickness\",\n        \"drawable_enabled_ringThicknessRatio\",\n        \"drawable_enabled_ringInnerRadius\",\n        \"drawable_enabled_ringInnerRadiusRatio\",\n\n        \"drawable_focused_shapeMode\",\n        \"drawable_focused_solidColor\",\n        \"drawable_focused_strokeColor\",\n        \"drawable_focused_strokeWidth\",\n        \"drawable_focused_strokeDash\",\n        \"drawable_focused_strokeDashGap\",\n        \"drawable_focused_radius\",\n        \"drawable_focused_radiusLT\",\n        \"drawable_focused_radiusLB\",\n        \"drawable_focused_radiusRT\",\n        \"drawable_focused_radiusRB\",\n        \"drawable_focused_startColor\",\n        \"drawable_focused_centerColor\",\n        \"drawable_focused_endColor\",\n        \"drawable_focused_orientation\",\n        \"drawable_focused_gradientType\",\n        \"drawable_focused_radialCenterX\",\n        \"drawable_focused_radialCenterY\",\n        \"drawable_focused_radialRadius\",\n        \"drawable_focused_width\",\n        \"drawable_focused_height\",\n        \"drawable_focused_marginLeft\",\n        \"drawable_focused_marginTop\",\n        \"drawable_focused_marginRight\",\n        \"drawable_focused_marginBottom\",\n        \"drawable_focused_ringThickness\",\n        \"drawable_focused_ringThicknessRatio\",\n        \"drawable_focused_ringInnerRadius\",\n        \"drawable_focused_ringInnerRadiusRatio\",\n\n        \"drawable_pressed_shapeMode\",\n        \"drawable_pressed_solidColor\",\n        \"drawable_pressed_strokeColor\",\n        \"drawable_pressed_strokeWidth\",\n        \"drawable_pressed_strokeDash\",\n        \"drawable_pressed_strokeDashGap\",\n        \"drawable_pressed_radius\",\n        \"drawable_pressed_radiusLT\",\n        \"drawable_pressed_radiusLB\",\n        \"drawable_pressed_radiusRT\",\n        \"drawable_pressed_radiusRB\",\n        \"drawable_pressed_startColor\",\n        \"drawable_pressed_centerColor\",\n        \"drawable_pressed_endColor\",\n        \"drawable_pressed_orientation\",\n        \"drawable_pressed_gradientType\",\n        \"drawable_pressed_radialCenterX\",\n        \"drawable_pressed_radialCenterY\",\n        \"drawable_pressed_radialRadius\",\n        \"drawable_pressed_width\",\n        \"drawable_pressed_height\",\n        \"drawable_pressed_marginLeft\",\n        \"drawable_pressed_marginTop\",\n        \"drawable_pressed_marginRight\",\n        \"drawable_pressed_marginBottom\",\n        \"drawable_pressed_ringThickness\",\n        \"drawable_pressed_ringThicknessRatio\",\n        \"drawable_pressed_ringInnerRadius\",\n        \"drawable_pressed_ringInnerRadiusRatio\",\n\n        \"drawable_selected_shapeMode\",\n        \"drawable_selected_solidColor\",\n        \"drawable_selected_strokeColor\",\n        \"drawable_selected_strokeWidth\",\n        \"drawable_selected_strokeDash\",\n        \"drawable_selected_strokeDashGap\",\n        \"drawable_selected_radius\",\n        \"drawable_selected_radiusLT\",\n        \"drawable_selected_radiusLB\",\n        \"drawable_selected_radiusRT\",\n        \"drawable_selected_radiusRB\",\n        \"drawable_selected_startColor\",\n        \"drawable_selected_centerColor\",\n        \"drawable_selected_endColor\",\n        \"drawable_selected_orientation\",\n        \"drawable_selected_gradientType\",\n        \"drawable_selected_radialCenterX\",\n        \"drawable_selected_radialCenterY\",\n        \"drawable_selected_radialRadius\",\n        \"drawable_selected_width\",\n        \"drawable_selected_height\",\n        \"drawable_selected_marginLeft\",\n        \"drawable_selected_marginTop\",\n        \"drawable_selected_marginRight\",\n        \"drawable_selected_marginBottom\",\n        \"drawable_selected_ringThickness\",\n        \"drawable_selected_ringThicknessRatio\",\n        \"drawable_selected_ringInnerRadius\",\n        \"drawable_selected_ringInnerRadiusRatio\",\n\n        // normal, checked, checkable, enabled, focused, pressed, selected\n\n        \"drawable\",\n        \"drawable_checked\",\n        \"drawable_checkable\",\n        \"drawable_enabled\",\n        \"drawable_focused\",\n        \"drawable_pressed\",\n        \"drawable_selected\",\n\n    }, requireAll = false)\n    public static void setViewBackground(\n        View view,\n\n        @ShapeMode int shapeMode,\n        @ColorInt Integer solidColor,\n        @ColorInt int strokeColor,\n        float strokeWidth,\n        float strokeDash,\n        float strokeDashGap,\n        float radius,\n        float radiusLT,\n        float radiusLB,\n        float radiusRT,\n        float radiusRB,\n        @ColorInt Integer startColor,\n        @ColorInt Integer centerColor,\n        @ColorInt Integer endColor,\n        @Orientation int orientation,\n        @GradientType int gradientType,\n        Float radialCenterX,\n        Float radialCenterY,\n        float radialRadius,\n        float width,\n        float height,\n        float marginLeft,\n        float marginTop,\n        float marginRight,\n        float marginBottom,\n        float ringThickness,\n        float ringThicknessRatio,\n        float ringInnerRadius,\n        float ringInnerRadiusRatio,\n\n        @ShapeMode int checked_shapeMode,\n        @ColorInt Integer checked_solidColor,\n        @ColorInt int checked_strokeColor,\n        float checked_strokeWidth,\n        float checked_strokeDash,\n        float checked_strokeDashGap,\n        float checked_radius,\n        float checked_radiusLT,\n        float checked_radiusLB,\n        float checked_radiusRT,\n        float checked_radiusRB,\n        @ColorInt Integer checked_startColor,\n        @ColorInt Integer checked_centerColor,\n        @ColorInt Integer checked_endColor,\n        @Orientation int checked_orientation,\n        @GradientType int checked_gradientType,\n        Float checked_radialCenterX,\n        Float checked_radialCenterY,\n        float checked_radialRadius,\n        float checked_width,\n        float checked_height,\n        float checked_marginLeft,\n        float checked_marginTop,\n        float checked_marginRight,\n        float checked_marginBottom,\n        float checked_ringThickness,\n        float checked_ringThicknessRatio,\n        float checked_ringInnerRadius,\n        float checked_ringInnerRadiusRatio,\n\n        @ShapeMode int checkable_shapeMode,\n        @ColorInt Integer checkable_solidColor,\n        @ColorInt int checkable_strokeColor,\n        float checkable_strokeWidth,\n        float checkable_strokeDash,\n        float checkable_strokeDashGap,\n        float checkable_radius,\n        float checkable_radiusLT,\n        float checkable_radiusLB,\n        float checkable_radiusRT,\n        float checkable_radiusRB,\n        @ColorInt Integer checkable_startColor,\n        @ColorInt Integer checkable_centerColor,\n        @ColorInt Integer checkable_endColor,\n        @Orientation int checkable_orientation,\n        @GradientType int checkable_gradientType,\n        Float checkable_radialCenterX,\n        Float checkable_radialCenterY,\n        float checkable_radialRadius,\n        float checkable_width,\n        float checkable_height,\n        float checkable_marginLeft,\n        float checkable_marginTop,\n        float checkable_marginRight,\n        float checkable_marginBottom,\n        float checkable_ringThickness,\n        float checkable_ringThicknessRatio,\n        float checkable_ringInnerRadius,\n        float checkable_ringInnerRadiusRatio,\n\n        @ShapeMode int enabled_shapeMode,\n        @ColorInt Integer enabled_solidColor,\n        @ColorInt int enabled_strokeColor,\n        float enabled_strokeWidth,\n        float enabled_strokeDash,\n        float enabled_strokeDashGap,\n        float enabled_radius,\n        float enabled_radiusLT,\n        float enabled_radiusLB,\n        float enabled_radiusRT,\n        float enabled_radiusRB,\n        @ColorInt Integer enabled_startColor,\n        @ColorInt Integer enabled_centerColor,\n        @ColorInt Integer enabled_endColor,\n        @Orientation int enabled_orientation,\n        @GradientType int enabled_gradientType,\n        Float enabled_radialCenterX,\n        Float enabled_radialCenterY,\n        float enabled_radialRadius,\n        float enabled_width,\n        float enabled_height,\n        float enabled_marginLeft,\n        float enabled_marginTop,\n        float enabled_marginRight,\n        float enabled_marginBottom,\n        float enabled_ringThickness,\n        float enabled_ringThicknessRatio,\n        float enabled_ringInnerRadius,\n        float enabled_ringInnerRadiusRatio,\n\n        @ShapeMode int focused_shapeMode,\n        @ColorInt Integer focused_solidColor,\n        @ColorInt int focused_strokeColor,\n        float focused_strokeWidth,\n        float focused_strokeDash,\n        float focused_strokeDashGap,\n        float focused_radius,\n        float focused_radiusLT,\n        float focused_radiusLB,\n        float focused_radiusRT,\n        float focused_radiusRB,\n        @ColorInt Integer focused_startColor,\n        @ColorInt Integer focused_centerColor,\n        @ColorInt Integer focused_endColor,\n        @Orientation int focused_orientation,\n        @GradientType int focused_gradientType,\n        Float focused_radialCenterX,\n        Float focused_radialCenterY,\n        float focused_radialRadius,\n        float focused_width,\n        float focused_height,\n        float focused_marginLeft,\n        float focused_marginTop,\n        float focused_marginRight,\n        float focused_marginBottom,\n        float focused_ringThickness,\n        float focused_ringThicknessRatio,\n        float focused_ringInnerRadius,\n        float focused_ringInnerRadiusRatio,\n\n        @ShapeMode int pressed_shapeMode,\n        @ColorInt Integer pressed_solidColor,\n        @ColorInt int pressed_strokeColor,\n        float pressed_strokeWidth,\n        float pressed_strokeDash,\n        float pressed_strokeDashGap,\n        float pressed_radius,\n        float pressed_radiusLT,\n        float pressed_radiusLB,\n        float pressed_radiusRT,\n        float pressed_radiusRB,\n        @ColorInt Integer pressed_startColor,\n        @ColorInt Integer pressed_centerColor,\n        @ColorInt Integer pressed_endColor,\n        @Orientation int pressed_orientation,\n        @GradientType int pressed_gradientType,\n        Float pressed_radialCenterX,\n        Float pressed_radialCenterY,\n        float pressed_radialRadius,\n        float pressed_width,\n        float pressed_height,\n        float pressed_marginLeft,\n        float pressed_marginTop,\n        float pressed_marginRight,\n        float pressed_marginBottom,\n        float pressed_ringThickness,\n        float pressed_ringThicknessRatio,\n        float pressed_ringInnerRadius,\n        float pressed_ringInnerRadiusRatio,\n\n        @ShapeMode int selected_shapeMode,\n        @ColorInt Integer selected_solidColor,\n        @ColorInt int selected_strokeColor,\n        float selected_strokeWidth,\n        float selected_strokeDash,\n        float selected_strokeDashGap,\n        float selected_radius,\n        float selected_radiusLT,\n        float selected_radiusLB,\n        float selected_radiusRT,\n        float selected_radiusRB,\n        @ColorInt Integer selected_startColor,\n        @ColorInt Integer selected_centerColor,\n        @ColorInt Integer selected_endColor,\n        @Orientation int selected_orientation,\n        @GradientType int selected_gradientType,\n        Float selected_radialCenterX,\n        Float selected_radialCenterY,\n        float selected_radialRadius,\n        float selected_width,\n        float selected_height,\n        float selected_marginLeft,\n        float selected_marginTop,\n        float selected_marginRight,\n        float selected_marginBottom,\n        float selected_ringThickness,\n        float selected_ringThicknessRatio,\n        float selected_ringInnerRadius,\n        float selected_ringInnerRadiusRatio,\n\n        Drawable drawable,\n        Drawable drawable_checked,\n        Drawable drawable_checkable,\n        Drawable drawable_enabled,\n        Drawable drawable_focused,\n        Drawable drawable_pressed,\n        Drawable drawable_selected\n    ) {\n        boolean isDefaultNull = false;\n        int count = 0;\n        Drawable defaultDrawable = drawable != null ? drawable : create(\n            shapeMode,\n            solidColor,\n            strokeColor,\n            strokeWidth,\n            strokeDash,\n            strokeDashGap,\n            radius,\n            radiusLT,\n            radiusLB,\n            radiusRT,\n            radiusRB,\n            startColor,\n            centerColor,\n            endColor,\n            orientation,\n            gradientType,\n            radialCenterX,\n            radialCenterY,\n            radialRadius,\n            width,\n            height,\n            marginLeft,\n            marginTop,\n            marginRight,\n            marginBottom,\n            ringThickness,\n            ringThicknessRatio,\n            ringInnerRadius,\n            ringInnerRadiusRatio\n        );\n        if (defaultDrawable != null) {\n            count++;\n        } else {\n            isDefaultNull = true;\n        }\n        Drawable checkedDrawable = drawable_checked != null ? drawable_checked : create(\n            checked_shapeMode,\n            checked_solidColor,\n            checked_strokeColor,\n            checked_strokeWidth,\n            checked_strokeDash,\n            checked_strokeDashGap,\n            checked_radius,\n            checked_radiusLT,\n            checked_radiusLB,\n            checked_radiusRT,\n            checked_radiusRB,\n            checked_startColor,\n            checked_centerColor,\n            checked_endColor,\n            checked_orientation,\n            checked_gradientType,\n            checked_radialCenterX,\n            checked_radialCenterY,\n            checked_radialRadius,\n            checked_width,\n            checked_height,\n            checked_marginLeft,\n            checked_marginTop,\n            checked_marginRight,\n            checked_marginBottom,\n            checked_ringThickness,\n            checked_ringThicknessRatio,\n            checked_ringInnerRadius,\n            checked_ringInnerRadiusRatio\n        );\n        if (checkedDrawable != null) {\n            count++;\n        }\n        Drawable checkableDrawable = drawable_checkable != null ? drawable_checkable : create(\n            checkable_shapeMode,\n            checkable_solidColor,\n            checkable_strokeColor,\n            checkable_strokeWidth,\n            checkable_strokeDash,\n            checkable_strokeDashGap,\n            checkable_radius,\n            checkable_radiusLT,\n            checkable_radiusLB,\n            checkable_radiusRT,\n            checkable_radiusRB,\n            checkable_startColor,\n            checkable_centerColor,\n            checkable_endColor,\n            checkable_orientation,\n            checkable_gradientType,\n            checkable_radialCenterX,\n            checkable_radialCenterY,\n            checkable_radialRadius,\n            checkable_width,\n            checkable_height,\n            checkable_marginLeft,\n            checkable_marginTop,\n            checkable_marginRight,\n            checkable_marginBottom,\n            checkable_ringThickness,\n            checkable_ringThicknessRatio,\n            checkable_ringInnerRadius,\n            checkable_ringInnerRadiusRatio\n        );\n        if (checkableDrawable != null) {\n            count++;\n        }\n        Drawable enabledDrawable = drawable_enabled != null ? drawable_enabled : create(\n            enabled_shapeMode,\n            enabled_solidColor,\n            enabled_strokeColor,\n            enabled_strokeWidth,\n            enabled_strokeDash,\n            enabled_strokeDashGap,\n            enabled_radius,\n            enabled_radiusLT,\n            enabled_radiusLB,\n            enabled_radiusRT,\n            enabled_radiusRB,\n            enabled_startColor,\n            enabled_centerColor,\n            enabled_endColor,\n            enabled_orientation,\n            enabled_gradientType,\n            enabled_radialCenterX,\n            enabled_radialCenterY,\n            enabled_radialRadius,\n            enabled_width,\n            enabled_height,\n            enabled_marginLeft,\n            enabled_marginTop,\n            enabled_marginRight,\n            enabled_marginBottom,\n            enabled_ringThickness,\n            enabled_ringThicknessRatio,\n            enabled_ringInnerRadius,\n            enabled_ringInnerRadiusRatio\n        );\n        if (enabledDrawable != null) {\n            count++;\n        }\n        Drawable focusedDrawable = drawable_focused != null ? drawable_focused : create(\n            focused_shapeMode,\n            focused_solidColor,\n            focused_strokeColor,\n            focused_strokeWidth,\n            focused_strokeDash,\n            focused_strokeDashGap,\n            focused_radius,\n            focused_radiusLT,\n            focused_radiusLB,\n            focused_radiusRT,\n            focused_radiusRB,\n            focused_startColor,\n            focused_centerColor,\n            focused_endColor,\n            focused_orientation,\n            focused_gradientType,\n            focused_radialCenterX,\n            focused_radialCenterY,\n            focused_radialRadius,\n            focused_width,\n            focused_height,\n            focused_marginLeft,\n            focused_marginTop,\n            focused_marginRight,\n            focused_marginBottom,\n            focused_ringThickness,\n            focused_ringThicknessRatio,\n            focused_ringInnerRadius,\n            focused_ringInnerRadiusRatio\n        );\n        if (focusedDrawable != null) {\n            count++;\n        }\n        Drawable pressedDrawable = drawable_pressed != null ? drawable_pressed : create(\n            pressed_shapeMode,\n            pressed_solidColor,\n            pressed_strokeColor,\n            pressed_strokeWidth,\n            pressed_strokeDash,\n            pressed_strokeDashGap,\n            pressed_radius,\n            pressed_radiusLT,\n            pressed_radiusLB,\n            pressed_radiusRT,\n            pressed_radiusRB,\n            pressed_startColor,\n            pressed_centerColor,\n            pressed_endColor,\n            pressed_orientation,\n            pressed_gradientType,\n            pressed_radialCenterX,\n            pressed_radialCenterY,\n            pressed_radialRadius,\n            pressed_width,\n            pressed_height,\n            pressed_marginLeft,\n            pressed_marginTop,\n            pressed_marginRight,\n            pressed_marginBottom,\n            pressed_ringThickness,\n            pressed_ringThicknessRatio,\n            pressed_ringInnerRadius,\n            pressed_ringInnerRadiusRatio\n        );\n        if (pressedDrawable != null) {\n            count++;\n        }\n        Drawable selectedDrawable = drawable_selected != null ? drawable_selected : create(\n            selected_shapeMode,\n            selected_solidColor,\n            selected_strokeColor,\n            selected_strokeWidth,\n            selected_strokeDash,\n            selected_strokeDashGap,\n            selected_radius,\n            selected_radiusLT,\n            selected_radiusLB,\n            selected_radiusRT,\n            selected_radiusRB,\n            selected_startColor,\n            selected_centerColor,\n            selected_endColor,\n            selected_orientation,\n            selected_gradientType,\n            selected_radialCenterX,\n            selected_radialCenterY,\n            selected_radialRadius,\n            selected_width,\n            selected_height,\n            selected_marginLeft,\n            selected_marginTop,\n            selected_marginRight,\n            selected_marginBottom,\n            selected_ringThickness,\n            selected_ringThicknessRatio,\n            selected_ringInnerRadius,\n            selected_ringInnerRadiusRatio\n        );\n        if (selectedDrawable != null) {\n            count++;\n        }\n        //noinspection StatementWithEmptyBody\n        if (count < 1) {\n            // impossible，因为该方法被调用说明至少声明了一条属性\n        } else {\n            boolean needReSetPadding = false;\n            if (isDefaultNull || count == 1) {\n                // 当设置了margin（非view的margin）时，InsetDrawable会导致view本身的padding失效\n                needReSetPadding = true;\n                TMP_PADDING[0] = view.getPaddingLeft();\n                TMP_PADDING[1] = view.getPaddingTop();\n                TMP_PADDING[2] = view.getPaddingRight();\n                TMP_PADDING[3] = view.getPaddingBottom();\n            }\n            if (count == 1 && !isDefaultNull) {\n                view.setBackground(defaultDrawable);\n            } else {\n                ProxyDrawable listDrawable = new ProxyDrawable();\n                if (checkedDrawable != null) {\n                    listDrawable.addState(new int[]{android.R.attr.state_checked}, checkedDrawable);\n                }\n                if (checkableDrawable != null) {\n                    listDrawable.addState(new int[]{android.R.attr.state_checkable}, checkableDrawable);\n                }\n                if (enabledDrawable != null) {\n                    listDrawable.addState(new int[]{android.R.attr.state_enabled}, enabledDrawable);\n                }\n                if (focusedDrawable != null) {\n                    listDrawable.addState(new int[]{android.R.attr.state_focused}, focusedDrawable);\n                }\n                if (pressedDrawable != null) {\n                    listDrawable.addState(new int[]{android.R.attr.state_pressed}, pressedDrawable);\n                }\n                if (selectedDrawable != null) {\n                    listDrawable.addState(new int[]{android.R.attr.state_selected}, selectedDrawable);\n                }\n                if (defaultDrawable != null) {\n                    listDrawable.addState(new int[]{0}, defaultDrawable);\n                } else {\n                    Drawable originDrawable = view.getBackground();\n                    if (originDrawable != null) {\n                        if (originDrawable instanceof ProxyDrawable) {\n                            originDrawable = ((ProxyDrawable) originDrawable).getOriginDrawable();\n                        }\n                        listDrawable.addState(new int[]{0}, originDrawable);\n                    }\n                }\n                view.setBackground(listDrawable);\n            }\n            if (needReSetPadding) {\n                view.setPadding(TMP_PADDING[0], TMP_PADDING[1], TMP_PADDING[2], TMP_PADDING[3]);\n            }\n        }\n    }\n\n    public static Drawable create(\n        @ShapeMode int shapeMode, @ColorInt Integer solidColor,\n        @ColorInt int strokeColor, @DP float strokeWidth, @DP float strokeDash, @DP float strokeDashGap,\n        @DP float radius, @DP float radiusLT, @DP float radiusLB, @DP float radiusRT, @DP float radiusRB,\n        @ColorInt Integer startColor, @ColorInt Integer centerColor, @ColorInt Integer endColor,\n        @Orientation int orientation, @GradientType int gradientType,\n        Float radialCenterX, Float radialCenterY, float radialRadius,\n        @DP float width, @DP float height,\n        @DP float marginLeft, @DP float marginTop, @DP float marginRight, @DP float marginBottom,\n        @DP float ringThickness,\n        @DP float ringThicknessRatio,\n        @DP float ringInnerRadius,\n        @DP float ringInnerRadiusRatio\n    ) {\n        if (shapeMode == INVALID && solidColor == null && strokeColor == INVALID\n            && strokeWidth == INVALID && strokeDash == INVALID && strokeDashGap == INVALID\n            && radius == INVALID && radiusLT == INVALID && radiusLB == INVALID\n            && radiusRT == INVALID && radiusRB == INVALID && startColor == null\n            && centerColor == null && endColor == null && orientation == INVALID\n            && gradientType == INVALID && radialCenterX == null && radialCenterY == null\n            && radialRadius == INVALID && width == INVALID && height == INVALID\n            && marginLeft == INVALID && marginTop == INVALID && marginRight == INVALID && marginBottom == INVALID\n        ) {\n            // 这里需要判断empty，因为有可能只设置了一个state的drawable，那么其他state的就是empty了\n            return null;\n        }\n        GradientDrawable drawable = new GradientDrawable();\n        if (startColor != null && endColor != null) {\n            int[] colors;\n            if (centerColor != null) {\n                colors = new int[3];\n                colors[0] = startColor;\n                colors[1] = centerColor;\n                colors[2] = endColor;\n            } else {\n                colors = new int[2];\n                colors[0] = startColor;\n                colors[1] = endColor;\n            }\n            drawable.setColors(colors);\n            drawable.setOrientation(mapOrientation(orientation));\n            drawable.setGradientType(gradientType);\n            if (gradientType == GradientType.RADIAL) {\n                drawable.setGradientCenter(radialCenterX == null ? .5f : radialCenterX,\n                    radialCenterY == null ? .5f : radialCenterY);\n                drawable.setGradientRadius(dip2px(radialRadius));\n            }\n        } else {\n            if (solidColor != null) {\n                drawable.setColor(solidColor);\n            }\n        }\n        drawable.setShape(validShapeMode(shapeMode));\n        if (shapeMode == ShapeMode.RING) {\n            // 由于GradientDrawable中没有ring相关的公开API，所以使用反射，若对性能有要求，请注意。\n            setRingValue(drawable, ringThickness, ringThicknessRatio, ringInnerRadius, ringInnerRadiusRatio);\n        }\n        if (strokeWidth > 0) {\n            drawable.setStroke(dip2px(strokeWidth), strokeColor, dip2px(strokeDash), dip2px(strokeDashGap));\n        }\n        if (radius <= 0) {\n            float[] radiusEach = new float[]{dip2px(radiusLT), dip2px(radiusLT), dip2px(radiusRT), dip2px(radiusRT),\n                dip2px(radiusRB), dip2px(radiusRB), dip2px(radiusLB), dip2px(radiusLB)};\n            drawable.setCornerRadii(radiusEach);\n        } else {\n            drawable.setCornerRadius(dip2px(radius));\n        }\n        if (width > 0 && height > 0) {\n            // https://stackoverflow.com/a/29180660/4698946\n            drawable.setSize(dip2px(width), dip2px(height));\n        }\n        if (marginLeft != 0 || marginTop != 0 || marginRight != 0 || marginBottom != 0) {\n            return new InsetDrawable(drawable,\n                dip2px(marginLeft),\n                dip2px(marginTop),\n                dip2px(marginRight),\n                dip2px(marginBottom));\n        } else {\n            return drawable;\n        }\n    }\n\n    private static int validShapeMode(@ShapeMode int shapeMode) {\n        return shapeMode > ShapeMode.RING || shapeMode < ShapeMode.RECTANGLE\n            ? GradientDrawable.RECTANGLE : shapeMode;\n    }\n\n    private static GradientDrawable.Orientation mapOrientation(@Orientation int orientation) {\n        switch (orientation) {\n            case Orientation.BL_TR:\n                return GradientDrawable.Orientation.BL_TR;\n            case Orientation.BOTTOM_TOP:\n                return GradientDrawable.Orientation.BOTTOM_TOP;\n            case Orientation.BR_TL:\n                return GradientDrawable.Orientation.BR_TL;\n            case Orientation.LEFT_RIGHT:\n                return GradientDrawable.Orientation.LEFT_RIGHT;\n            case Orientation.RIGHT_LEFT:\n                return GradientDrawable.Orientation.RIGHT_LEFT;\n            case Orientation.TL_BR:\n                return GradientDrawable.Orientation.TL_BR;\n            case Orientation.TOP_BOTTOM:\n                return GradientDrawable.Orientation.TOP_BOTTOM;\n            case Orientation.TR_BL:\n                return GradientDrawable.Orientation.TR_BL;\n            default:\n                break;\n        }\n        return GradientDrawable.Orientation.TOP_BOTTOM;\n    }\n\n    private static void setRingValue(GradientDrawable drawable,\n                                     Float thickness, Float thicknessRatio,\n                                     Float innerRadius, Float innerRadiusRatio) {\n        try {\n            Field mGradientState = drawable.getClass().getDeclaredField(\"mGradientState\");\n            mGradientState.setAccessible(true);\n            Class mGradientStateClass = mGradientState.get(drawable).getClass();\n            Field mUseLevelForShape = mGradientStateClass.getDeclaredField(\"mUseLevelForShape\");\n            mUseLevelForShape.setAccessible(true);\n            mUseLevelForShape.setBoolean(mGradientState.get(drawable), false);\n            if (thickness != null) {\n                Field mThickness = mGradientStateClass.getDeclaredField(\"mThickness\");\n                mThickness.setAccessible(true);\n                mThickness.setInt(mGradientState.get(drawable), dip2px(thickness));\n            }\n            if (thicknessRatio != null) {\n                Field mThicknessRatio = mGradientStateClass.getDeclaredField(\"mThicknessRatio\");\n                mThicknessRatio.setAccessible(true);\n                mThicknessRatio.setFloat(mGradientState.get(drawable), dip2px(thicknessRatio));\n            }\n            if (innerRadius != null) {\n                Field mInnerRadius = mGradientStateClass.getDeclaredField(\"mInnerRadius\");\n                mInnerRadius.setAccessible(true);\n                mInnerRadius.setInt(mGradientState.get(drawable), dip2px(innerRadius));\n            }\n            if (innerRadiusRatio != null) {\n                Field mInnerRadiusRatio = mGradientStateClass.getDeclaredField(\"mInnerRadiusRatio\");\n                mInnerRadiusRatio.setAccessible(true);\n                mInnerRadiusRatio.setFloat(mGradientState.get(drawable), dip2px(innerRadiusRatio));\n            }\n        } catch (NoSuchFieldException | IllegalAccessException t) {\n            t.printStackTrace();\n        }\n    }\n\n    private static int dip2px(float dipValue) {\n        final float scale = Resources.getSystem().getDisplayMetrics().density;\n        return (int) (dipValue * scale + .5f);\n    }\n\n    @IntDef({\n        ShapeMode.RECTANGLE,\n        ShapeMode.OVAL,\n        ShapeMode.LINE,\n        ShapeMode.RING,\n    })\n    @Retention(RetentionPolicy.SOURCE)\n    public @interface ShapeMode {\n        int RECTANGLE = GradientDrawable.RECTANGLE;\n        int OVAL = GradientDrawable.OVAL;\n        /**\n         * 画线时，有几点特性必须要知道的：\n         * 1. 只能画水平线，画不了竖线；\n         * 2. 线的高度是通过stroke的android:width属性设置的；\n         * 3. size的android:height属性定义的是整个形状区域的高度；\n         * 4. size的height必须大于stroke的width，否则，线无法显示；\n         * 5. 线在整个形状区域中是居中显示的；\n         * 6. 线左右两边会留有空白间距，线越粗，空白越大；\n         * 7. 引用虚线的view需要添加属性android:layerType，值设为\"software\"，否则显示不了虚线。\n         */\n        int LINE = GradientDrawable.LINE;\n        int RING = GradientDrawable.RING;\n    }\n\n    @IntDef({\n        GradientType.LINEAR,\n        GradientType.RADIAL,\n        GradientType.SWEEP\n    })\n    @Retention(RetentionPolicy.SOURCE)\n    public @interface GradientType {\n        int LINEAR = 0;\n        int RADIAL = 1;\n        int SWEEP = 2;\n    }\n\n    @IntDef({\n        Orientation.TOP_BOTTOM,\n        Orientation.TR_BL,\n        Orientation.RIGHT_LEFT,\n        Orientation.BR_TL,\n        Orientation.BOTTOM_TOP,\n        Orientation.BL_TR,\n        Orientation.LEFT_RIGHT,\n        Orientation.TL_BR\n    })\n    @Retention(RetentionPolicy.SOURCE)\n    public @interface Orientation {\n        int TOP_BOTTOM = 0;\n        int TR_BL = 1;\n        int RIGHT_LEFT = 2;\n        int BR_TL = 3;\n        int BOTTOM_TOP = 4;\n        int BL_TR = 5;\n        int LEFT_RIGHT = 6;\n        int TL_BR = 7;\n    }\n\n    @Retention(RetentionPolicy.SOURCE)\n    @Target({PARAMETER, FIELD})\n    @interface DP {\n    }\n\n    public static class ProxyDrawable extends StateListDrawable {\n\n        private Drawable originDrawable;\n\n        @Override\n        public void addState(int[] stateSet, Drawable drawable) {\n            if (stateSet != null && stateSet.length == 1 && stateSet[0] == 0) {\n                originDrawable = drawable;\n            }\n            super.addState(stateSet, drawable);\n        }\n\n        Drawable getOriginDrawable() {\n            return originDrawable;\n        }\n    }\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/ui/page/BaseActivity.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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\npackage com.kunminx.architecture.ui.page;\n\nimport android.app.Activity;\nimport android.content.Intent;\nimport android.content.res.Resources;\nimport android.graphics.Color;\nimport android.net.Uri;\nimport android.os.Bundle;\nimport android.view.inputmethod.InputMethodManager;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.lifecycle.ViewModel;\n\nimport com.kunminx.architecture.data.response.manager.NetworkStateManager;\nimport com.kunminx.architecture.ui.scope.ViewModelScope;\nimport com.kunminx.architecture.utils.AdaptScreenUtils;\nimport com.kunminx.architecture.utils.BarUtils;\nimport com.kunminx.architecture.utils.ScreenUtils;\n\n/**\n * Create by KunMinX at 19/8/1\n */\npublic abstract class BaseActivity extends DataBindingActivity {\n\n    private final ViewModelScope mViewModelScope = new ViewModelScope();\n\n    @Override\n    protected void onCreate(@Nullable Bundle savedInstanceState) {\n\n        BarUtils.setStatusBarColor(this, Color.TRANSPARENT);\n        BarUtils.setStatusBarLightMode(this, true);\n\n        super.onCreate(savedInstanceState);\n\n        getLifecycle().addObserver(NetworkStateManager.getInstance());\n\n        //TODO tip 1: DataBinding 严格模式（详见 DataBindingActivity - - - - - ）：\n        // 将 DataBinding 实例限制于 base 页面中，默认不向子类暴露，\n        // 通过这方式，彻底解决 View 实例 Null 安全一致性问题，\n        // 如此，View 实例 Null 安全性将和基于函数式编程思想的 Jetpack Compose 持平。\n\n        // 如这么说无体会，详见 https://xiaozhuanlan.com/topic/9816742350 和 https://xiaozhuanlan.com/topic/2356748910\n    }\n\n    //TODO tip 2: Jetpack 通过 \"工厂模式\" 实现 ViewModel 作用域可控，\n    //目前我们在项目中提供了 Application、Activity、Fragment 三个级别的作用域，\n    //值得注意的是，通过不同作用域 Provider 获得 ViewModel 实例非同一个，\n    //故若 ViewModel 状态信息保留不符合预期，可从该角度出发排查 是否眼前 ViewModel 实例非目标实例所致。\n\n    //如这么说无体会，详见 https://xiaozhuanlan.com/topic/6257931840\n\n    protected <T extends ViewModel> T getActivityScopeViewModel(@NonNull Class<T> modelClass) {\n        return mViewModelScope.getActivityScopeViewModel(this, modelClass);\n    }\n\n    protected <T extends ViewModel> T getApplicationScopeViewModel(@NonNull Class<T> modelClass) {\n        return mViewModelScope.getApplicationScopeViewModel(modelClass);\n    }\n\n    @Override\n    public Resources getResources() {\n        if (ScreenUtils.isPortrait()) {\n            return AdaptScreenUtils.adaptWidth(super.getResources(), 360);\n        } else {\n            return AdaptScreenUtils.adaptHeight(super.getResources(), 640);\n        }\n    }\n\n    protected void toggleSoftInput() {\n        InputMethodManager imm = (InputMethodManager) getSystemService(Activity.INPUT_METHOD_SERVICE);\n        imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS);\n    }\n\n    protected void openUrlInBrowser(String url) {\n        Uri uri = Uri.parse(url);\n        Intent intent = new Intent(Intent.ACTION_VIEW, uri);\n        startActivity(intent);\n    }\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/ui/page/BaseFragment.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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\npackage com.kunminx.architecture.ui.page;\n\nimport android.app.Activity;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.net.Uri;\nimport android.view.inputmethod.InputMethodManager;\n\nimport androidx.annotation.NonNull;\nimport androidx.lifecycle.ViewModel;\nimport androidx.navigation.NavController;\nimport androidx.navigation.fragment.NavHostFragment;\n\nimport com.kunminx.architecture.ui.scope.ViewModelScope;\n\n/**\n * Create by KunMinX at 19/7/11\n */\npublic abstract class BaseFragment extends DataBindingFragment {\n\n    private final ViewModelScope mViewModelScope = new ViewModelScope();\n\n    //TODO tip 1: DataBinding 严格模式（详见 DataBindingFragment - - - - - ）：\n    // 将 DataBinding 实例限制于 base 页面中，默认不向子类暴露，\n    // 通过这方式，彻底解决 View 实例 Null 安全一致性问题，\n    // 如此，View 实例 Null 安全性将和基于函数式编程思想的 Jetpack Compose 持平。\n\n    // 如这么说无体会，详见 https://xiaozhuanlan.com/topic/9816742350 和 https://xiaozhuanlan.com/topic/2356748910\n\n    //TODO tip 2: Jetpack 通过 \"工厂模式\" 实现 ViewModel 作用域可控，\n    //目前我们在项目中提供了 Application、Activity、Fragment 三个级别的作用域，\n    //值得注意的是，通过不同作用域 Provider 获得 ViewModel 实例非同一个，\n    //故若 ViewModel 状态信息保留不符合预期，可从该角度出发排查 是否眼前 ViewModel 实例非目标实例所致。\n\n    //如这么说无体会，详见 https://xiaozhuanlan.com/topic/6257931840\n\n    protected <T extends ViewModel> T getFragmentScopeViewModel(@NonNull Class<T> modelClass) {\n        return mViewModelScope.getFragmentScopeViewModel(this, modelClass);\n    }\n\n    protected <T extends ViewModel> T getActivityScopeViewModel(@NonNull Class<T> modelClass) {\n        return mViewModelScope.getActivityScopeViewModel(mActivity, modelClass);\n    }\n\n    protected <T extends ViewModel> T getApplicationScopeViewModel(@NonNull Class<T> modelClass) {\n        return mViewModelScope.getApplicationScopeViewModel(modelClass);\n    }\n\n    protected NavController nav() {\n        return NavHostFragment.findNavController(this);\n    }\n\n    protected void toggleSoftInput() {\n        InputMethodManager imm = (InputMethodManager) mActivity.getSystemService(Activity.INPUT_METHOD_SERVICE);\n        imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS);\n    }\n\n    protected void openUrlInBrowser(String url) {\n        Uri uri = Uri.parse(url);\n        Intent intent = new Intent(Intent.ACTION_VIEW, uri);\n        startActivity(intent);\n    }\n\n    protected Context getApplicationContext() {\n        return mActivity.getApplicationContext();\n    }\n\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/ui/page/StateHolder.java",
    "content": "package com.kunminx.architecture.ui.page;\n\nimport androidx.lifecycle.ViewModel;\n\n/**\n * Create by KunMinX at 2022/8/11\n */\npublic class StateHolder extends ViewModel {\n\n    //TODO tip 6：每个页面都需单独准备一个 state-ViewModel，托管与 \"控件属性\" 发生绑定的 State，\n    // 此外，state-ViewModel 职责仅限于状态托管和保存恢复，不建议在此处理 UI 逻辑，\n\n    // UI 逻辑和业务逻辑，本质区别在于，前者是数据的消费者，后者是数据的生产者，\n    // 数据总是来自领域层业务逻辑的处理，并单向回推至 UI 层，在 UI 层中响应数据的变化（也即处理 UI 逻辑），\n    // 换言之，UI 逻辑只适合在 Activity/Fragment 等视图控制器中编写，将来升级到 Jetpack Compose 更是如此。\n\n    //如这么说无体会，详见 https://xiaozhuanlan.com/topic/6741932805\n\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/utils/AdaptScreenUtils.java",
    "content": "package com.kunminx.architecture.utils;\n\nimport android.content.res.Resources;\nimport android.util.DisplayMetrics;\nimport android.util.Log;\n\nimport java.lang.reflect.Field;\n\n/**\n * <pre>\n *     author: Blankj\n *     blog  : http://blankj.com\n *     time  : 2016/09/23\n *     desc  : AdaptScreenUtils\n * </pre>\n */\npublic final class AdaptScreenUtils {\n\n    private static boolean isInitMiui = false;\n    private static Field mTmpMetricsField;\n\n    /**\n     * Adapt for the horizontal screen, and call it in [android.app.Activity.getResources].\n     */\n    public static Resources adaptWidth(Resources resources, int designWidth) {\n        DisplayMetrics dm = getDisplayMetrics(resources);\n        float newXdpi = dm.xdpi = (dm.widthPixels * 72f) / designWidth;\n        setAppDmXdpi(newXdpi);\n        return resources;\n    }\n\n    /**\n     * Adapt for the vertical screen, and call it in [android.app.Activity.getResources].\n     */\n    public static Resources adaptHeight(Resources resources, int designHeight) {\n        DisplayMetrics dm = getDisplayMetrics(resources);\n        float newXdpi = dm.xdpi = (dm.heightPixels * 72f) / designHeight;\n        setAppDmXdpi(newXdpi);\n        return resources;\n    }\n\n    /**\n     * @param resources The resources.\n     * @return the resource\n     */\n    public static Resources closeAdapt(Resources resources) {\n        DisplayMetrics dm = getDisplayMetrics(resources);\n        float newXdpi = dm.xdpi = dm.density * 72;\n        setAppDmXdpi(newXdpi);\n        return resources;\n    }\n\n    /**\n     * Value of pt to value of px.\n     *\n     * @param ptValue The value of pt.\n     * @return value of px\n     */\n    public static int pt2Px(float ptValue) {\n        DisplayMetrics metrics = Utils.getApp().getResources().getDisplayMetrics();\n        return (int) (ptValue * metrics.xdpi / 72f + 0.5);\n    }\n\n    /**\n     * Value of px to value of pt.\n     *\n     * @param pxValue The value of px.\n     * @return value of pt\n     */\n    public static int px2Pt(float pxValue) {\n        DisplayMetrics metrics = Utils.getApp().getResources().getDisplayMetrics();\n        return (int) (pxValue * 72 / metrics.xdpi + 0.5);\n    }\n\n    private static void setAppDmXdpi(final float xdpi) {\n        Utils.getApp().getResources().getDisplayMetrics().xdpi = xdpi;\n    }\n\n    private static DisplayMetrics getDisplayMetrics(Resources resources) {\n        DisplayMetrics miuiDisplayMetrics = getMiuiTmpMetrics(resources);\n        if (miuiDisplayMetrics == null) {\n            return resources.getDisplayMetrics();\n        }\n        return miuiDisplayMetrics;\n    }\n\n    private static DisplayMetrics getMiuiTmpMetrics(Resources resources) {\n        if (!isInitMiui) {\n            DisplayMetrics ret = null;\n            String simpleName = resources.getClass().getSimpleName();\n            if (\"MiuiResources\".equals(simpleName) || \"XResources\".equals(simpleName)) {\n                try {\n                    //noinspection JavaReflectionMemberAccess\n                    mTmpMetricsField = Resources.class.getDeclaredField(\"mTmpMetrics\");\n                    mTmpMetricsField.setAccessible(true);\n                    ret = (DisplayMetrics) mTmpMetricsField.get(resources);\n                } catch (Exception e) {\n                    Log.e(\"AdaptScreenUtils\", \"no field of mTmpMetrics in resources.\");\n                }\n            }\n            isInitMiui = true;\n            return ret;\n        }\n        if (mTmpMetricsField == null) {\n            return null;\n        }\n        try {\n            return (DisplayMetrics) mTmpMetricsField.get(resources);\n        } catch (Exception e) {\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/utils/BarUtils.java",
    "content": "package com.kunminx.architecture.utils;\n\nimport static android.Manifest.permission.EXPAND_STATUS_BAR;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.content.ContextWrapper;\nimport android.content.res.Resources;\nimport android.graphics.Color;\nimport android.graphics.Point;\nimport android.os.Build;\nimport android.util.Log;\nimport android.util.TypedValue;\nimport android.view.Display;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.view.ViewGroup.MarginLayoutParams;\nimport android.view.Window;\nimport android.view.WindowManager;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.RequiresApi;\nimport androidx.annotation.RequiresPermission;\nimport androidx.appcompat.app.AppCompatActivity;\nimport androidx.drawerlayout.widget.DrawerLayout;\n\nimport java.lang.reflect.Method;\n\n/**\n * <pre>\n *     author: Blankj\n *     blog  : http://blankj.com\n *     time  : 2016/09/23\n *     desc  : utils about bar\n * </pre>\n */\npublic final class BarUtils {\n\n    ///////////////////////////////////////////////////////////////////////////\n    // status bar\n    ///////////////////////////////////////////////////////////////////////////\n\n    private static final String TAG_STATUS_BAR = \"TAG_STATUS_BAR\";\n    private static final String TAG_OFFSET = \"TAG_OFFSET\";\n    private static final int KEY_OFFSET = -123;\n\n    private BarUtils() {\n        throw new UnsupportedOperationException(\"u can't instantiate me...\");\n    }\n\n    /**\n     * Return the status bar's height.\n     *\n     * @return the status bar's height\n     */\n    public static int getStatusBarHeight() {\n        Resources resources = Resources.getSystem();\n        int resourceId = resources.getIdentifier(\"status_bar_height\", \"dimen\", \"android\");\n        return resources.getDimensionPixelSize(resourceId);\n    }\n\n    /**\n     * Set the status bar's visibility.\n     *\n     * @param activity  The activity.\n     * @param isVisible True to set status bar visible, false otherwise.\n     */\n    public static void setStatusBarVisibility(@NonNull final AppCompatActivity activity,\n                                              final boolean isVisible) {\n        setStatusBarVisibility(activity.getWindow(), isVisible);\n    }\n\n    /**\n     * Set the status bar's visibility.\n     *\n     * @param window    The window.\n     * @param isVisible True to set status bar visible, false otherwise.\n     */\n    public static void setStatusBarVisibility(@NonNull final Window window,\n                                              final boolean isVisible) {\n        if (isVisible) {\n            window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);\n            showStatusBarView(window);\n            addMarginTopEqualStatusBarHeight(window);\n        } else {\n            window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);\n            hideStatusBarView(window);\n            subtractMarginTopEqualStatusBarHeight(window);\n        }\n    }\n\n    /**\n     * Return whether the status bar is visible.\n     *\n     * @param activity The activity.\n     * @return {@code true}: yes<br>{@code false}: no\n     */\n    public static boolean isStatusBarVisible(@NonNull final AppCompatActivity activity) {\n        int flags = activity.getWindow().getAttributes().flags;\n        return (flags & WindowManager.LayoutParams.FLAG_FULLSCREEN) == 0;\n    }\n\n    /**\n     * Set the status bar's light mode.\n     *\n     * @param activity    The activity.\n     * @param isLightMode True to set status bar light mode, false otherwise.\n     */\n    public static void setStatusBarLightMode(@NonNull final AppCompatActivity activity,\n                                             final boolean isLightMode) {\n        setStatusBarLightMode(activity.getWindow(), isLightMode);\n    }\n\n    /**\n     * Set the status bar's light mode.\n     *\n     * @param window      The window.\n     * @param isLightMode True to set status bar light mode, false otherwise.\n     */\n    public static void setStatusBarLightMode(@NonNull final Window window,\n                                             final boolean isLightMode) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n            View decorView = window.getDecorView();\n            if (decorView != null) {\n                int vis = decorView.getSystemUiVisibility();\n                if (isLightMode) {\n                    vis |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;\n                } else {\n                    vis &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;\n                }\n                decorView.setSystemUiVisibility(vis);\n            }\n        }\n    }\n\n    /**\n     * Is the status bar light mode.\n     *\n     * @param activity The activity.\n     * @return {@code true}: yes<br>{@code false}: no\n     */\n    public static boolean isStatusBarLightMode(@NonNull final AppCompatActivity activity) {\n        return isStatusBarLightMode(activity.getWindow());\n    }\n\n    /**\n     * Is the status bar light mode.\n     *\n     * @param window The window.\n     * @return {@code true}: yes<br>{@code false}: no\n     */\n    public static boolean isStatusBarLightMode(@NonNull final Window window) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n            View decorView = window.getDecorView();\n            if (decorView != null) {\n                int vis = decorView.getSystemUiVisibility();\n                return (vis & View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) != 0;\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Add the top margin size equals status bar's height for view.\n     *\n     * @param view The view.\n     */\n    public static void addMarginTopEqualStatusBarHeight(@NonNull View view) {\n        view.setTag(TAG_OFFSET);\n        Object haveSetOffset = view.getTag(KEY_OFFSET);\n        if (haveSetOffset != null && (Boolean) haveSetOffset) {\n            return;\n        }\n        MarginLayoutParams layoutParams = (MarginLayoutParams) view.getLayoutParams();\n        layoutParams.setMargins(layoutParams.leftMargin,\n            layoutParams.topMargin + getStatusBarHeight(),\n            layoutParams.rightMargin,\n            layoutParams.bottomMargin);\n        view.setTag(KEY_OFFSET, true);\n    }\n\n    /**\n     * Subtract the top margin size equals status bar's height for view.\n     *\n     * @param view The view.\n     */\n    public static void subtractMarginTopEqualStatusBarHeight(@NonNull View view) {\n        Object haveSetOffset = view.getTag(KEY_OFFSET);\n        if (haveSetOffset == null || !(Boolean) haveSetOffset) {\n            return;\n        }\n        MarginLayoutParams layoutParams = (MarginLayoutParams) view.getLayoutParams();\n        layoutParams.setMargins(layoutParams.leftMargin,\n            layoutParams.topMargin - getStatusBarHeight(),\n            layoutParams.rightMargin,\n            layoutParams.bottomMargin);\n        view.setTag(KEY_OFFSET, false);\n    }\n\n    private static void addMarginTopEqualStatusBarHeight(final Window window) {\n        View withTag = window.getDecorView().findViewWithTag(TAG_OFFSET);\n        if (withTag == null) {\n            return;\n        }\n        addMarginTopEqualStatusBarHeight(withTag);\n    }\n\n    private static void subtractMarginTopEqualStatusBarHeight(final Window window) {\n        View withTag = window.getDecorView().findViewWithTag(TAG_OFFSET);\n        if (withTag == null) {\n            return;\n        }\n        subtractMarginTopEqualStatusBarHeight(withTag);\n    }\n\n    /**\n     * Set the status bar's color.\n     *\n     * @param activity The activity.\n     * @param color    The status bar's color.\n     */\n    public static View setStatusBarColor(@NonNull final AppCompatActivity activity,\n                                         @ColorInt final int color) {\n        return setStatusBarColor(activity, color, false);\n    }\n\n    /**\n     * Set the status bar's color.\n     *\n     * @param activity The activity.\n     * @param color    The status bar's color.\n     * @param isDecor  True to add fake status bar in DecorView,\n     *                 false to add fake status bar in ContentView.\n     */\n    public static View setStatusBarColor(@NonNull final AppCompatActivity activity,\n                                         @ColorInt final int color,\n                                         final boolean isDecor) {\n        transparentStatusBar(activity);\n        return applyStatusBarColor(activity, color, isDecor);\n    }\n\n    /**\n     * Set the status bar's color.\n     *\n     * @param fakeStatusBar The fake status bar view.\n     * @param color         The status bar's color.\n     */\n    public static void setStatusBarColor(@NonNull final View fakeStatusBar,\n                                         @ColorInt final int color) {\n        AppCompatActivity activity = getActivityByView(fakeStatusBar);\n        if (activity == null) {\n            return;\n        }\n        transparentStatusBar(activity);\n        fakeStatusBar.setVisibility(View.VISIBLE);\n        ViewGroup.LayoutParams layoutParams = fakeStatusBar.getLayoutParams();\n        layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;\n        layoutParams.height = getStatusBarHeight();\n        fakeStatusBar.setBackgroundColor(color);\n    }\n\n    /**\n     * Set the custom status bar.\n     *\n     * @param fakeStatusBar The fake status bar view.\n     */\n    public static void setStatusBarCustom(@NonNull final View fakeStatusBar) {\n        AppCompatActivity activity = getActivityByView(fakeStatusBar);\n        if (activity == null) {\n            return;\n        }\n        transparentStatusBar(activity);\n        fakeStatusBar.setVisibility(View.VISIBLE);\n        ViewGroup.LayoutParams layoutParams = fakeStatusBar.getLayoutParams();\n        if (layoutParams == null) {\n            layoutParams = new ViewGroup.LayoutParams(\n                ViewGroup.LayoutParams.MATCH_PARENT,\n                getStatusBarHeight()\n            );\n            fakeStatusBar.setLayoutParams(layoutParams);\n        } else {\n            layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;\n            layoutParams.height = getStatusBarHeight();\n        }\n    }\n\n    /**\n     * Set the status bar's color for DrawerLayout.\n     * <p>DrawLayout must add {@code android:fitsSystemWindows=\"true\"}</p>\n     *\n     * @param drawer        The DrawLayout.\n     * @param fakeStatusBar The fake status bar view.\n     * @param color         The status bar's color.\n     */\n    public static void setStatusBarColor4Drawer(@NonNull final DrawerLayout drawer,\n                                                @NonNull final View fakeStatusBar,\n                                                @ColorInt final int color) {\n        setStatusBarColor4Drawer(drawer, fakeStatusBar, color, false);\n    }\n\n    /**\n     * Set the status bar's color for DrawerLayout.\n     * <p>DrawLayout must add {@code android:fitsSystemWindows=\"true\"}</p>\n     *\n     * @param drawer        The DrawLayout.\n     * @param fakeStatusBar The fake status bar view.\n     * @param color         The status bar's color.\n     * @param isTop         True to set DrawerLayout at the top layer, false otherwise.\n     */\n    public static void setStatusBarColor4Drawer(@NonNull final DrawerLayout drawer,\n                                                @NonNull final View fakeStatusBar,\n                                                @ColorInt final int color,\n                                                final boolean isTop) {\n        AppCompatActivity activity = getActivityByView(fakeStatusBar);\n        if (activity == null) {\n            return;\n        }\n        transparentStatusBar(activity);\n        drawer.setFitsSystemWindows(false);\n        setStatusBarColor(fakeStatusBar, color);\n        for (int i = 0, count = drawer.getChildCount(); i < count; i++) {\n            drawer.getChildAt(i).setFitsSystemWindows(false);\n        }\n        if (isTop) {\n            hideStatusBarView(activity);\n        } else {\n            setStatusBarColor(activity, color, false);\n        }\n    }\n\n    private static View applyStatusBarColor(final AppCompatActivity activity,\n                                            final int color,\n                                            boolean isDecor) {\n        ViewGroup parent = isDecor ?\n            (ViewGroup) activity.getWindow().getDecorView() :\n            (ViewGroup) activity.findViewById(android.R.id.content);\n        View fakeStatusBarView = parent.findViewWithTag(TAG_STATUS_BAR);\n        if (fakeStatusBarView != null) {\n            if (fakeStatusBarView.getVisibility() == View.GONE) {\n                fakeStatusBarView.setVisibility(View.VISIBLE);\n            }\n            fakeStatusBarView.setBackgroundColor(color);\n        } else {\n            fakeStatusBarView = createStatusBarView(activity, color);\n            parent.addView(fakeStatusBarView);\n        }\n        return fakeStatusBarView;\n    }\n\n    private static void hideStatusBarView(final AppCompatActivity activity) {\n        hideStatusBarView(activity.getWindow());\n    }\n\n    private static void hideStatusBarView(final Window window) {\n        ViewGroup decorView = (ViewGroup) window.getDecorView();\n        View fakeStatusBarView = decorView.findViewWithTag(TAG_STATUS_BAR);\n        if (fakeStatusBarView == null) {\n            return;\n        }\n        fakeStatusBarView.setVisibility(View.GONE);\n    }\n\n    private static void showStatusBarView(final Window window) {\n        ViewGroup decorView = (ViewGroup) window.getDecorView();\n        View fakeStatusBarView = decorView.findViewWithTag(TAG_STATUS_BAR);\n        if (fakeStatusBarView == null) {\n            return;\n        }\n        fakeStatusBarView.setVisibility(View.VISIBLE);\n    }\n\n    private static View createStatusBarView(final AppCompatActivity activity,\n                                            final int color) {\n        View statusBarView = new View(activity);\n        statusBarView.setLayoutParams(new ViewGroup.LayoutParams(\n            ViewGroup.LayoutParams.MATCH_PARENT, getStatusBarHeight()));\n        statusBarView.setBackgroundColor(color);\n        statusBarView.setTag(TAG_STATUS_BAR);\n        return statusBarView;\n    }\n\n    private static void transparentStatusBar(final AppCompatActivity activity) {\n        Window window = activity.getWindow();\n        window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);\n        int option = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n            int vis = window.getDecorView().getSystemUiVisibility() & View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;\n            window.getDecorView().setSystemUiVisibility(option | vis);\n        } else {\n            window.getDecorView().setSystemUiVisibility(option);\n        }\n        window.setStatusBarColor(Color.TRANSPARENT);\n    }\n\n    ///////////////////////////////////////////////////////////////////////////\n    // action bar\n    ///////////////////////////////////////////////////////////////////////////\n\n    /**\n     * Return the action bar's height.\n     *\n     * @return the action bar's height\n     */\n    public static int getActionBarHeight() {\n        TypedValue tv = new TypedValue();\n        if (Utils.getApp().getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true)) {\n            return TypedValue.complexToDimensionPixelSize(\n                tv.data, Utils.getApp().getResources().getDisplayMetrics()\n            );\n        }\n        return 0;\n    }\n\n    ///////////////////////////////////////////////////////////////////////////\n    // notification bar\n    ///////////////////////////////////////////////////////////////////////////\n\n    /**\n     * Set the notification bar's visibility.\n     * <p>Must hold {@code <uses-permission android:name=\"android.permission.EXPAND_STATUS_BAR\" />}</p>\n     *\n     * @param isVisible True to set notification bar visible, false otherwise.\n     */\n    @RequiresPermission(EXPAND_STATUS_BAR)\n    public static void setNotificationBarVisibility(final boolean isVisible) {\n        String methodName;\n        if (isVisible) {\n            methodName = \"expandNotificationsPanel\";\n        } else {\n            methodName = \"collapsePanels\";\n        }\n        invokePanels(methodName);\n    }\n\n    private static void invokePanels(final String methodName) {\n        try {\n            @SuppressLint(\"WrongConstant\")\n            Object service = Utils.getApp().getSystemService(\"statusbar\");\n            @SuppressLint(\"PrivateApi\")\n            Class<?> statusBarManager = Class.forName(\"android.app.StatusBarManager\");\n            Method expand = statusBarManager.getMethod(methodName);\n            expand.invoke(service);\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n    }\n\n    ///////////////////////////////////////////////////////////////////////////\n    // navigation bar\n    ///////////////////////////////////////////////////////////////////////////\n\n    /**\n     * Return the navigation bar's height.\n     *\n     * @return the navigation bar's height\n     */\n    public static int getNavBarHeight() {\n        Resources res = Resources.getSystem();\n        int resourceId = res.getIdentifier(\"navigation_bar_height\", \"dimen\", \"android\");\n        if (resourceId != 0) {\n            return res.getDimensionPixelSize(resourceId);\n        } else {\n            return 0;\n        }\n    }\n\n    /**\n     * Set the navigation bar's visibility.\n     *\n     * @param activity  The activity.\n     * @param isVisible True to set navigation bar visible, false otherwise.\n     */\n    public static void setNavBarVisibility(@NonNull final AppCompatActivity activity, boolean isVisible) {\n        setNavBarVisibility(activity.getWindow(), isVisible);\n    }\n\n    /**\n     * Set the navigation bar's visibility.\n     *\n     * @param window    The window.\n     * @param isVisible True to set navigation bar visible, false otherwise.\n     */\n    public static void setNavBarVisibility(@NonNull final Window window, boolean isVisible) {\n        final ViewGroup decorView = (ViewGroup) window.getDecorView();\n        for (int i = 0, count = decorView.getChildCount(); i < count; i++) {\n            final View child = decorView.getChildAt(i);\n            final int id = child.getId();\n            if (id != View.NO_ID) {\n                String resourceEntryName = Utils.getApp()\n                    .getResources()\n                    .getResourceEntryName(id);\n                if (\"navigationBarBackground\".equals(resourceEntryName)) {\n                    child.setVisibility(isVisible ? View.VISIBLE : View.INVISIBLE);\n                }\n            }\n        }\n        final int uiOptions = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION\n            | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION\n            | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;\n        if (isVisible) {\n            decorView.setSystemUiVisibility(decorView.getSystemUiVisibility() & ~uiOptions);\n        } else {\n            decorView.setSystemUiVisibility(decorView.getSystemUiVisibility() | uiOptions);\n        }\n    }\n\n    /**\n     * Return whether the navigation bar visible.\n     * <p>Call it in onWindowFocusChanged will get right result.</p>\n     *\n     * @param activity The activity.\n     * @return {@code true}: yes<br>{@code false}: no\n     */\n    public static boolean isNavBarVisible(@NonNull final AppCompatActivity activity) {\n        return isNavBarVisible(activity.getWindow());\n    }\n\n    /**\n     * Return whether the navigation bar visible.\n     * <p>Call it in onWindowFocusChanged will get right result.</p>\n     *\n     * @param window The window.\n     * @return {@code true}: yes<br>{@code false}: no\n     */\n    public static boolean isNavBarVisible(@NonNull final Window window) {\n        boolean isVisible = false;\n        ViewGroup decorView = (ViewGroup) window.getDecorView();\n        for (int i = 0, count = decorView.getChildCount(); i < count; i++) {\n            final View child = decorView.getChildAt(i);\n            final int id = child.getId();\n            if (id != View.NO_ID) {\n                String resourceEntryName = Utils.getApp()\n                    .getResources()\n                    .getResourceEntryName(id);\n                if (\"navigationBarBackground\".equals(resourceEntryName)\n                    && child.getVisibility() == View.VISIBLE) {\n                    isVisible = true;\n                    break;\n                }\n            }\n        }\n        if (isVisible) {\n            int visibility = decorView.getSystemUiVisibility();\n            isVisible = (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0;\n        }\n        return isVisible;\n    }\n\n    /**\n     * Set the navigation bar's color.\n     *\n     * @param activity The activity.\n     * @param color    The navigation bar's color.\n     */\n    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)\n    public static void setNavBarColor(@NonNull final AppCompatActivity activity, @ColorInt final int color) {\n        setNavBarColor(activity.getWindow(), color);\n    }\n\n    /**\n     * Set the navigation bar's color.\n     *\n     * @param window The window.\n     * @param color  The navigation bar's color.\n     */\n    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)\n    public static void setNavBarColor(@NonNull final Window window, @ColorInt final int color) {\n        window.setNavigationBarColor(color);\n    }\n\n    /**\n     * Return the color of navigation bar.\n     *\n     * @param activity The activity.\n     * @return the color of navigation bar\n     */\n    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)\n    public static int getNavBarColor(@NonNull final AppCompatActivity activity) {\n        return getNavBarColor(activity.getWindow());\n    }\n\n    /**\n     * Return the color of navigation bar.\n     *\n     * @param window The window.\n     * @return the color of navigation bar\n     */\n    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)\n    public static int getNavBarColor(@NonNull final Window window) {\n        return window.getNavigationBarColor();\n    }\n\n    /**\n     * Return whether the navigation bar visible.\n     *\n     * @return {@code true}: yes<br>{@code false}: no\n     */\n    public static boolean isSupportNavBar() {\n        WindowManager wm = (WindowManager) Utils.getApp().getSystemService(Context.WINDOW_SERVICE);\n        if (wm == null) {\n            return false;\n        }\n        Display display = wm.getDefaultDisplay();\n        Point size = new Point();\n        Point realSize = new Point();\n        display.getSize(size);\n        display.getRealSize(realSize);\n        return realSize.y != size.y || realSize.x != size.x;\n    }\n\n    private static AppCompatActivity getActivityByView(@NonNull final View view) {\n        Context context = view.getContext();\n        while (context instanceof ContextWrapper) {\n            if (context instanceof AppCompatActivity) {\n                return (AppCompatActivity) context;\n            }\n            context = ((ContextWrapper) context).getBaseContext();\n        }\n        Log.e(\"BarUtils\", \"the view's Context is not an Activity.\");\n        return null;\n    }\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/utils/ClickUtils.java",
    "content": "package com.kunminx.architecture.utils;\n\nimport android.view.View;\n\nimport androidx.annotation.IntRange;\nimport androidx.annotation.NonNull;\n\n/**\n * <pre>\n *     author: Blankj\n *     blog  : http://blankj.com\n *     time  : 2019/06/12\n *     desc  : utils about click\n * </pre>\n */\npublic class ClickUtils {\n\n    private static final int DEBOUNCING_TAG = -7;\n    private static final long DEBOUNCING_DEFAULT_VALUE = 700;\n\n    private ClickUtils() {\n        throw new UnsupportedOperationException(\"u can't instantiate me...\");\n    }\n\n    /**\n     * Apply single debouncing for the view's click.\n     *\n     * @param view     The view.\n     * @param listener The listener.\n     */\n    public static void applySingleDebouncing(final View view, final View.OnClickListener listener) {\n        applySingleDebouncing(new View[]{view}, listener);\n    }\n\n    /**\n     * Apply single debouncing for the views' click.\n     *\n     * @param views    The views.\n     * @param listener The listener.\n     */\n    public static void applySingleDebouncing(final View[] views, final View.OnClickListener listener) {\n        applySingleDebouncing(views, DEBOUNCING_DEFAULT_VALUE, listener);\n    }\n\n    /**\n     * Apply single debouncing for the views' click.\n     *\n     * @param views    The views.\n     * @param duration The duration of debouncing.\n     * @param listener The listener.\n     */\n    public static void applySingleDebouncing(final View[] views,\n                                             @IntRange(from = 0) long duration,\n                                             final View.OnClickListener listener) {\n        applyDebouncing(views, false, duration, listener);\n    }\n\n    private static void applyDebouncing(final View[] views,\n                                        final boolean isGlobal,\n                                        @IntRange(from = 0) long duration,\n                                        final View.OnClickListener listener) {\n        if (views == null || views.length == 0 || listener == null) {\n            return;\n        }\n        for (View view : views) {\n            if (view == null) {\n                continue;\n            }\n            view.setOnClickListener(new OnDebouncingClickListener(isGlobal, duration) {\n                @Override\n                public void onDebouncingClick(View v) {\n                    listener.onClick(v);\n                }\n            });\n        }\n    }\n\n    public static abstract class OnDebouncingClickListener implements View.OnClickListener {\n\n        private static boolean mEnabled = true;\n\n        private static final Runnable ENABLE_AGAIN = () -> mEnabled = true;\n        private final long mDuration;\n        private final boolean mIsGlobal;\n\n        public OnDebouncingClickListener() {\n            this(true, DEBOUNCING_DEFAULT_VALUE);\n        }\n\n        public OnDebouncingClickListener(final boolean isGlobal) {\n            this(isGlobal, DEBOUNCING_DEFAULT_VALUE);\n        }\n\n        public OnDebouncingClickListener(final long duration) {\n            this(true, duration);\n        }\n\n        public OnDebouncingClickListener(final boolean isGlobal, final long duration) {\n            mIsGlobal = isGlobal;\n            mDuration = duration;\n        }\n\n        private static boolean isValid(@NonNull final View view, final long duration) {\n            long curTime = System.currentTimeMillis();\n            Object tag = view.getTag(DEBOUNCING_TAG);\n            if (!(tag instanceof Long)) {\n                view.setTag(DEBOUNCING_TAG, curTime);\n                return true;\n            }\n            long preTime = (Long) tag;\n            if (curTime - preTime <= duration) {\n                return false;\n            }\n            view.setTag(DEBOUNCING_TAG, curTime);\n            return true;\n        }\n\n        public abstract void onDebouncingClick(View v);\n\n        @Override\n        public final void onClick(View v) {\n            if (mIsGlobal) {\n                if (mEnabled) {\n                    mEnabled = false;\n                    v.postDelayed(ENABLE_AGAIN, mDuration);\n                    onDebouncingClick(v);\n                }\n            } else {\n                if (isValid(v, mDuration)) {\n                    onDebouncingClick(v);\n                }\n            }\n        }\n    }\n\n\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/utils/DisplayUtils.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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\npackage com.kunminx.architecture.utils;\n\n/**\n * Create by KunMinX at 19/7/20\n */\n\npublic class DisplayUtils {\n\n    /**\n     * convert px to its equivalent dp\n     * <p>\n     * 将px转换为与之相等的dp\n     */\n    public static int px2dp(float pxValue) {\n        final float scale = Utils.getApp().getResources().getDisplayMetrics().density;\n        return (int) (pxValue / scale + 0.5f);\n    }\n\n    /**\n     * convert dp to its equivalent px\n     * <p>\n     * 将dp转换为与之相等的px\n     */\n    public static int dp2px(float dipValue) {\n        final float scale = Utils.getApp().getResources().getDisplayMetrics().density;\n        return (int) (dipValue * scale + 0.5f);\n    }\n\n    /**\n     * convert px to its equivalent sp\n     * <p>\n     * 将px转换为sp\n     */\n    public static int px2sp(float pxValue) {\n        final float fontScale = Utils.getApp().getResources().getDisplayMetrics().scaledDensity;\n        return (int) (pxValue / fontScale + 0.5f);\n    }\n\n    /**\n     * convert sp to its equivalent px\n     * <p>\n     * 将sp转换为px\n     */\n    public static int sp2px(float spValue) {\n        final float fontScale = Utils.getApp().getResources().getDisplayMetrics().scaledDensity;\n        return (int) (spValue * fontScale + 0.5f);\n    }\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/utils/ImageUtils.java",
    "content": "package com.kunminx.architecture.utils;\n\nimport android.graphics.Bitmap;\nimport android.graphics.BitmapFactory;\n\n/**\n * <pre>\n *     author: Blankj\n *     blog  : http://blankj.com\n *     time  : 2016/08/12\n *     desc  : utils about image\n * </pre>\n */\npublic final class ImageUtils {\n\n    /**\n     * Return bitmap.\n     *\n     * @param filePath The path of file.\n     * @return bitmap\n     */\n    public static Bitmap getBitmap(final String filePath) {\n        if (isSpace(filePath)) {\n            return null;\n        }\n        return BitmapFactory.decodeFile(filePath);\n    }\n\n    private static boolean isSpace(final String s) {\n        if (s == null) {\n            return true;\n        }\n        for (int i = 0, len = s.length(); i < len; ++i) {\n            if (!Character.isWhitespace(s.charAt(i))) {\n                return false;\n            }\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/utils/NetworkUtils.java",
    "content": "package com.kunminx.architecture.utils;\n\nimport static android.Manifest.permission.ACCESS_NETWORK_STATE;\n\nimport android.content.Context;\nimport android.net.ConnectivityManager;\nimport android.net.NetworkInfo;\n\nimport androidx.annotation.RequiresPermission;\n\n/**\n * <pre>\n *     author: Blankj\n *     blog  : http://blankj.com\n *     time  : 2016/08/02\n *     desc  : utils about network\n * </pre>\n */\npublic final class NetworkUtils {\n\n    /**\n     * Return whether network is connected.\n     * <p>Must hold {@code <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />}</p>\n     *\n     * @return {@code true}: connected<br>{@code false}: disconnected\n     */\n    @RequiresPermission(ACCESS_NETWORK_STATE)\n    public static boolean isConnected() {\n        NetworkInfo info = getActiveNetworkInfo();\n        return info != null && info.isConnected();\n    }\n\n    @RequiresPermission(ACCESS_NETWORK_STATE)\n    private static NetworkInfo getActiveNetworkInfo() {\n        ConnectivityManager cm =\n            (ConnectivityManager) Utils.getApp().getSystemService(Context.CONNECTIVITY_SERVICE);\n        if (cm == null) {\n            return null;\n        }\n        return cm.getActiveNetworkInfo();\n    }\n\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/utils/Res.java",
    "content": "package com.kunminx.architecture.utils;\n\nimport android.graphics.drawable.Drawable;\n\nimport androidx.core.content.ContextCompat;\n\nimport java.util.Objects;\n/**\n * Create by KunMinX at 2023/6/5\n */\npublic class Res {\n    public static Drawable getDrawable(int resId) {\n        return Objects.requireNonNull(ContextCompat.getDrawable(Utils.getApp(), resId));\n    }\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/utils/ScreenUtils.java",
    "content": "package com.kunminx.architecture.utils;\n\nimport static android.Manifest.permission.WRITE_SETTINGS;\n\nimport android.annotation.SuppressLint;\nimport android.app.KeyguardManager;\nimport android.content.Context;\nimport android.content.pm.ActivityInfo;\nimport android.content.res.Configuration;\nimport android.content.res.Resources;\nimport android.graphics.Bitmap;\nimport android.graphics.Point;\nimport android.provider.Settings;\nimport android.util.DisplayMetrics;\nimport android.view.Surface;\nimport android.view.View;\nimport android.view.Window;\nimport android.view.WindowManager;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.RequiresPermission;\nimport androidx.appcompat.app.AppCompatActivity;\n\n/**\n * <pre>\n *     author: Blankj\n *     blog  : http://blankj.com\n *     time  : 2016/08/02\n *     desc  : utils about screen\n * </pre>\n */\npublic final class ScreenUtils {\n\n    private ScreenUtils() {\n        throw new UnsupportedOperationException(\"u can't instantiate me...\");\n    }\n\n    /**\n     * Return the width of screen, in pixel.\n     *\n     * @return the width of screen, in pixel\n     */\n    public static int getScreenWidth() {\n        WindowManager wm = (WindowManager) Utils.getApp().getSystemService(Context.WINDOW_SERVICE);\n        Point point = new Point();\n        wm.getDefaultDisplay().getRealSize(point);\n        return point.x;\n    }\n\n    /**\n     * Return the height of screen, in pixel.\n     *\n     * @return the height of screen, in pixel\n     */\n    public static int getScreenHeight() {\n        WindowManager wm = (WindowManager) Utils.getApp().getSystemService(Context.WINDOW_SERVICE);\n        Point point = new Point();\n        wm.getDefaultDisplay().getRealSize(point);\n        return point.y;\n    }\n\n    /**\n     * Return the density of screen.\n     *\n     * @return the density of screen\n     */\n    public static float getScreenDensity() {\n        return Resources.getSystem().getDisplayMetrics().density;\n    }\n\n    /**\n     * Return the screen density expressed as dots-per-inch.\n     *\n     * @return the screen density expressed as dots-per-inch\n     */\n    public static int getScreenDensityDpi() {\n        return Resources.getSystem().getDisplayMetrics().densityDpi;\n    }\n\n    /**\n     * Set full screen.\n     *\n     * @param activity The activity.\n     */\n    public static void setFullScreen(@NonNull final AppCompatActivity activity) {\n        activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);\n    }\n\n    /**\n     * Set non full screen.\n     *\n     * @param activity The activity.\n     */\n    public static void setNonFullScreen(@NonNull final AppCompatActivity activity) {\n        activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);\n    }\n\n    /**\n     * Toggle full screen.\n     *\n     * @param activity The activity.\n     */\n    public static void toggleFullScreen(@NonNull final AppCompatActivity activity) {\n        int fullScreenFlag = WindowManager.LayoutParams.FLAG_FULLSCREEN;\n        Window window = activity.getWindow();\n        if ((window.getAttributes().flags & fullScreenFlag) == fullScreenFlag) {\n            window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN\n                | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);\n        } else {\n            window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN\n                | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);\n        }\n    }\n\n    /**\n     * Return whether screen is full.\n     *\n     * @param activity The activity.\n     * @return {@code true}: yes<br>{@code false}: no\n     */\n    public static boolean isFullScreen(@NonNull final AppCompatActivity activity) {\n        int fullScreenFlag = WindowManager.LayoutParams.FLAG_FULLSCREEN;\n        return (activity.getWindow().getAttributes().flags & fullScreenFlag) == fullScreenFlag;\n    }\n\n    /**\n     * Return whether screen is landscape.\n     *\n     * @return {@code true}: yes<br>{@code false}: no\n     */\n    public static boolean isLandscape() {\n        return Utils.getApp().getResources().getConfiguration().orientation\n            == Configuration.ORIENTATION_LANDSCAPE;\n    }\n\n    /**\n     * Set the screen to landscape.\n     *\n     * @param activity The activity.\n     */\n    public static void setLandscape(@NonNull final AppCompatActivity activity) {\n        activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);\n    }\n\n    /**\n     * Return whether screen is portrait.\n     *\n     * @return {@code true}: yes<br>{@code false}: no\n     */\n    public static boolean isPortrait() {\n        return Utils.getApp().getResources().getConfiguration().orientation\n            == Configuration.ORIENTATION_PORTRAIT;\n    }\n\n    /**\n     * Set the screen to portrait.\n     *\n     * @param activity The activity.\n     */\n    public static void setPortrait(@NonNull final AppCompatActivity activity) {\n        activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);\n    }\n\n    /**\n     * Return the rotation of screen.\n     *\n     * @param activity The activity.\n     * @return the rotation of screen\n     */\n    @SuppressLint(\"SwitchIntDef\")\n    public static int getScreenRotation(@NonNull final AppCompatActivity activity) {\n        switch (activity.getWindowManager().getDefaultDisplay().getRotation()) {\n            case Surface.ROTATION_90:\n                return 90;\n            case Surface.ROTATION_180:\n                return 180;\n            case Surface.ROTATION_270:\n                return 270;\n            default:\n                return 0;\n        }\n    }\n\n    /**\n     * Return the bitmap of screen.\n     *\n     * @param activity The activity.\n     * @return the bitmap of screen\n     */\n    public static Bitmap screenShot(@NonNull final AppCompatActivity activity) {\n        return screenShot(activity, false);\n    }\n\n    /**\n     * Return the bitmap of screen.\n     *\n     * @param activity          The activity.\n     * @param isDeleteStatusBar True to delete status bar, false otherwise.\n     * @return the bitmap of screen\n     */\n    public static Bitmap screenShot(@NonNull final AppCompatActivity activity, boolean isDeleteStatusBar) {\n        View decorView = activity.getWindow().getDecorView();\n        decorView.setDrawingCacheEnabled(true);\n        decorView.setWillNotCacheDrawing(false);\n        Bitmap bmp = decorView.getDrawingCache();\n        if (bmp == null) {\n            return null;\n        }\n        DisplayMetrics dm = new DisplayMetrics();\n        activity.getWindowManager().getDefaultDisplay().getMetrics(dm);\n        Bitmap ret;\n        if (isDeleteStatusBar) {\n            Resources resources = activity.getResources();\n            int resourceId = resources.getIdentifier(\"status_bar_height\", \"dimen\", \"android\");\n            int statusBarHeight = resources.getDimensionPixelSize(resourceId);\n            ret = Bitmap.createBitmap(\n                bmp,\n                0,\n                statusBarHeight,\n                dm.widthPixels,\n                dm.heightPixels - statusBarHeight\n            );\n        } else {\n            ret = Bitmap.createBitmap(bmp, 0, 0, dm.widthPixels, dm.heightPixels);\n        }\n        decorView.destroyDrawingCache();\n        return ret;\n    }\n\n    /**\n     * Return whether screen is locked.\n     *\n     * @return {@code true}: yes<br>{@code false}: no\n     */\n    public static boolean isScreenLock() {\n        KeyguardManager km =\n            (KeyguardManager) Utils.getApp().getSystemService(Context.KEYGUARD_SERVICE);\n        return km.inKeyguardRestrictedInputMode();\n    }\n\n    /**\n     * Return the duration of sleep.\n     *\n     * @return the duration of sleep.\n     */\n    public static int getSleepDuration() {\n        try {\n            return Settings.System.getInt(\n                Utils.getApp().getContentResolver(),\n                Settings.System.SCREEN_OFF_TIMEOUT\n            );\n        } catch (Settings.SettingNotFoundException e) {\n            e.printStackTrace();\n            return -123;\n        }\n    }\n\n    /**\n     * Set the duration of sleep.\n     * <p>Must hold {@code <uses-permission android:name=\"android.permission.WRITE_SETTINGS\" />}</p>\n     *\n     * @param duration The duration.\n     */\n    @RequiresPermission(WRITE_SETTINGS)\n    public static void setSleepDuration(final int duration) {\n        Settings.System.putInt(\n            Utils.getApp().getContentResolver(),\n            Settings.System.SCREEN_OFF_TIMEOUT,\n            duration\n        );\n    }\n\n    /**\n     * Return whether device is tablet.\n     *\n     * @return {@code true}: yes<br>{@code false}: no\n     */\n    public static boolean isTablet() {\n        return (Utils.getApp().getResources().getConfiguration().screenLayout\n            & Configuration.SCREENLAYOUT_SIZE_MASK)\n            >= Configuration.SCREENLAYOUT_SIZE_LARGE;\n    }\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/utils/ToastUtils.java",
    "content": "package com.kunminx.architecture.utils;\n\nimport android.widget.Toast;\n\n/**\n * Create by KunMinX at 2021/8/19\n */\npublic class ToastUtils {\n\n    public static void showLongToast(String text) {\n        Toast.makeText(Utils.getApp().getApplicationContext(), text, Toast.LENGTH_LONG).show();\n    }\n\n    public static void showShortToast(String text) {\n        Toast.makeText(Utils.getApp().getApplicationContext(), text, Toast.LENGTH_SHORT).show();\n    }\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/utils/Utils.java",
    "content": "package com.kunminx.architecture.utils;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.app.Application;\nimport android.app.Application.ActivityLifecycleCallbacks;\nimport android.content.Context;\nimport android.os.Bundle;\nimport android.view.View;\nimport android.view.inputmethod.InputMethodManager;\n\nimport androidx.annotation.NonNull;\nimport androidx.core.content.FileProvider;\n\nimport java.lang.reflect.Field;\nimport java.lang.reflect.InvocationTargetException;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.Iterator;\nimport java.util.LinkedList;\nimport java.util.Map;\nimport java.util.Set;\n\n/**\n * <pre>\n *     author:\n *                                      ___           ___           ___         ___\n *         _____                       /  /\\         /__/\\         /__/|       /  /\\\n *        /  /::\\                     /  /::\\        \\  \\:\\       |  |:|      /  /:/\n *       /  /:/\\:\\    ___     ___    /  /:/\\:\\        \\  \\:\\      |  |:|     /__/::\\\n *      /  /:/~/::\\  /__/\\   /  /\\  /  /:/~/::\\   _____\\__\\:\\   __|  |:|     \\__\\/\\:\\\n *     /__/:/ /:/\\:| \\  \\:\\ /  /:/ /__/:/ /:/\\:\\ /__/::::::::\\ /__/\\_|:|____    \\  \\:\\\n *     \\  \\:\\/:/~/:/  \\  \\:\\  /:/  \\  \\:\\/:/__\\/ \\  \\:\\~~\\~~\\/ \\  \\:\\/:::::/     \\__\\:\\\n *      \\  \\::/ /:/    \\  \\:\\/:/    \\  \\::/       \\  \\:\\  ~~~   \\  \\::/~~~~      /  /:/\n *       \\  \\:\\/:/      \\  \\::/      \\  \\:\\        \\  \\:\\        \\  \\:\\         /__/:/\n *        \\  \\::/        \\__\\/        \\  \\:\\        \\  \\:\\        \\  \\:\\        \\__\\/\n *         \\__\\/                       \\__\\/         \\__\\/         \\__\\/\n *     blog  : http://blankj.com\n *     time  : 16/12/08\n *     desc  : utils about initialization\n * </pre>\n */\npublic final class Utils {\n\n    private static final String PERMISSION_ACTIVITY_CLASS_NAME =\n        \"com.blankj.utilcode.util.PermissionUtils$PermissionActivity\";\n\n    private static final ActivityLifecycleImpl ACTIVITY_LIFECYCLE = new ActivityLifecycleImpl();\n\n    @SuppressLint(\"StaticFieldLeak\")\n    private static Application sApplication;\n\n    private Utils() {\n        throw new UnsupportedOperationException(\"u can't instantiate me...\");\n    }\n\n    /**\n     * Init utils.\n     * <p>Init it in the class of Application.</p>\n     *\n     * @param context context\n     */\n    public static void init(final Context context) {\n        if (context == null) {\n            init(getApplicationByReflect());\n            return;\n        }\n        init((Application) context.getApplicationContext());\n    }\n\n    /**\n     * Init utils.\n     * <p>Init it in the class of Application.</p>\n     *\n     * @param app application\n     */\n    public static void init(final Application app) {\n        if (sApplication == null) {\n            if (app == null) {\n                sApplication = getApplicationByReflect();\n            } else {\n                sApplication = app;\n            }\n            sApplication.registerActivityLifecycleCallbacks(ACTIVITY_LIFECYCLE);\n        } else {\n            if (app != null && app.getClass() != sApplication.getClass()) {\n                sApplication.unregisterActivityLifecycleCallbacks(ACTIVITY_LIFECYCLE);\n                ACTIVITY_LIFECYCLE.mActivityList.clear();\n                sApplication = app;\n                sApplication.registerActivityLifecycleCallbacks(ACTIVITY_LIFECYCLE);\n            }\n        }\n    }\n\n    /**\n     * Return the context of Application object.\n     *\n     * @return the context of Application object\n     */\n    public static Application getApp() {\n        if (sApplication != null) {\n            return sApplication;\n        }\n        Application app = getApplicationByReflect();\n        init(app);\n        return app;\n    }\n\n    private static Application getApplicationByReflect() {\n        try {\n            @SuppressLint(\"PrivateApi\")\n            Class<?> activityThread = Class.forName(\"android.app.ActivityThread\");\n            Object thread = activityThread.getMethod(\"currentActivityThread\").invoke(null);\n            Object app = activityThread.getMethod(\"getApplication\").invoke(thread);\n            if (app == null) {\n                throw new NullPointerException(\"u should init first\");\n            }\n            return (Application) app;\n        } catch (NoSuchMethodException | IllegalAccessException | ClassNotFoundException | InvocationTargetException e) {\n            e.printStackTrace();\n        }\n        throw new NullPointerException(\"u should init first\");\n    }\n\n    public interface OnAppStatusChangedListener {\n        void onForeground();\n\n        void onBackground();\n    }\n\n    public interface OnActivityDestroyedListener {\n        void onActivityDestroyed(Activity activity);\n    }\n\n    ///////////////////////////////////////////////////////////////////////////\n    // interface\n    ///////////////////////////////////////////////////////////////////////////\n\n    static class ActivityLifecycleImpl implements ActivityLifecycleCallbacks {\n\n        final LinkedList<Activity> mActivityList = new LinkedList<>();\n        final Map<Object, OnAppStatusChangedListener> mStatusListenerMap = new HashMap<>();\n        final Map<Activity, Set<OnActivityDestroyedListener>> mDestroyedListenerMap = new HashMap<>();\n\n        private int mForegroundCount = 0;\n        private int mConfigCount = 0;\n        private boolean mIsBackground = false;\n\n        private static void fixSoftInputLeaks(final Activity activity) {\n            if (activity == null) {\n                return;\n            }\n            InputMethodManager imm =\n                (InputMethodManager) Utils.getApp().getSystemService(Context.INPUT_METHOD_SERVICE);\n            if (imm == null) {\n                return;\n            }\n            String[] leakViews = new String[]{\"mLastSrvView\", \"mCurRootView\", \"mServedView\", \"mNextServedView\"};\n            for (String leakView : leakViews) {\n                try {\n                    Field leakViewField = InputMethodManager.class.getDeclaredField(leakView);\n                    if (leakViewField == null) {\n                        continue;\n                    }\n                    if (!leakViewField.isAccessible()) {\n                        leakViewField.setAccessible(true);\n                    }\n                    Object obj = leakViewField.get(imm);\n                    if (!(obj instanceof View)) {\n                        continue;\n                    }\n                    View view = (View) obj;\n                    if (view.getRootView() == activity.getWindow().getDecorView().getRootView()) {\n                        leakViewField.set(imm, null);\n                    }\n                } catch (Throwable ignore) { /**/ }\n            }\n        }\n\n        @Override\n        public void onActivityCreated(@NonNull Activity activity, Bundle savedInstanceState) {\n            setTopActivity(activity);\n        }\n\n        @Override\n        public void onActivityStarted(@NonNull Activity activity) {\n            if (!mIsBackground) {\n                setTopActivity(activity);\n            }\n            if (mConfigCount < 0) {\n                ++mConfigCount;\n            } else {\n                ++mForegroundCount;\n            }\n        }\n\n        @Override\n        public void onActivityResumed(@NonNull Activity activity) {\n            setTopActivity(activity);\n            if (mIsBackground) {\n                mIsBackground = false;\n                postStatus(true);\n            }\n        }\n\n        @Override\n        public void onActivityPaused(@NonNull Activity activity) {/**/\n\n        }\n\n        @Override\n        public void onActivityStopped(Activity activity) {\n            if (activity.isChangingConfigurations()) {\n                --mConfigCount;\n            } else {\n                --mForegroundCount;\n                if (mForegroundCount <= 0) {\n                    mIsBackground = true;\n                    postStatus(false);\n                }\n            }\n        }\n\n        @Override\n        public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {/**/}\n\n        @Override\n        public void onActivityDestroyed(@NonNull Activity activity) {\n            mActivityList.remove(activity);\n            consumeOnActivityDestroyedListener(activity);\n            fixSoftInputLeaks(activity);\n        }\n\n        Activity getTopActivity() {\n            if (!mActivityList.isEmpty()) {\n                final Activity topActivity = mActivityList.getLast();\n                if (topActivity != null) {\n                    return topActivity;\n                }\n            }\n            Activity topActivityByReflect = getTopActivityByReflect();\n            if (topActivityByReflect != null) {\n                setTopActivity(topActivityByReflect);\n            }\n            return topActivityByReflect;\n        }\n\n        private void setTopActivity(final Activity activity) {\n            if (PERMISSION_ACTIVITY_CLASS_NAME.equals(activity.getClass().getName())) {\n                return;\n            }\n            if (mActivityList.contains(activity)) {\n                if (!mActivityList.getLast().equals(activity)) {\n                    mActivityList.remove(activity);\n                    mActivityList.addLast(activity);\n                }\n            } else {\n                mActivityList.addLast(activity);\n            }\n        }\n\n        void addOnAppStatusChangedListener(final Object object,\n                                           final OnAppStatusChangedListener listener) {\n            mStatusListenerMap.put(object, listener);\n        }\n\n        void removeOnAppStatusChangedListener(final Object object) {\n            mStatusListenerMap.remove(object);\n        }\n\n        void removeOnActivityDestroyedListener(final Activity activity) {\n            if (activity == null) {\n                return;\n            }\n            mDestroyedListenerMap.remove(activity);\n        }\n\n        void addOnActivityDestroyedListener(final Activity activity,\n                                            final OnActivityDestroyedListener listener) {\n            if (activity == null || listener == null) {\n                return;\n            }\n            Set<OnActivityDestroyedListener> listeners;\n            if (!mDestroyedListenerMap.containsKey(activity)) {\n                listeners = new HashSet<>();\n                mDestroyedListenerMap.put(activity, listeners);\n            } else {\n                listeners = mDestroyedListenerMap.get(activity);\n                if (listeners.contains(listener)) {\n                    return;\n                }\n            }\n            listeners.add(listener);\n        }\n\n        private void postStatus(final boolean isForeground) {\n            if (mStatusListenerMap.isEmpty()) {\n                return;\n            }\n            for (OnAppStatusChangedListener onAppStatusChangedListener : mStatusListenerMap.values()) {\n                if (onAppStatusChangedListener == null) {\n                    return;\n                }\n                if (isForeground) {\n                    onAppStatusChangedListener.onForeground();\n                } else {\n                    onAppStatusChangedListener.onBackground();\n                }\n            }\n        }\n\n        private void consumeOnActivityDestroyedListener(Activity activity) {\n            Iterator<Map.Entry<Activity, Set<OnActivityDestroyedListener>>> iterator\n                = mDestroyedListenerMap.entrySet().iterator();\n            while (iterator.hasNext()) {\n                Map.Entry<Activity, Set<OnActivityDestroyedListener>> entry = iterator.next();\n                if (entry.getKey() == activity) {\n                    Set<OnActivityDestroyedListener> value = entry.getValue();\n                    for (OnActivityDestroyedListener listener : value) {\n                        listener.onActivityDestroyed(activity);\n                    }\n                    iterator.remove();\n                }\n            }\n        }\n\n        private Activity getTopActivityByReflect() {\n            try {\n                @SuppressLint(\"PrivateApi\")\n                Class<?> activityThreadClass = Class.forName(\"android.app.ActivityThread\");\n                Object currentActivityThreadMethod = activityThreadClass.getMethod(\"currentActivityThread\").invoke(null);\n                Field mActivityListField = activityThreadClass.getDeclaredField(\"mActivityList\");\n                mActivityListField.setAccessible(true);\n                Map activities = (Map) mActivityListField.get(currentActivityThreadMethod);\n                if (activities == null) {\n                    return null;\n                }\n                for (Object activityRecord : activities.values()) {\n                    Class activityRecordClass = activityRecord.getClass();\n                    Field pausedField = activityRecordClass.getDeclaredField(\"paused\");\n                    pausedField.setAccessible(true);\n                    if (!pausedField.getBoolean(activityRecord)) {\n                        Field activityField = activityRecordClass.getDeclaredField(\"activity\");\n                        activityField.setAccessible(true);\n                        return (Activity) activityField.get(activityRecord);\n                    }\n                }\n            } catch (ClassNotFoundException | NoSuchFieldException | NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {\n                e.printStackTrace();\n            }\n            return null;\n        }\n    }\n\n    public static final class FileProvider4UtilCode extends FileProvider {\n\n        @Override\n        public boolean onCreate() {\n            Utils.init(getContext());\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "architecture/src/main/res/values/strings.xml",
    "content": "<!--\n  ~ Copyright 2018-present KunMinX\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\n<resources>\n    <string name=\"app_name\">architecture</string>\n\n    <string name=\"network_not_good\">网络不给力</string>\n\n</resources>\n"
  },
  {
    "path": "architecture/src/main/res/xml/file_paths.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <paths>\n        <external-path\n            name=\"camera_photos\"\n            path=\"\" />\n    </paths>\n</resources>"
  },
  {
    "path": "architecture/src/test/java/com/kunminx/architecture/ExampleUnitTest.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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\npackage com.kunminx.architecture;\n\nimport static org.junit.Assert.assertEquals;\n\nimport org.junit.Test;\n\n/**\n * Example local unit test, which will execute on the development machine (host).\n *\n * @see <a href=\"http://d.android.com/tools/testing\">Testing documentation</a>\n */\npublic class ExampleUnitTest {\n    @Test\n    public void addition_isCorrect() {\n        assertEquals(4, 2 + 2);\n    }\n}\n"
  },
  {
    "path": "build.gradle",
    "content": "buildscript {\n    ext {\n        appTargetSdk = 33\n        appMinSdk = 23\n        appVersionCode = 50500\n        appVersionName = \"5.5.0\"\n    }\n\n    repositories {\n        google()\n        gradlePluginPortal()\n        maven { url 'https://jitpack.io' }\n\n        //默认使用 gradlePluginPortal，以便在依赖库有紧急更新时能第一时间获取\n        //如对日常的拉取速度有追求，可考虑使用以下远程仓库（是对 central 的国内同步仓库，存在 1 天左右的时差）\n        //maven { url \"https://maven.aliyun.com/repository/public\" }\n    }\n    dependencies {\n        classpath 'com.android.tools.build:gradle:7.3.1'\n    }\n}\n\nallprojects {\n    repositories {\n        google()\n        mavenCentral()\n        maven { url 'https://jitpack.io' }\n        //maven { url \"https://maven.aliyun.com/repository/public\" }\n    }\n}\n\ntask clean(type: Delete) {\n    rootProject.allprojects {\n        delete(it.buildDir)\n    }\n}\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#Tue May 10 12:54:34 CST 2022\ndistributionBase=GRADLE_USER_HOME\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-7.4-bin.zip\ndistributionPath=wrapper/dists\nzipStorePath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\n"
  },
  {
    "path": "gradle.properties",
    "content": "android.enableJetifier=true\nandroid.injected.testOnly=false\nandroid.useAndroidX=true\norg.gradle.caching=true\norg.gradle.configureondemand=true\norg.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC -Dfile.encoding=UTF-8\norg.gradle.parallel=true"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env sh\n\n#\n# Copyright 2015 the original author or authors.\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#      https://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\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='\"-Xmx64m\" \"-Xms64m\"'\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\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\n  NONSTOP* )\n    nonstop=true\n    ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\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 or MSYS, switch paths to Windows format before running java\nif [ \"$cygwin\" = \"true\" -o \"$msys\" = \"true\" ] ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n\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=`expr $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\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\r\n@rem you may not use this file except in compliance with the License.\r\n@rem You may obtain a copy of the License at\r\n@rem\r\n@rem      https://www.apache.org/licenses/LICENSE-2.0\r\n@rem\r\n@rem Unless required by applicable law or agreed to in writing, software\r\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\r\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n@rem See the License for the specific language governing permissions and\r\n@rem limitations under the License.\r\n@rem\r\n\r\n@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 Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\r\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\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=\"-Xmx64m\" \"-Xms64m\"\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 execute\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 execute\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:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\r\n\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 %*\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": "settings.gradle",
    "content": "include ':app', ':architecture'\nrootProject.name = 'PureMusic'\n"
  }
]