master 543eb8659089 cached
125 files
403.4 KB
105.3k tokens
519 symbols
1 requests
Download .txt
Showing preview only (442K chars total). Download the full file or copy to clipboard to get everything.
Repository: KunMinX/Jetpack-MVVM-Best-Practice
Branch: master
Commit: 543eb8659089
Files: 125
Total size: 403.4 KB

Directory structure:
gitextract_yu3lvodp/

├── .editorconfig
├── .gitattributes
├── .github/
│   ├── dependabot.yml
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── README.md
├── app/
│   ├── build.gradle
│   ├── libs/
│   │   ├── materialiconlib-1.1.5.aar
│   │   └── videocache-2.7.1.aar
│   ├── proguard-rules.pro
│   └── src/
│       ├── androidTest/
│       │   └── java/
│       │       └── com/
│       │           └── kunminx/
│       │               └── puremusic/
│       │                   └── ExampleInstrumentedTest.java
│       ├── main/
│       │   ├── AndroidManifest.xml
│       │   ├── assets/
│       │   │   └── summary.html
│       │   ├── java/
│       │   │   └── com/
│       │   │       └── kunminx/
│       │   │           └── puremusic/
│       │   │               ├── MainActivity.java
│       │   │               ├── data/
│       │   │               │   ├── api/
│       │   │               │   │   ├── APIs.java
│       │   │               │   │   └── AccountService.java
│       │   │               │   ├── bean/
│       │   │               │   │   ├── DownloadState.java
│       │   │               │   │   ├── LibraryInfo.java
│       │   │               │   │   ├── TestAlbum.java
│       │   │               │   │   └── User.java
│       │   │               │   ├── config/
│       │   │               │   │   ├── Configs.java
│       │   │               │   │   └── Const.java
│       │   │               │   └── repository/
│       │   │               │       └── DataRepository.java
│       │   │               ├── domain/
│       │   │               │   ├── event/
│       │   │               │   │   ├── DownloadEvent.java
│       │   │               │   │   └── Messages.java
│       │   │               │   ├── message/
│       │   │               │   │   ├── DrawerCoordinateManager.java
│       │   │               │   │   ├── PageMessenger.java
│       │   │               │   │   ├── PlayerReceiver.java
│       │   │               │   │   └── SharedViewModel.java
│       │   │               │   ├── proxy/
│       │   │               │   │   └── PlayerManager.java
│       │   │               │   ├── request/
│       │   │               │   │   ├── AccountRequester.java
│       │   │               │   │   ├── DownloadRequester.java
│       │   │               │   │   ├── InfoRequester.java
│       │   │               │   │   └── MusicRequester.java
│       │   │               │   └── usecase/
│       │   │               │       ├── CanBeStoppedUseCase.java
│       │   │               │       └── DownloadUseCase.java
│       │   │               └── ui/
│       │   │                   ├── bind/
│       │   │                   │   ├── CommonBindingAdapter.java
│       │   │                   │   ├── DrawerBindingAdapter.java
│       │   │                   │   ├── IconBindingAdapter.java
│       │   │                   │   ├── TabPageBindingAdapter.java
│       │   │                   │   └── WebViewBindingAdapter.java
│       │   │                   ├── page/
│       │   │                   │   ├── DrawerFragment.java
│       │   │                   │   ├── LoginFragment.java
│       │   │                   │   ├── MainFragment.java
│       │   │                   │   ├── PlayerFragment.java
│       │   │                   │   ├── SearchFragment.java
│       │   │                   │   ├── adapter/
│       │   │                   │   │   ├── DiffUtils.java
│       │   │                   │   │   ├── DrawerAdapter.java
│       │   │                   │   │   └── PlaylistAdapter.java
│       │   │                   │   └── helper/
│       │   │                   │       └── DefaultInterface.java
│       │   │                   ├── view/
│       │   │                   │   ├── PlayPauseDrawable.java
│       │   │                   │   ├── PlayPauseView.java
│       │   │                   │   └── PlayerSlideListener.java
│       │   │                   └── widget/
│       │   │                       └── PlayerService.java
│       │   └── res/
│       │       ├── anim/
│       │       │   ├── h_fragment_enter.xml
│       │       │   ├── h_fragment_exit.xml
│       │       │   ├── h_fragment_pop_enter.xml
│       │       │   └── h_fragment_pop_exit.xml
│       │       ├── drawable/
│       │       │   ├── bar_selector_white.xml
│       │       │   ├── ic_menu_black_48dp.xml
│       │       │   ├── ic_music_note_black_48dp.xml
│       │       │   ├── ic_search_black_48dp.xml
│       │       │   ├── loading_animation.xml
│       │       │   └── progressbar_color.xml
│       │       ├── layout/
│       │       │   ├── activity_main.xml
│       │       │   ├── adapter_library.xml
│       │       │   ├── adapter_play_item.xml
│       │       │   ├── fragment_drawer.xml
│       │       │   ├── fragment_login.xml
│       │       │   ├── fragment_main.xml
│       │       │   ├── fragment_player.xml
│       │       │   ├── fragment_search.xml
│       │       │   ├── notify_player_big.xml
│       │       │   └── notify_player_small.xml
│       │       ├── layout-land/
│       │       │   ├── activity_main.xml
│       │       │   ├── fragment_main.xml
│       │       │   └── fragment_player.xml
│       │       ├── navigation/
│       │       │   ├── nav_drawer.xml
│       │       │   ├── nav_main.xml
│       │       │   └── nav_slide.xml
│       │       ├── values/
│       │       │   ├── attrs.xml
│       │       │   ├── colors.xml
│       │       │   ├── dimen.xml
│       │       │   ├── strings.xml
│       │       │   └── styles.xml
│       │       └── xml/
│       │           └── network_security_config.xml
│       └── test/
│           └── java/
│               └── com/
│                   └── kunminx/
│                       └── puremusic/
│                           └── ExampleUnitTest.java
├── architecture/
│   ├── build.gradle
│   └── src/
│       ├── androidTest/
│       │   └── java/
│       │       └── com/
│       │           └── kunminx/
│       │               └── architecture/
│       │                   └── ExampleInstrumentedTest.java
│       ├── main/
│       │   ├── AndroidManifest.xml
│       │   ├── java/
│       │   │   └── com/
│       │   │       └── kunminx/
│       │   │           └── architecture/
│       │   │               ├── data/
│       │   │               │   └── response/
│       │   │               │       ├── DataResult.java
│       │   │               │       ├── ResponseStatus.java
│       │   │               │       ├── ResultSource.java
│       │   │               │       └── manager/
│       │   │               │           ├── NetworkStateManager.java
│       │   │               │           └── NetworkStateReceive.java
│       │   │               ├── domain/
│       │   │               │   ├── request/
│       │   │               │   │   ├── AsyncTask.java
│       │   │               │   │   └── Requester.java
│       │   │               │   └── usecase/
│       │   │               │       ├── UseCase.java
│       │   │               │       ├── UseCaseHandler.java
│       │   │               │       ├── UseCaseScheduler.java
│       │   │               │       └── UseCaseThreadPoolScheduler.java
│       │   │               ├── ui/
│       │   │               │   ├── adapter/
│       │   │               │   │   └── CommonViewPagerAdapter.java
│       │   │               │   ├── bind/
│       │   │               │   │   └── DrawablesBindingAdapter.java
│       │   │               │   └── page/
│       │   │               │       ├── BaseActivity.java
│       │   │               │       ├── BaseFragment.java
│       │   │               │       └── StateHolder.java
│       │   │               └── utils/
│       │   │                   ├── AdaptScreenUtils.java
│       │   │                   ├── BarUtils.java
│       │   │                   ├── ClickUtils.java
│       │   │                   ├── DisplayUtils.java
│       │   │                   ├── ImageUtils.java
│       │   │                   ├── NetworkUtils.java
│       │   │                   ├── Res.java
│       │   │                   ├── ScreenUtils.java
│       │   │                   ├── ToastUtils.java
│       │   │                   └── Utils.java
│       │   └── res/
│       │       ├── values/
│       │       │   └── strings.xml
│       │       └── xml/
│       │           └── file_paths.xml
│       └── test/
│           └── java/
│               └── com/
│                   └── kunminx/
│                       └── architecture/
│                           └── ExampleUnitTest.java
├── build.gradle
├── gradle/
│   └── wrapper/
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
└── settings.gradle

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

================================================
FILE: .editorconfig
================================================
[*]
charset = utf-8
ij_java_use_single_class_imports = true
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

[*.yml]
indent_size = 2


================================================
FILE: .gitattributes
================================================
* text=auto eol=lf

*.bat text eol=crlf
*.jar binary

================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
  - package-ecosystem: gradle
    directory: "/"
    schedule:
      interval: daily
      time: "21:00"
    open-pull-requests-limit: 10
    target-branch: master

================================================
FILE: .github/workflows/ci.yml
================================================
name: CI

on: [ push, pull_request ]

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    strategy:
      matrix:
        os: [ubuntu-18.04, macOS-latest, windows-2016]
        java: [11, 11.0.3] 
    steps:
      - uses: actions/checkout@v1
      - name: set up JDK 11
        uses: actions/setup-java@v2
        with:
          distribution: 'zulu'
          java-version: ${{ matrix.java }}
      - name: Make gradlew executable
        run: chmod +x ./gradlew
      - name: Build
        run: ./gradlew --parallel app:assembleRelease


================================================
FILE: .gitignore
================================================
.classpath
.DS_Store
.externalNativeBuild
.project
.gradle
.mtj.tmp
.vscode
.settings
.cxx

/.idea

local.properties
maven-repository
mvn-clone
build
captures
gen
out
target

*.class
*.txt
*.ear
*.iml
*.jar
*.keystore
*.log
*.nar
*.rar
*.tar.gz
*.war
*.zip
*.apk

================================================
FILE: README.md
================================================
![](https://images.xiaozhuanlan.com/photo/2021/b106fd65d34a4a724244e7c5b42a2372.jpg)

[《重学安卓》](https://xiaozhuanlan.com/kunminx)付费读者加微信进群:myatejx

> [免费试读](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)

 

# 版权声明

我们就本项目 "被卖课" 一事,在掘金发表一期专访 [《开源项目被人拿去做课程卖了 1000 多万是什么体验》](https://juejin.im/post/5ecb4950518825431a669897)

本项目系我为方便开发者们 **无痛理解 Google 开源 Jetpack MVVM 中每个架构组件的 存在缘由、职责边界**,而 **精心设计的高频应用场景**,

与此同时,本项目是作为 [《重学安卓》](https://xiaozhuanlan.com/topic/6017825943)专栏 Jetpack MVVM 系列文章 “配套项目” 而存在,**文章内容和项目代码设计均涉及本人对 Jetpack MVVM 独家理解,本人对此享有著作权**。

任何组织或个人,未经与作者本人沟通,不得将本项目代码设计和本人对 Jetpack MVVM 独家理解用于 "**打包贩卖、引流、出书 和 卖课**" 等商业用途。

 

# 架构图一览

![](https://images.xiaozhuanlan.com/photo/2023/b10d6c52e0cdb4197725059399fad12f.jpg)

 

# 前言

上周我在各大 “技术社区” 发表了一篇 [《Jetpack MVVM 精讲》](https://juejin.im/post/5dafc49b6fb9a04e17209922),原以为在 “知识网红” 唱衰 Android 的 2019 会无人问津,没想到文章一经发布,从 “国内知名公司” 架构师、技术经理,到 “世界级公司”  Android 开发都在看。

且从读者反馈来看,近期大部分 Android 开发已跳出舒适圈,开始尝试认识和应用 Jetpack MVVM 到实际项目中。

只可惜,关于 Jetpack MVVM,网上多是 **东拼西凑、人云亦云、通篇贴代码** 文章,这不仅不能提供 “完整视角” 帮助读者 首先明确背景状况,更是给还没入门 Jetpack 读者 **徒添困扰**、起 **劝退** 作用。

好消息是,这一期,我们带着 **精心打磨 Jetpack MVVM 最佳实践案例** 来了!

 
 


|                       爱不释手交互设计                       |                         连贯用户体验                         |                      可信源统一分发                      |
| :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: |
| ![](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) |



|                   横竖屏布局无缝切换                   |
| :----------------------------------------------------: |
| ![](https://i.loli.net/2021/08/25/X9rado7AfnCEgv3.gif) |

 
 

# 项目简介

本人拥有 3 年 “移动端业务架构” 践行和设计经验,领导或参与团队 “重构” 中大型项目多达十数个,对 Jetpack MVVM 架构在 “确立规范化、标准化开发模式 以 **减少不可预期错误**” 所作的努力,有深入理解。



在这个案例中,我将为你展示,Jetpack MVVM 是如何 **以简驭繁** 地将原本十分容易出错、一出错就会耽搁半天的开发工作,通过寥寥几行代码 轻而易举完成。

> 👆👆👆 划重点!

 

该项目中,

> 我们为 **横、竖屏** 场景安排两套 **截然不同布局**,且在 [生命周期](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) 等知识点帮助下,通过寥寥几行代码,轻松做到 **在横竖屏两种布局间 无缝切换,且不产生任何 预期外错误**。


> 我们在多个 Fragment 页面 分别安排 **播放状态指示器**(包括 播放暂停按钮状态、播放列表当前索引指示 等),并向你展示 “如何” 及 “为何” 通过 [LiveData](https://xiaozhuanlan.com/topic/0168753249) **配合** 可信源 [ViewModel](https://xiaozhuanlan.com/topic/6257931840) 或单例,实现 **全应用范围内 “可追溯事件” 统一分发**。


> 我们在 Fragment 和 Activity 之间分别安排 跨页面通信,从而向你展示 如何基于 **迪米特原则**(也称 最少知道原则)、通过 UnPeekLiveData 和 应用级 SharedViewModel 实现 **生命周期安全、确保消息同步可靠一致的 页面通信**。


> 我们在 `ui.page` 、`domain.request` 、`data.repository` 等目录下,分别安排 视图控制器、[ViewModel](https://xiaozhuanlan.com/topic/6257931840) 、Dispatcher 、DataRepository 等 内容,从而向你展示,**单向依赖** 架构设计,是如何通过分层 数据请求和响应,**规避 “内存泄漏”** 等问题。


> 本项目代码一律采用 经过 ISO 认证 标准化工业级语言 Java 来编写。且在上述类中,我们大都 **提供丰富注释**,助你理解 “骨架代码” 为何要如此设计、如此设计能 **在软件工程背景下** 避免哪些不可预期错误。

 
 

除了 **在 "以简驭繁" 代码中 掌握 MVVM 最佳实践**,你还可从该项目中获得内容包括:

1. 整洁代码风格 和 标准资源命名规范。
2. 对 “视图控制器” 知识点的 深入理解 和 正确使用。
3. AndroidX 和 Material Design 2 全面使用。
4. ConstraintLayout 约束布局最佳实践。
5. **优秀的 用户体验 和 交互设计**。
6. 绝不使用 Dagger,绝不使用奇技淫巧、编写艰深晦涩代码。
7. The one more thing is:

即日起,可在 "应用商店" 下载体验!

[![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)


 
 

# Thanks to

[AndroidX](https://developer.android.google.cn/jetpack/androidx)

[Jetpack](https://developer.android.google.cn/jetpack/)

[material-components-android](https://github.com/material-components/material-components-android)

[轻听](https://play.google.com/store/apps/details?id=com.tencent.qqmusiclocalplayer)

[AndroidSlidingUpPanel](https://github.com/umano/AndroidSlidingUpPanel)

项目中使用 图片素材 来自 [UnSplash](https://unsplash.com/) 提供 **免费授权图片**。

项目中使用 音频素材 来自 [BenSound](https://www.bensound.com/) 提供 **免费授权音乐**。

 
 

# Who is using

根据小伙伴们 “开源库使用情况” 匿名调查问卷参与,截至 2022年5月28日,我们了解到

包括 “腾讯音乐、网易、BMW、TCL” 在内诸多知名厂商软件,都参考过我们开源的此架构模式,或正在使用我们维护的 [UnPeek-LiveData](https://github.com/KunMinX/UnPeek-LiveData) 等框架。

目前已将统计数据更新到 相关开源库 ReadMe 中,错过本次问卷调查的小伙伴也不用担心,我们继续对此保持开放,不定期将小伙伴们登记的公司和产品更新到表格,

以便吸纳更多小伙伴参与对 “架构组件” 的使用和反馈,集众人所长,让组件得以不断演化和升级。

https://wj.qq.com/s2/8362688/124a/

| 集团 / 公司 / 品牌 / 团队                             | 产品           |
| ----------------------------------------------------- | -------------- |
| 腾讯音乐                                              | QQ 音乐        |
| 网易                                                  | 网易云音乐     |
| TCL                                                   | 内置文件管理器 |
| 贵州广电网络                                          | 乐播播         |
| 上海亿保健康管理有限公司                              | 安诺保         |
|                                                       | 小辣椒         |
| ezen                                                  | Egshig音乐     |
| BMW                                                   | Speech         |
| 上海互教信息有限公司                                  | 知心慧学教师   |
| 美术宝                                                | 弹唱宝         |
|                                                       | 网安           |
| 字节跳动直播                                          | 直播 SDK       |
| 一加手机                                              | OPNote         |

 
 


# My Pages

Email:[kunminx@gmail.com](mailto:kunminx@gmail.com)

Juejin:[KunMinX 在掘金](https://juejin.im/user/58ab0de9ac502e006975d757/posts)

[《重学安卓》 专栏](https://xiaozhuanlan.com/kunminx)

付费读者加微信进群:myatejx

[![重学安卓小专栏](https://images.xiaozhuanlan.com/photo/2021/d493a54a32e38e7fbcfa68d424ebfd1e.png)](https://xiaozhuanlan.com/kunminx)

 
 

# License

```
Copyright 2019-present KunMinX

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

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

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



================================================
FILE: app/build.gradle
================================================
apply plugin: "com.android.application"

android {
    namespace "com.kunminx.puremusic"
    compileSdk appTargetSdk
    defaultConfig {
        applicationId "com.kunminx.puremusic"
        minSdk appMinSdk
        targetSdk appTargetSdk
        versionCode appVersionCode
        versionName appVersionName
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        debug {
            applicationIdSuffix ".debug"
            manifestPlaceholders = [
                APP_NAME: "@string/app_name_debug",
            ]
        }
        release {
            manifestPlaceholders = [
                APP_NAME: "@string/app_name",
            ]
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
        }
    }

    lintOptions {
        checkReleaseBuilds false
        abortOnError false
    }

    buildFeatures {
        dataBinding true
    }
}

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"])
    implementation project(":architecture")

    testImplementation "junit:junit:4.13.2"
    androidTestImplementation "androidx.test.ext:junit:1.1.5"
    androidTestImplementation "androidx.test.espresso:espresso-core:3.5.1"

    implementation "org.slf4j:slf4j-android:1.7.36"
    implementation "com.sothree.slidinguppanel:library:3.4.0"
    implementation 'com.github.KunMinX:Jetpack-MusicPlayer:5.2.0'
    implementation 'com.github.KunMinX.KeyValueX:keyvalue:3.7.0-beta'
    annotationProcessor 'com.github.KunMinX.KeyValueX:keyvalue-compiler:3.7.0-beta'
}


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

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

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

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

-keep class com.kunminx.puremusic.data.bean.** { *; }
-keep class com.kunminx.puremusic.data.config.*
-keep interface com.kunminx.puremusic.data.config.*
-keep class com.kunminx.player.bean.** { *; }

-keep class * implements android.os.Parcelable {
  public static final android.os.Parcelable$Creator *;
}

-keepnames class * implements java.io.Serializable

-keepclassmembers class * implements java.io.Serializable {
    static final long serialVersionUID;
    private static final java.io.ObjectStreamField[] serialPersistentFields;
    !static !transient <fields>;
    !private <fields>;
    !private <methods>;
    private void writeObject(java.io.ObjectOutputStream);
    private void readObject(java.io.ObjectInputStream);
    java.lang.Object writeReplace();
    java.lang.Object readResolve();
}

# webview
-keepclassmembers class fqcn.of.javascript.interface.for.Webview {
   public *;
}
-keepclassmembers class * extends android.webkit.WebViewClient {
    public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
    public boolean *(android.webkit.WebView, java.lang.String);
}
-keepclassmembers class * extends android.webkit.WebViewClient {
    public void *(android.webkit.WebView, jav.lang.String);
}


# AndroidX

-keep class com.google.android.material.** {*;}
-keep class androidx.** {*;}
-keep public class * extends androidx.**
-keep interface androidx.** {*;}
-dontwarn com.google.android.material.**
-dontnote com.google.android.material.**
-dontwarn androidx.**

# OkHttp

-dontwarn okhttp3.**
-keep class okhttp3.**{*;}
-dontwarn okio.**
-keep class okio.**{*;}

# glide
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
  **[] $VALUES;
  public *;
}

# RxJava

-keep class rx.schedulers.Schedulers {
    public static <methods>;
}
-keep class rx.schedulers.ImmediateScheduler {
    public <methods>;
}
-keep class rx.schedulers.TestScheduler {
    public <methods>;
}
-keep class rx.schedulers.Schedulers {
    public static ** test();
}
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
    long producerIndex;
    long consumerIndex;
}
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef {
    long producerNode;
    long consumerNode;
}
-dontwarn sun.misc.Unsafe


# Gson
-keepattributes Signature
-keepattributes *Annotation*
-keep class sun.misc.Unsafe { *; }
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer


================================================
FILE: app/src/androidTest/java/com/kunminx/puremusic/ExampleInstrumentedTest.java
================================================
package com.kunminx.puremusic;

import static org.junit.Assert.assertEquals;

import android.content.Context;

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

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

/**
 * Instrumented test, which will execute on an Android device.
 *
 * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
 */
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
    @Test
    public void useAppContext() {
        // Context of the app under test.
        Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();

        assertEquals("com.kunminx.puremusic", appContext.getPackageName());
    }
}


================================================
FILE: app/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="${APP_NAME}"
        android:networkSecurityConfig="@xml/network_security_config"
        android:roundIcon="@drawable/ic_launcher"
        android:supportsRtl="true"
        android:theme="@style/AppTheme"
        android:usesCleartextTraffic="true"
        tools:ignore="AllowBackup,GoogleAppIndexingWarning,UnusedAttribute">

        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service android:name=".ui.widget.PlayerService" />

        <receiver
            android:name=".domain.message.PlayerReceiver"
            android:exported="false"
            tools:ignore="ExportedReceiver">
            <intent-filter>
                <action android:name="pure_music.kunminx.close" />
                <action android:name="pure_music.kunminx.pause" />
                <action android:name="pure_music.kunminx.next" />
                <action android:name="pure_music.kunminx.play" />
                <action android:name="pure_music.kunminx.previous" />
                <action android:name="android.intent.action.MEDIA_BUTTON" />
                <action android:name="android.media.AUDIO_BECOMING_NOISY" />
            </intent-filter>
        </receiver>

    </application>

</manifest>


================================================
FILE: app/src/main/assets/summary.html
================================================
<!doctype html>
<html>
<head>
    <meta charset='UTF-8'>
    <meta content='width=device-width initial-scale=1' name='viewport'>
    <title>前言</title>
    <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; }
html { font-size: 14px; background-color: var(--bg-color); color: var(--text-color); font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; }
body { 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; }
iframe { margin: auto; }
a.url { word-break: break-all; }
a:active, a:hover { outline: 0px; }
.in-text-selection, ::selection { text-shadow: none; background: var(--select-text-bg-color); color: var(--select-text-font-color); }
#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; }
#write.first-line-indent p { text-indent: 2em; }
#write.first-line-indent li p, #write.first-line-indent p * { text-indent: 0px; }
#write.first-line-indent li { margin-left: 2em; }
.for-image #write { padding-left: 8px; padding-right: 8px; }
body.typora-export { padding-left: 30px; padding-right: 30px; }
.typora-export .footnote-line, .typora-export li, .typora-export p { white-space: pre-wrap; }
@media screen and (max-width: 500px) {
  body.typora-export { padding-left: 0px; padding-right: 0px; }
  #write { padding-left: 20px; padding-right: 20px; }
  .CodeMirror-sizer { margin-left: 0px !important; }
  .CodeMirror-gutters { display: none !important; }
}
#write li > figure:last-child { margin-bottom: 0.5rem; }
#write ol, #write ul { position: relative; }
img { max-width: 100%; vertical-align: middle; }
button, input, select, textarea { color: inherit; font: inherit; }
input[type="checkbox"], input[type="radio"] { line-height: normal; padding: 0px; }
*, ::after, ::before { box-sizing: border-box; }
#write h1, #write h2, #write h3, #write h4, #write h5, #write h6, #write p, #write pre { width: inherit; }
#write h1, #write h2, #write h3, #write h4, #write h5, #write h6, #write p { position: relative; }
p { line-height: inherit; }
h1, h2, h3, h4, h5, h6 { break-after: avoid-page; break-inside: avoid; orphans: 2; }
p { orphans: 4; }
h1 { font-size: 2rem; }
h2 { font-size: 1.8rem; }
h3 { font-size: 1.6rem; }
h4 { font-size: 1.4rem; }
h5 { font-size: 1.2rem; }
h6 { font-size: 1rem; }
.md-math-block, .md-rawblock, h1, h2, h3, h4, h5, h6, p { margin-top: 1rem; margin-bottom: 1rem; }
.hidden { display: none; }
.md-blockmeta { color: rgb(204, 204, 204); font-weight: 700; font-style: italic; }
a { cursor: pointer; }
sup.md-footnote { padding: 2px 4px; background-color: rgba(238, 238, 238, 0.7); color: rgb(85, 85, 85); border-radius: 4px; cursor: pointer; }
sup.md-footnote a, sup.md-footnote a:hover { color: inherit; text-transform: inherit; text-decoration: inherit; }
#write input[type="checkbox"] { cursor: pointer; width: inherit; height: inherit; }
figure { overflow-x: auto; margin: 1.2em 0px; max-width: calc(100% + 16px); padding: 0px; }
figure > table { margin: 0px !important; }
tr { break-inside: avoid; break-after: auto; }
thead { display: table-header-group; }
table { border-collapse: collapse; border-spacing: 0px; width: 100%; overflow: auto; break-inside: auto; text-align: left; }
table.md-table td { min-width: 32px; }
.CodeMirror-gutters { border-right: 0px; background-color: inherit; }
.CodeMirror-linenumber { user-select: none; }
.CodeMirror { text-align: left; }
.CodeMirror-placeholder { opacity: 0.3; }
.CodeMirror pre { padding: 0px 4px; }
.CodeMirror-lines { padding: 0px; }
div.hr:focus { cursor: none; }
#write pre { white-space: pre-wrap; }
#write.fences-no-line-wrapping pre { white-space: pre; }
#write pre.ty-contain-cm { white-space: normal; }
.CodeMirror-gutters { margin-right: 4px; }
.md-fences { font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; overflow: visible; white-space: pre; background: inherit; position: relative !important; }
.md-diagram-panel { width: 100%; margin-top: 10px; text-align: center; padding-top: 0px; padding-bottom: 8px; overflow-x: auto; }
#write .md-fences.mock-cm { white-space: pre-wrap; }
.md-fences.md-fences-with-lineno { padding-left: 0px; }
#write.fences-no-line-wrapping .md-fences.mock-cm { white-space: pre; overflow-x: auto; }
.md-fences.mock-cm.md-fences-with-lineno { padding-left: 8px; }
.CodeMirror-line, twitterwidget { break-inside: avoid; }
.footnotes { opacity: 0.8; font-size: 0.9rem; margin-top: 1em; margin-bottom: 1em; }
.footnotes + .footnotes { margin-top: 0px; }
.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; }
li div { padding-top: 0px; }
blockquote { margin: 1rem 0px; }
li .mathjax-block, li p { margin: 0.5rem 0px; }
li { margin: 0px; position: relative; }
blockquote > :last-child { margin-bottom: 0px; }
blockquote > :first-child, li > :first-child { margin-top: 0px; }
.footnotes-area { color: rgb(136, 136, 136); margin-top: 0.714rem; padding-bottom: 0.143rem; white-space: normal; }
#write .footnote-line { white-space: pre-wrap; }
@media print {
  body, html { border: 1px solid transparent; height: 99%; break-after: avoid; break-before: avoid; }
  #write { margin-top: 0px; padding-top: 0px; border-color: transparent !important; }
  .typora-export * { -webkit-print-color-adjust: exact; }
  html.blink-to-pdf { font-size: 13px; }
  .typora-export #write { padding-left: 32px; padding-right: 32px; padding-bottom: 0px; break-after: avoid; }
  .typora-export #write::after { height: 0px; }
}
.footnote-line { margin-top: 0.714em; font-size: 0.7em; }
a img, img a { cursor: pointer; }
pre.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; }
p > .md-image:only-child:not(.md-img-error) img, p > img:only-child { display: block; margin: auto; }
p > .md-image:only-child { display: inline-block; width: 100%; }
#write .MathJax_Display { margin: 0.8em 0px 0px; }
.md-math-block { width: 100%; }
.md-math-block:not(:empty)::after { display: none; }
[contenteditable="true"]:active, [contenteditable="true"]:focus { outline: 0px; box-shadow: none; }
.md-task-list-item { position: relative; list-style-type: none; }
.task-list-item.md-task-list-item { padding-left: 0px; }
.md-task-list-item > input { position: absolute; top: 0px; left: 0px; margin-left: -1.2em; margin-top: calc(1em - 10px); border: none; }
.math { font-size: 1rem; }
.md-toc { min-height: 3.58rem; position: relative; font-size: 0.9rem; border-radius: 10px; }
.md-toc-content { position: relative; margin-left: 0px; }
.md-toc-content::after, .md-toc::after { display: none; }
.md-toc-item { display: block; color: rgb(65, 131, 196); }
.md-toc-item a { text-decoration: none; }
.md-toc-inner:hover { text-decoration: underline; }
.md-toc-inner { display: inline-block; cursor: pointer; }
.md-toc-h1 .md-toc-inner { margin-left: 0px; font-weight: 700; }
.md-toc-h2 .md-toc-inner { margin-left: 2em; }
.md-toc-h3 .md-toc-inner { margin-left: 4em; }
.md-toc-h4 .md-toc-inner { margin-left: 6em; }
.md-toc-h5 .md-toc-inner { margin-left: 8em; }
.md-toc-h6 .md-toc-inner { margin-left: 10em; }
@media screen and (max-width: 48em) {
  .md-toc-h3 .md-toc-inner { margin-left: 3.5em; }
  .md-toc-h4 .md-toc-inner { margin-left: 5em; }
  .md-toc-h5 .md-toc-inner { margin-left: 6.5em; }
  .md-toc-h6 .md-toc-inner { margin-left: 8em; }
}
a.md-toc-inner { font-size: inherit; font-style: inherit; font-weight: inherit; line-height: inherit; }
.footnote-line a:not(.reversefootnote) { color: inherit; }
.md-attr { display: none; }
.md-fn-count::after { content: "."; }
code, pre, samp, tt { font-family: var(--monospace); }
kbd { 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; }
.md-comment { color: rgb(162, 127, 3); opacity: 0.8; font-family: var(--monospace); }
code { text-align: left; vertical-align: initial; }
a.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; }
.md-inline-math .MathJax_SVG .noError { display: none !important; }
.html-for-mac .inline-math-svg .MathJax_SVG { vertical-align: 0.2px; }
.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; }
.MathJax_SVG_Display, .md-inline-math .MathJax_SVG_Display { width: auto; margin: inherit; display: inline-block !important; }
.MathJax_SVG .MJX-monospace { font-family: var(--monospace); }
.MathJax_SVG .MJX-sans-serif { font-family: sans-serif; }
.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; }
.MathJax_SVG * { transition: none 0s ease 0s; }
.MathJax_SVG_Display svg { vertical-align: middle !important; margin-bottom: 0px !important; margin-top: 0px !important; }
.os-windows.monocolor-emoji .md-emoji { font-family: "Segoe UI Symbol", sans-serif; }
.md-diagram-panel > svg { max-width: 100%; }
[lang="mermaid"] svg, [lang="flow"] svg { max-width: 100%; height: auto; }
[lang="mermaid"] .node text { font-size: 1rem; }
table tr th { border-bottom: 0px; }
video { max-width: 100%; display: block; margin: 0px auto; }
iframe { max-width: 100%; width: 100%; border: none; }
.highlight td, .highlight tr { border: 0px; }


.CodeMirror { height: auto; }
.CodeMirror.cm-s-inner { background: inherit; }
.CodeMirror-scroll { overflow: auto hidden; z-index: 3; }
.CodeMirror-gutter-filler, .CodeMirror-scrollbar-filler { background-color: rgb(255, 255, 255); }
.CodeMirror-gutters { border-right: 1px solid rgb(221, 221, 221); background: inherit; white-space: nowrap; }
.CodeMirror-linenumber { padding: 0px 3px 0px 5px; text-align: right; color: rgb(153, 153, 153); }
.cm-s-inner .cm-keyword { color: rgb(119, 0, 136); }
.cm-s-inner .cm-atom, .cm-s-inner.cm-atom { color: rgb(34, 17, 153); }
.cm-s-inner .cm-number { color: rgb(17, 102, 68); }
.cm-s-inner .cm-def { color: rgb(0, 0, 255); }
.cm-s-inner .cm-variable { color: rgb(0, 0, 0); }
.cm-s-inner .cm-variable-2 { color: rgb(0, 85, 170); }
.cm-s-inner .cm-variable-3 { color: rgb(0, 136, 85); }
.cm-s-inner .cm-string { color: rgb(170, 17, 17); }
.cm-s-inner .cm-property { color: rgb(0, 0, 0); }
.cm-s-inner .cm-operator { color: rgb(152, 26, 26); }
.cm-s-inner .cm-comment, .cm-s-inner.cm-comment { color: rgb(170, 85, 0); }
.cm-s-inner .cm-string-2 { color: rgb(255, 85, 0); }
.cm-s-inner .cm-meta { color: rgb(85, 85, 85); }
.cm-s-inner .cm-qualifier { color: rgb(85, 85, 85); }
.cm-s-inner .cm-builtin { color: rgb(51, 0, 170); }
.cm-s-inner .cm-bracket { color: rgb(153, 153, 119); }
.cm-s-inner .cm-tag { color: rgb(17, 119, 0); }
.cm-s-inner .cm-attribute { color: rgb(0, 0, 204); }
.cm-s-inner .cm-header, .cm-s-inner.cm-header { color: rgb(0, 0, 255); }
.cm-s-inner .cm-quote, .cm-s-inner.cm-quote { color: rgb(0, 153, 0); }
.cm-s-inner .cm-hr, .cm-s-inner.cm-hr { color: rgb(153, 153, 153); }
.cm-s-inner .cm-link, .cm-s-inner.cm-link { color: rgb(0, 0, 204); }
.cm-negative { color: rgb(221, 68, 68); }
.cm-positive { color: rgb(34, 153, 34); }
.cm-header, .cm-strong { font-weight: 700; }
.cm-del { text-decoration: line-through; }
.cm-em { font-style: italic; }
.cm-link { text-decoration: underline; }
.cm-error { color: red; }
.cm-invalidchar { color: red; }
.cm-constant { color: rgb(38, 139, 210); }
.cm-defined { color: rgb(181, 137, 0); }
div.CodeMirror span.CodeMirror-matchingbracket { color: rgb(0, 255, 0); }
div.CodeMirror span.CodeMirror-nonmatchingbracket { color: rgb(255, 34, 34); }
.cm-s-inner .CodeMirror-activeline-background { background: inherit; }
.CodeMirror { position: relative; overflow: hidden; }
.CodeMirror-scroll { height: 100%; outline: 0px; position: relative; box-sizing: content-box; background: inherit; }
.CodeMirror-sizer { position: relative; }
.CodeMirror-gutter-filler, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-vscrollbar { position: absolute; z-index: 6; display: none; }
.CodeMirror-vscrollbar { right: 0px; top: 0px; overflow: hidden; }
.CodeMirror-hscrollbar { bottom: 0px; left: 0px; overflow: hidden; }
.CodeMirror-scrollbar-filler { right: 0px; bottom: 0px; }
.CodeMirror-gutter-filler { left: 0px; bottom: 0px; }
.CodeMirror-gutters { position: absolute; left: 0px; top: 0px; padding-bottom: 30px; z-index: 3; }
.CodeMirror-gutter { white-space: normal; height: 100%; box-sizing: content-box; padding-bottom: 30px; margin-bottom: -32px; display: inline-block; }
.CodeMirror-gutter-wrapper { position: absolute; z-index: 4; background: 0px 0px !important; border: none !important; }
.CodeMirror-gutter-background { position: absolute; top: 0px; bottom: 0px; z-index: 4; }
.CodeMirror-gutter-elt { position: absolute; cursor: default; z-index: 4; }
.CodeMirror-lines { cursor: text; }
.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; }
.CodeMirror-wrap pre { overflow-wrap: break-word; white-space: pre-wrap; word-break: normal; }
.CodeMirror-code pre { border-right: 30px solid transparent; width: fit-content; }
.CodeMirror-wrap .CodeMirror-code pre { border-right: none; width: auto; }
.CodeMirror-linebackground { position: absolute; left: 0px; right: 0px; top: 0px; bottom: 0px; z-index: 0; }
.CodeMirror-linewidget { position: relative; z-index: 2; overflow: auto; }
.CodeMirror-wrap .CodeMirror-scroll { overflow-x: hidden; }
.CodeMirror-measure { position: absolute; width: 100%; height: 0px; overflow: hidden; visibility: hidden; }
.CodeMirror-measure pre { position: static; }
.CodeMirror div.CodeMirror-cursor { position: absolute; visibility: hidden; border-right: none; width: 0px; }
.CodeMirror div.CodeMirror-cursor { visibility: hidden; }
.CodeMirror-focused div.CodeMirror-cursor { visibility: inherit; }
.cm-searching { background: rgba(255, 255, 0, 0.4); }
@media print {
  .CodeMirror div.CodeMirror-cursor { visibility: hidden; }
}


:root { --side-bar-bg-color: #fafafa; --control-text-color: #777; }
html { font-size: 16px; }
body { font-family: "Open Sans", "Clear Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; color: rgb(51, 51, 51); line-height: 1.6; }
#write { max-width: 860px; margin: 0px auto; padding: 30px 30px 100px; }
#write > ul:first-child, #write > ol:first-child { margin-top: 30px; }
a { color: rgb(65, 131, 196); }
h1, h2, h3, h4, h5, h6 { position: relative; margin-top: 1rem; margin-bottom: 1rem; font-weight: bold; line-height: 1.4; cursor: text; }
h1: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; }
h1 tt, h1 code { font-size: inherit; }
h2 tt, h2 code { font-size: inherit; }
h3 tt, h3 code { font-size: inherit; }
h4 tt, h4 code { font-size: inherit; }
h5 tt, h5 code { font-size: inherit; }
h6 tt, h6 code { font-size: inherit; }
h1 { padding-bottom: 0.3em; font-size: 2.25em; line-height: 1.2; border-bottom: 1px solid rgb(238, 238, 238); }
h2 { padding-bottom: 0.3em; font-size: 1.75em; line-height: 1.225; border-bottom: 1px solid rgb(238, 238, 238); }
h3 { font-size: 1.5em; line-height: 1.43; }
h4 { font-size: 1.25em; }
h5 { font-size: 1em; }
h6 { font-size: 1em; color: rgb(119, 119, 119); }
p, blockquote, ul, ol, dl, table { margin: 0.8em 0px; }
li > ol, li > ul { margin: 0px; }
hr { height: 2px; padding: 0px; margin: 16px 0px; background-color: rgb(231, 231, 231); border: 0px none; overflow: hidden; box-sizing: content-box; }
li p.first { display: inline-block; }
ul, ol { padding-left: 30px; }
ul:first-child, ol:first-child { margin-top: 0px; }
ul:last-child, ol:last-child { margin-bottom: 0px; }
blockquote { border-left: 4px solid rgb(223, 226, 229); padding: 0px 15px; color: rgb(119, 119, 119); }
blockquote blockquote { padding-right: 0px; }
table { padding: 0px; word-break: initial; }
table tr { border-top: 1px solid rgb(223, 226, 229); margin: 0px; padding: 0px; }
table tr:nth-child(2n), thead { background-color: rgb(248, 248, 248); }
table 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; }
table tr td { border: 1px solid rgb(223, 226, 229); margin: 0px; padding: 6px 13px; }
table tr th:first-child, table tr td:first-child { margin-top: 0px; }
table tr th:last-child, table tr td:last-child { margin-bottom: 0px; }
.CodeMirror-lines { padding-left: 4px; }
.code-tooltip { box-shadow: rgba(0, 28, 36, 0.3) 0px 1px 1px 0px; border-top: 1px solid rgb(238, 242, 242); }
.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; }
code { background-color: rgb(243, 244, 244); padding: 0px 2px; }
.md-fences { margin-bottom: 15px; margin-top: 15px; padding-top: 8px; padding-bottom: 6px; }
.md-task-list-item > input { margin-left: -1.3em; }
@media print {
  html { font-size: 13px; }
  table, pre { break-inside: avoid; }
  pre { overflow-wrap: break-word; }
}
.md-fences { background-color: rgb(248, 248, 248); }
#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; }
.mathjax-block > .code-tooltip { bottom: 0.375rem; }
.md-mathjax-midline { background: rgb(250, 250, 250); }
#write > h3.md-focus::before { left: -1.5625rem; top: 0.375rem; }
#write > h4.md-focus::before { left: -1.5625rem; top: 0.285714rem; }
#write > h5.md-focus::before { left: -1.5625rem; top: 0.285714rem; }
#write > h6.md-focus::before { left: -1.5625rem; top: 0.285714rem; }
.md-image > .md-meta { border-radius: 3px; padding: 2px 0px 0px 4px; font-size: 0.9em; color: inherit; }
.md-tag { color: rgb(167, 167, 167); opacity: 1; }
.md-toc { margin-top: 20px; padding-bottom: 20px; }
.sidebar-tabs { border-bottom: none; }
#typora-quick-open { border: 1px solid rgb(221, 221, 221); background-color: rgb(248, 248, 248); }
#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; }
.on-focus-mode blockquote { border-left-color: rgba(85, 85, 85, 0.12); }
header, .context-menu, .megamenu-content, footer { font-family: "Segoe UI", Arial, sans-serif; }
.file-node-content:hover .file-node-icon, .file-node-content:hover .file-node-open-state { visibility: visible; }
.mac-seamless-mode #typora-sidebar { background-color: var(--side-bar-bg-color); }
.md-lang { color: rgb(180, 101, 77); }
.html-for-mac .context-menu { --item-hover-bg-color: #E6F0FE; }
#md-notification .btn { border: 0px; }
.dropdown-menu .divider { border-color: rgb(229, 229, 229); }
.ty-preferences .window-content { background-color: rgb(250, 250, 250); }
.ty-preferences .nav-group-item.active { color: white; background: rgb(153, 153, 153); }

 .typora-export li, .typora-export p, .typora-export,  .footnote-line {white-space: normal;}



    </style>
</head>
<body class='typora-export os-windows'>
<div class='is-node' id='write'><p><a
        href='https://xiaozhuanlan.com/kunminx'><span>《重学安卓》</span></a><span>付费读者加微信进群:myatejx</span>
</p>
    <p>&nbsp;</p>
    <h2><a class="md-header-anchor" name="前言"></a><span>前言</span></h2>
    <p><span>很高兴见到你!</span></p>
    <p><span>上周我在 各大技术社区 发表了一篇 </span><a
            href='https://juejin.im/post/5dafc49b6fb9a04e17209922'><span>《Jetpack MVVM 精讲》</span></a><span>,原以为在 知识网红 唱衰安卓 的 2019 会无人问津,没想到文章一经发布,从 国内知名公司 的架构师、技术经理,到 世界级公司 的 Android 开发 都在看。</span>
    </p>
    <p><img alt='reader_say.png'
            referrerPolicy='no-referrer'
            src='https://upload-images.jianshu.io/upload_images/57036-5445e7b4d66d97c7.png'/></p>
    <p><span>并且从读者的反馈来看,近期大部分安卓开发 已跳出舒适圈,开始尝试认识和应用 Jetpack MVVM 到实际的项目开发中。</span></p>
    <p><span>只可惜,关于 Jetpack MVVM,网上多是 </span><strong><span>东拼西凑、人云亦云、通篇贴代码</span></strong><span> 的文章,这不仅不能提供完整的视角 来帮助读者 首先明确背景状况,更是给还没入门 Jetpack 的读者 </span><strong><span>徒添困扰</span></strong><span>、起到 </span><strong><span>劝退</span></strong><span> 的作用。</span>
    </p>
    <p>
        <span>好消息是,这一期,我们带着 </span><strong><span>精心打磨的 Jetpack MVVM 最佳实践案例</span></strong><span> 来了!</span>
        &nbsp;</p>
    <figure>
        <table>
            <thead>
            <tr>
                <th style='text-align:center;'><span>是让人 爱不释手 的 交互设计!</span></th>
                <th style='text-align:center;'><span>是 连贯 的 用户体验</span></th>
                <th style='text-align:center;'><span>可信源 的 统一分发</span></th>
            </tr>
            </thead>
            <tbody>
            <tr>
                <td style='text-align:center;'><img
                        alt='1231111323.gif'
                        referrerPolicy='no-referrer'
                        src='https://upload-images.jianshu.io/upload_images/57036-0a5cdc68f003211a.gif'/>
                </td>
                <td style='text-align:center;'><img
                        alt='222.gif'
                        referrerPolicy='no-referrer'
                        src='https://upload-images.jianshu.io/upload_images/57036-2b21db531e51ff03.gif'/>
                </td>
                <td style='text-align:center;'><img
                        alt='333.gif'
                        referrerPolicy='no-referrer'
                        src='https://upload-images.jianshu.io/upload_images/57036-9a541148ce5bed2e.gif'/>
                </td>
            </tr>
            </tbody>
        </table>
    </figure>
    <p>&nbsp;</p>
    <figure>
        <table>
            <thead>
            <tr>
                <th style='text-align:center;'><span>横竖屏布局 的 无缝切换</span></th>
            </tr>
            </thead>
            <tbody>
            <tr>
                <td style='text-align:center;'><img
                        alt='444.gif'
                        referrerPolicy='no-referrer'
                        src='https://upload-images.jianshu.io/upload_images/57036-688f3eafc76cfa27.gif'/>
                </td>
            </tr>
            </tbody>
        </table>
    </figure>
    <p>&nbsp;</p>
    <h2><a class="md-header-anchor" name="项目简介"></a><span>项目简介</span></h2>
    <p><span>本人拥有 3 年的 移动端架构 践行和设计经验,领导或参与团队重构的 中大型项目 多达十数个,对 Jetpack MVVM 架构在 确立规范化、标准化 开发模式 以 </span><strong><span>减少不可预期的错误</span></strong><span> 所作的努力,有着深入的理解。</span>
    </p>
    <p><span>在这个案例中,我将为你展示,Jetpack MVVM 是如何 </span><strong><span>以简驭繁</span></strong><span> 地 将原本十分容易出错、一出错就会耽搁半天时间的开发工作,通过 寥寥的几行代码 轻而易举地完成。</span>
    </p>
    <blockquote><p><span>👆👆👆 划重点!</span></p></blockquote>
    <p>&nbsp;</p>
    <p><span>在这个项目中,</span></p>
    <blockquote><p>
        <span>我们为 </span><strong><span>横、竖屏</span></strong><span> 的情况 分别安排了两套 </span><strong><span>截然不同的布局</span></strong><span>,并且在 </span><a
            href='https://xiaozhuanlan.com/topic/0213584967'><span>生命周期</span></a><span>、</span><a
            href='https://xiaozhuanlan.com/topic/7692814530'><span>重建机制</span></a><span>、</span><a
            href='https://xiaozhuanlan.com/topic/7692814530'><span>状态管理</span></a><span>、</span><a
            href='https://xiaozhuanlan.com/topic/9816742350'><span>DataBinding</span></a><span>、</span><a
            href='https://xiaozhuanlan.com/topic/6257931840'><span>ViewModel</span></a><span>、</span><a
            href='https://xiaozhuanlan.com/topic/0168753249'><span>LiveData</span></a><span> 、</span><a
            href='https://xiaozhuanlan.com/topic/5860149732'><span>Navigation</span></a><span> 等知识点的帮助下,通过寥寥几行代码,轻松做到 </span><strong><span>在横竖屏两种布局间 无缝地切换,并且不产生任何 预期外的错误</span></strong><span>。</span>
    </p></blockquote>
    <p>&nbsp;</p>
    <blockquote><p><span>我们在多个 Fragment 页面 分别安排了 </span><strong><span>播放状态 指示器</span></strong><span>(包括 播放暂停按钮状态、播放列表当前索引指示 等),并向你展示了 如何 以及为何 通过 </span><a
            href='https://xiaozhuanlan.com/topic/0168753249'><span>LiveData</span></a><span> </span><strong><span>配合</span></strong><span> 作为可信源 的 </span><a
            href='https://xiaozhuanlan.com/topic/6257931840'><span>ViewModel</span></a><span> 或单例,来实现 </span><strong><span>全应用范围内 可追溯事件 的统一分发</span></strong><span>。</span>
    </p></blockquote>
    <p>&nbsp;</p>
    <blockquote><p><span>我们在 Fragment 和 Activity 之间分别安排了 跨页面通信,从而向你展示 如何基于 </span><strong><span>迪米特原则</span></strong><span>(也称 最少知道原则)、通过 UnPeekLiveData 和 应用级 SharedViewModel 来实现 </span><strong><span>生命周期安全的、确保消息同步一致性和可靠性的 页面通信</span></strong><span>(事件回调)。</span>
    </p></blockquote>
    <p>&nbsp;</p>
    <blockquote><p>
        <span>我们在 </span><code>ui.page</code><span> 、</span><code>data.repository</code><span>、</span><code>bridge.request</code><span> 等目录下,分别安排了 视图控制器、</span><a
            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>
    </p></blockquote>
    <p>&nbsp;</p>
    <blockquote><p>
        <span>本项目的代码一律采用 经过 ISO 认证的 标准化工业级语言 Java 来编写。并且,在上述目录 所包含的 类中,我们大都 </span><strong><span>提供了丰富的注释</span></strong><span>,来帮助你理解 骨架代码 为何要如此设计、如此设计能够 </span><strong><span>在软件工程的背景下</span></strong><span> 避免哪些不可预期的错误。</span>
    </p></blockquote>
    <p>&nbsp;
        &nbsp;</p>
    <p><span>除了 </span><strong><span>在 以简驭繁 的代码中 掌握 MVVM 最佳实践</span></strong><span>,你还可以 从这个开源项目中 获得的内容 包括:</span>
    </p>
    <ol start=''>
        <li><span>整洁的代码风格 和 标准的资源命名规范。</span></li>
        <li><span>对 视图控制器 知识点的 深入理解 和 正确使用。</span></li>
        <li><span>AndroidX 和 Material Design 2 的全面使用。</span></li>
        <li><span>ConstraintLayout 约束布局的最佳实践。</span></li>
        <li><strong><span>优秀的 用户体验 和 交互设计</span></strong><span>。</span></li>
        <li><span>绝不使用 Dagger,绝不使用奇技淫巧、编写艰深晦涩的代码。</span></li>
        <li><span>The one more thing is:</span></li>
    </ol>
    <p><span>即日起,可在 应用商店 下载体验!</span></p>
    <p>&nbsp;</p>
    <p><a href='https://www.coolapk.com/apk/247826'><img
            alt='google-play1.png'
            referrerPolicy='no-referrer'
            src='https://upload-images.jianshu.io/upload_images/57036-f9dbd7810d38ae95.png'/></a><span> </span><a
            href='https://www.coolapk.com/apk/247826'><img
            alt='coolapk1.png'
            referrerPolicy='no-referrer'
            src='https://upload-images.jianshu.io/upload_images/57036-6cf24d0c9efe8362.png'/></a>
    </p>
    <p>&nbsp;
        &nbsp;</p>
    <h2><a class="md-header-anchor" name="thanks-to"></a><span>Thanks to</span></h2>
    <p><a href='https://developer.android.google.cn/jetpack/androidx'><span>AndroidX</span></a></p>
    <p><a href='https://developer.android.google.cn/jetpack/'><span>Jetpack</span></a></p>
    <p><a href='https://github.com/material-components/material-components-android'><span>material-components-android</span></a>
    </p>
    <p><a href='https://play.google.com/store/apps/details?id=com.tencent.qqmusiclocalplayer'><span>轻听</span></a>
    </p>
    <p>
        <a href='https://github.com/umano/AndroidSlidingUpPanel'><span>AndroidSlidingUpPanel</span></a>
    </p>
    <p><span>项目中使用的 图片素材 来自 </span><a href='https://unsplash.com/'><span>UnSplash</span></a><span> 提供的 </span><strong><span>无版权免费图片</span></strong><span>。</span>
    </p>
    <p><span>项目中使用的 音频素材 来自 </span><a
            href='https://www.bensound.com/'><span>BenSound</span></a><span> 提供的 </span><strong><span>无版权免费音乐</span></strong><span>。</span>
    </p>
    <p>&nbsp;
        &nbsp;</p>
    <h2><a class="md-header-anchor" name="my-pages"></a><span>My Pages</span></h2>
    <p><span>Email:</span><a href='mailto:kunminx@gmail.com'><span>kunminx@gmail.com</span></a></p>
    <p><span>Home:</span><a href='https://www.kunminx.com/'><span>KunMinX 的个人博客</span></a></p>
    <p><span>Juejin:</span><a href='https://juejin.im/user/58ab0de9ac502e006975d757/posts'><span>KunMinX 在掘金</span></a>
    </p>
    <p><a href='https://xiaozhuanlan.com/kunminx?rel=kunminx'><span>《重学安卓》 专栏</span></a></p>
    <p><span>付费读者加微信进群:myatejx</span></p>
    <p><a href='https://xiaozhuanlan.com/kunminx?rel=kunminx'><img
            alt='重学安卓小专栏' referrerPolicy='no-referrer'
            src='https://images.xiaozhuanlan.com/photo/2021/d493a54a32e38e7fbcfa68d424ebfd1e.png'/></a></p>
    <h2><a class="md-header-anchor" name="license"></a><span>License</span></h2>
    <pre class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" spellcheck="false"><div
            class="CodeMirror cm-s-inner CodeMirror-wrap" lang=""><div
            style="overflow: hidden; position: relative; width: 3px; height: 0px; top: 0px; left: 8px;"><textarea
            autocapitalize="off" autocorrect="off" spellcheck="false"
            style="position: absolute; bottom: -1em; padding: 0px; width: 1000px; height: 1em; outline: none;"
            tabindex="0"></textarea></div><div
            class="CodeMirror-scrollbar-filler" cm-not-content="true"></div><div
            class="CodeMirror-gutter-filler" cm-not-content="true"></div><div
            class="CodeMirror-scroll" tabindex="-1"><div class="CodeMirror-sizer"
                                                         style="margin-left: 0px; margin-bottom: 0px; border-right-width: 0px; padding-right: 0px; padding-bottom: 0px;"><div
            style="position: relative; top: 0px;"><div class="CodeMirror-lines" role="presentation"><div
            role="presentation" style="position: relative; outline: none;"><div
            class="CodeMirror-measure"><span><span>​</span>x</span></div><div
            class="CodeMirror-measure"></div><div style="position: relative; z-index: 1;"></div><div
            class="CodeMirror-code" role="presentation" style=""><div class="CodeMirror-activeline"
                                                                      style="position: relative;"><div
            class="CodeMirror-activeline-background CodeMirror-linebackground"></div><div
            class="CodeMirror-gutter-background CodeMirror-activeline-gutter"
            style="left: 0px; width: 0px;"></div><pre class=" CodeMirror-line " role="presentation"><span
            role="presentation"
            style="padding-right: 0.1px;">Copyright 2018-present KunMinX</span></pre></div><pre
            class=" CodeMirror-line " role="presentation"><span role="presentation"
                                                                style="padding-right: 0.1px;"><span
            cm-text="">​</span></span></pre><pre class=" CodeMirror-line " role="presentation"><span
            role="presentation" style="padding-right: 0.1px;">Licensed under the Apache License, Version 2.0 (the "License");</span></pre><pre
            class=" CodeMirror-line " role="presentation"><span role="presentation"
                                                                style="padding-right: 0.1px;">you may not use this file except in compliance with the License.</span></pre><pre
            class=" CodeMirror-line " role="presentation"><span role="presentation"
                                                                style="padding-right: 0.1px;">You may obtain a copy of the License at</span></pre><pre
            class=" CodeMirror-line " role="presentation"><span role="presentation"
                                                                style="padding-right: 0.1px;"><span
            cm-text="">​</span></span></pre><pre class=" CodeMirror-line " role="presentation"><span
            role="presentation" style="padding-right: 0.1px;"> &nbsp; http://www.apache.org/licenses/LICENSE-2.0</span></pre><pre
            class=" CodeMirror-line " role="presentation"><span role="presentation"
                                                                style="padding-right: 0.1px;"><span
            cm-text="">​</span></span></pre><pre class=" CodeMirror-line " role="presentation"><span
            role="presentation" style="padding-right: 0.1px;">Unless required by applicable law or agreed to in writing, software</span></pre><pre
            class=" CodeMirror-line " role="presentation"><span role="presentation"
                                                                style="padding-right: 0.1px;">distributed under the License is distributed on an "AS IS" BASIS,</span></pre><pre
            class=" CodeMirror-line " role="presentation"><span role="presentation"
                                                                style="padding-right: 0.1px;">WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.</span></pre><pre
            class=" CodeMirror-line " role="presentation"><span role="presentation"
                                                                style="padding-right: 0.1px;">See the License for the specific language governing permissions and</span></pre><pre
            class=" CodeMirror-line " role="presentation"><span role="presentation"
                                                                style="padding-right: 0.1px;">limitations under the License.</span></pre></div></div></div></div></div><div
            style="position: absolute; height: 0px; width: 1px; border-bottom: 0px solid transparent; top: 291px;"></div><div
            class="CodeMirror-gutters"
            style="display: none; height: 291px;"></div></div></div></pre>
    <p>&nbsp;</p></div>
</body>
</html>


================================================
FILE: app/src/main/java/com/kunminx/puremusic/MainActivity.java
================================================
/*
 * Copyright 2018-present KunMinX
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kunminx.puremusic;

import android.os.Bundle;
import android.view.View;

import androidx.drawerlayout.widget.DrawerLayout;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;

import com.kunminx.architecture.ui.page.BaseActivity;
import com.kunminx.architecture.ui.page.DataBindingConfig;
import com.kunminx.architecture.ui.page.StateHolder;
import com.kunminx.architecture.ui.state.State;
import com.kunminx.puremusic.domain.event.Messages;
import com.kunminx.puremusic.domain.message.DrawerCoordinateManager;
import com.kunminx.puremusic.domain.message.PageMessenger;
import com.kunminx.puremusic.domain.proxy.PlayerManager;

/**
 * Create by KunMinX at 19/10/16
 */

public class MainActivity extends BaseActivity {

    //TODO tip 1:基于 "单一职责原则",应将 ViewModel 划分为 state-ViewModel 和 result-ViewModel,
    // state-ViewModel 职责仅限于托管、保存和恢复本页面 state,作用域仅限于本页面,
    // result-ViewModel 职责仅限于 "消息分发" 场景承担 "可信源",作用域依 "数据请求" 或 "跨页通信" 消息分发范围而定

    // 如这么说无体会,详见 https://xiaozhuanlan.com/topic/8204519736

    private MainActivityStates mStates;
    private PageMessenger mMessenger;
    private boolean mIsListened = false;

    @Override
    protected void initViewModel() {
        mStates = getActivityScopeViewModel(MainActivityStates.class);
        mMessenger = getApplicationScopeViewModel(PageMessenger.class);
    }

    @Override
    protected DataBindingConfig getDataBindingConfig() {

        //TODO tip 2: DataBinding 严格模式:
        // 将 DataBinding 实例限制于 base 页面中,默认不向子类暴露,
        // 通过这方式,彻底解决 View 实例 Null 安全一致性问题,
        // 如此,View 实例 Null 安全性将和基于函数式编程思想的 Jetpack Compose 持平。
        // 而 DataBindingConfig 就是在这样背景下,用于为 base 页面 DataBinding 提供绑定项。

        // 如这么说无体会,详见 https://xiaozhuanlan.com/topic/9816742350 和 https://xiaozhuanlan.com/topic/2356748910

        return new DataBindingConfig(R.layout.activity_main, BR.vm, mStates)
            .addBindingParam(BR.listener, new ListenerHandler());
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        PlayerManager.getInstance().init(this);

        //TODO tip 6: 从 PublishSubject 接收回推的数据,并在回调中响应数据的变化,
        // 也即通过 BehaviorSubject(例如 ObservableField)通知控件属性重新渲染,并为其兜住最后一次状态,

        //如这么说无体会,详见 https://xiaozhuanlan.com/topic/6741932805

        mMessenger.output(this, messages -> {
            switch (messages.eventId) {
                case Messages.EVENT_CLOSE_ACTIVITY_IF_ALLOWED:
                    NavController nav = Navigation.findNavController(this, R.id.main_fragment_host);
                    if (nav.getCurrentDestination() != null && nav.getCurrentDestination().getId() != R.id.mainFragment) {
                        nav.navigateUp();
                    } else if (Boolean.TRUE.equals(mStates.isDrawerOpened.get())) {

                        //TODO 同 tip 3
                        mStates.openDrawer.set(false);
                    } else {
                        super.onBackPressed();
                    }
                    break;
                case Messages.EVENT_OPEN_DRAWER:

                    //TODO yes:同 tip 2:
                    // 此处将 drawer 的 open 和 close 都放在 drawerBindingAdapter 中操作,
                    // 规避 View 实例 Null 安全一致性问题,因为横屏布局无 drawerLayout。
                    // 此处如果用手动判空,很容易因疏忽而造成空引用。

                    //TODO 此外,此处为 drawerLayout 绑定状态 "openDrawer",使用 "去防抖" ObservableField 子类,
                    // 主要考虑到 ObservableField 具有 "防抖" 特性,不适合该场景。

                    //如这么说无体会,详见 https://xiaozhuanlan.com/topic/9816742350

                    mStates.openDrawer.set(true);

                    //TODO do not:(容易因疏忽埋下 View 实例 Null 安全一致性隐患)

                    /*if (mBinding.dl != null) {
                        if (aBoolean && !mBinding.dl.isDrawerOpen(GravityCompat.START)) {
                            mBinding.dl.openDrawer(GravityCompat.START);
                        } else {
                            mBinding.dl.closeDrawer(GravityCompat.START);
                        }
                    }*/
                    break;
            }
        });

        DrawerCoordinateManager.getInstance().isEnableSwipeDrawer().observe(this, aBoolean -> {

            //TODO yes: 同 tip 2

            mStates.allowDrawerOpen.set(aBoolean);

            // TODO do not:(容易因疏忽埋下 View 实例 Null 安全一致性隐患)

            /*if (mBinding.dl != null) {
                mBinding.dl.setDrawerLockMode(aBoolean
                        ? DrawerLayout.LOCK_MODE_UNLOCKED
                        : DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
            }*/
        });
    }

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (!mIsListened) {

            // TODO tip 3:此处演示向 "可信源" 发送请求,以便实现 "生命周期安全、消息分发可靠一致" 的通知。

            // 如这么说无体会,详见 https://xiaozhuanlan.com/topic/0168753249
            // --------
            // 与此同时,此处传达的另一思想是 "最少知道原则",
            // Activity 内部事情在 Activity 内部消化,不要试图在 fragment 中调用和操纵 Activity 内部东西。
            // 因为 Activity 端的处理后续可能会改变,且可受用于更多 fragment,而不单单是本 fragment。

            mMessenger.input(new Messages(Messages.EVENT_ADD_SLIDE_LISTENER));

            mIsListened = true;
        }
    }

    @Override
    public void onBackPressed() {

        // TODO 同 tip 3

        mMessenger.input(new Messages(Messages.EVENT_CLOSE_SLIDE_PANEL_IF_EXPANDED));
    }

    public class ListenerHandler extends DrawerLayout.SimpleDrawerListener {
        @Override
        public void onDrawerOpened(View drawerView) {
            super.onDrawerOpened(drawerView);
            mStates.isDrawerOpened.set(true);
        }

        @Override
        public void onDrawerClosed(View drawerView) {
            super.onDrawerClosed(drawerView);
            mStates.isDrawerOpened.set(false);
            mStates.openDrawer.set(false);
        }
    }

    //TODO tip 5:基于单一职责原则,抽取 Jetpack ViewModel "状态保存和恢复" 的能力作为 StateHolder,
    // 并使用 ObservableField 的改良版子类 State 来承担 BehaviorSubject,用作所绑定控件的 "可信数据源",
    // 从而在收到来自 PublishSubject 的结果回推后,响应结果数据的变化,也即通知控件属性重新渲染,并为其兜住最后一次状态,

    //如这么说无体会,详见 https://xiaozhuanlan.com/topic/6741932805

    public static class MainActivityStates extends StateHolder {

        public final State<Boolean> isDrawerOpened = new State<>(false);

        public final State<Boolean> openDrawer = new State<>(false);

        public final State<Boolean> allowDrawerOpen = new State<>(true);

    }
}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/data/api/APIs.java
================================================
package com.kunminx.puremusic.data.api;

/**
 * Create by KunMinX at 2021/6/3
 */
public class APIs {
    public final static String BASE_URL = "https://test.com/";
}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/data/api/AccountService.java
================================================
package com.kunminx.puremusic.data.api;

import retrofit2.Call;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.POST;

/**
 * Create by KunMinX at 2021/6/3
 */
public interface AccountService {

    @POST("xxx/login")
    @FormUrlEncoded
    Call<String> login(
        @Field("username") String username,
        @Field("password") String password
    );

}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/data/bean/DownloadState.java
================================================
package com.kunminx.puremusic.data.bean;

/**
 * Create by KunMinX at 2022/7/15
 * <p>
 * bean,原始数据,只读,
 * Java 我们通过移除 setter
 * kotlin 直接将字段设为 val 即可
 */
public class DownloadState {
    public final boolean isForgive;
    public final int progress;

    public DownloadState() {
        this.isForgive = false;
        this.progress = 0;
    }

    public DownloadState(boolean isForgive, int progress) {
        this.isForgive = isForgive;
        this.progress = progress;
    }
}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/data/bean/LibraryInfo.java
================================================
/*
 * Copyright 2018-present KunMinX
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kunminx.puremusic.data.bean;

/**
 * Create by KunMinX at 19/11/2
 * <p>
 * bean,原始数据,只读,
 * Java 我们通过移除 setter
 * kotlin 直接将字段设为 val 即可
 */
public class LibraryInfo {
    private final String title;
    private final String summary;
    private final String url;

    public LibraryInfo(String title, String summary, String url) {
        this.title = title;
        this.summary = summary;
        this.url = url;
    }

    public String getTitle() {
        return title;
    }

    public String getSummary() {
        return summary;
    }

    public String getUrl() {
        return url;
    }
}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/data/bean/TestAlbum.java
================================================
/*
 * Copyright 2018-present KunMinX
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kunminx.puremusic.data.bean;

import com.kunminx.player.bean.base.BaseAlbumItem;
import com.kunminx.player.bean.base.BaseArtistItem;
import com.kunminx.player.bean.base.BaseMusicItem;

import java.util.List;

/**
 * Create by KunMinX at 19/10/31
 * <p>
 * bean,原始数据,只读
 * Java 我们通过移除 setter
 * kotlin 直接将字段设为 val 即可
 */
public class TestAlbum extends BaseAlbumItem<TestAlbum.TestMusic, TestAlbum.TestArtist> {

    private String albumMid;
    public TestAlbum(String albumId, String title, String summary, TestArtist artist, String coverImg, List<TestMusic> musics) {
        super(albumId, title, summary, artist, coverImg, musics);
    }

    public String getAlbumMid() {
        return albumMid;
    }

    public static class TestMusic extends BaseMusicItem<TestArtist> {

        private String songMid;
        public TestMusic(String musicId, String coverImg, String url, String title, TestArtist artist) {
            super(musicId, coverImg, url, title, artist);
        }

        public String getSongMid() {
            return songMid;
        }
    }

    public static class TestArtist extends BaseArtistItem {

        private String birthday;
        public TestArtist(String name) {
            super(name);
        }

        public String getBirthday() {
            return birthday;
        }
    }
}



================================================
FILE: app/src/main/java/com/kunminx/puremusic/data/bean/User.java
================================================
/*
 * Copyright 2018-present KunMinX
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kunminx.puremusic.data.bean;

/**
 * Create by KunMinX at 20/04/26
 * <p>
 * bean,原始数据,只读
 * Java 我们通过移除 setter
 * kotlin 直接将字段设为 val 即可
 */
public class User {
    private final String name;
    private final String password;

    public User(String name, String password) {
        this.name = name;
        this.password = password;
    }

    public String getName() {
        return name;
    }

    public String getPassword() {
        return password;
    }

}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/data/config/Configs.java
================================================
/*
 * Copyright 2018-present KunMinX
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kunminx.puremusic.data.config;

import com.kunminx.architecture.data.config.keyvalue.KeyValueBoolean;
import com.kunminx.architecture.data.config.keyvalue.KeyValueInteger;
import com.kunminx.architecture.data.config.keyvalue.KeyValueSerializable;
import com.kunminx.architecture.data.config.keyvalue.KeyValueString;
import com.kunminx.keyvalue.annotation.KeyValueX;
import com.kunminx.puremusic.data.bean.User;

/**
 * TODO tip 1:消除 Android 项目 KeyValue 样板代码,让 key、value、get、put、init 缩减为一,不再 KV 爆炸。
 * 如这么说无体会,详见 https://juejin.cn/post/7121955840319291428
 * <p>
 * Create by KunMinX at 18/9/28
 */
@KeyValueX
public interface Configs {
    KeyValueString token();
    KeyValueBoolean isLogin();
    KeyValueInteger alive();
    KeyValueSerializable<User> user();
}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/data/config/Const.java
================================================
package com.kunminx.puremusic.data.config;

import android.os.Environment;

import com.kunminx.architecture.utils.Utils;
import com.kunminx.puremusic.R;
/**
 * Create by KunMinX at 2022/8/18
 */
public class Const {
    public static final String COVER_PATH = Utils.getApp().getExternalFilesDir(Environment.DIRECTORY_PICTURES).getAbsolutePath();
    public static final String COLUMN_LINK = Utils.getApp().getString(R.string.article_navigation);
    public static final String PROJECT_LINK = Utils.getApp().getString(R.string.github_project);
}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/data/repository/DataRepository.java
================================================
/*
 * Copyright 2018-present KunMinX
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kunminx.puremusic.data.repository;

import android.annotation.SuppressLint;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.kunminx.architecture.data.response.DataResult;
import com.kunminx.architecture.data.response.ResponseStatus;
import com.kunminx.architecture.data.response.ResultSource;
import com.kunminx.architecture.domain.request.AsyncTask;
import com.kunminx.architecture.utils.Utils;
import com.kunminx.puremusic.R;
import com.kunminx.puremusic.data.api.APIs;
import com.kunminx.puremusic.data.api.AccountService;
import com.kunminx.puremusic.data.bean.LibraryInfo;
import com.kunminx.puremusic.data.bean.TestAlbum;
import com.kunminx.puremusic.data.bean.User;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.List;
import java.util.concurrent.TimeUnit;

import io.reactivex.Observable;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Call;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

/**
 * Create by KunMinX at 19/10/29
 */
public class DataRepository {

    private static final DataRepository S_REQUEST_MANAGER = new DataRepository();

    private DataRepository() {
    }

    public static DataRepository getInstance() {
        return S_REQUEST_MANAGER;
    }

    private final Retrofit retrofit;

    {
        HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
        logging.setLevel(HttpLoggingInterceptor.Level.BODY);
        OkHttpClient client = new OkHttpClient.Builder()
            .connectTimeout(8, TimeUnit.SECONDS)
            .readTimeout(8, TimeUnit.SECONDS)
            .writeTimeout(8, TimeUnit.SECONDS)
            .addInterceptor(logging)
            .build();
        retrofit = new Retrofit.Builder()
            .baseUrl(APIs.BASE_URL)
            .client(client)
            .addConverterFactory(GsonConverterFactory.create())
            .build();
    }

    //TODO tip: 通过 "响应式框架" 往领域层回推数据,
    // 与此相对应,kotlin 下使用 flow{ ... emit(...) }.flowOn(Dispatchers.xx)

    public Observable<DataResult<TestAlbum>> getFreeMusic() {
        return AsyncTask.doIO(emitter -> {
            Gson gson = new Gson();
            Type type = new TypeToken<TestAlbum>() {
            }.getType();
            TestAlbum testAlbum = gson.fromJson(Utils.getApp().getString(R.string.free_music_json), type);
            emitter.onNext(new DataResult<>(testAlbum, new ResponseStatus()));
        });
    }

    public Observable<DataResult<List<LibraryInfo>>> getLibraryInfo() {
        return AsyncTask.doIO(emitter -> {
            Gson gson = new Gson();
            Type type = new TypeToken<List<LibraryInfo>>() {
            }.getType();
            List<LibraryInfo> list = gson.fromJson(Utils.getApp().getString(R.string.library_json), type);
            emitter.onNext(new DataResult<>(list, new ResponseStatus()));
        });
    }

    /**
     * TODO:模拟下载任务:
     */
    @SuppressLint("CheckResult")
    public Observable<Integer> downloadFile() {
        return AsyncTask.doIO(emitter -> {
            //在内存中模拟 "数据读写",假装是在 "文件 IO",

            byte[] bytes = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20};
            try (ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
                 ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
                int b;
                while ((b = bis.read()) != -1) {
                    Thread.sleep(500);
                    emitter.onNext(b);
                }
            } catch (IOException | InterruptedException e) {
                e.printStackTrace();
            }
        });
    }

    /**
     * TODO 模拟登录的网络请求
     *
     * @param user ui 层填写的用户信息
     */
    public Observable<DataResult<String>> login(User user) {

        // 使用 retrofit 或任意你喜欢的库实现网络请求。此处以 retrofit 写个简单例子,
        // 并且如使用 rxjava,还可额外依赖 RxJavaCallAdapterFactory 来简化编写,具体自行网上查阅,此处不做累述,

        return AsyncTask.doIO(emitter -> {
            Call<String> call = retrofit.create(AccountService.class).login(user.getName(), user.getPassword());
            Response<String> response;
            try {
                response = call.execute();
                ResponseStatus responseStatus = new ResponseStatus(
                    String.valueOf(response.code()), response.isSuccessful(), ResultSource.NETWORK);
                emitter.onNext(new DataResult<>(response.body(), responseStatus));
            } catch (IOException e) {
                emitter.onNext(new DataResult<>(null,
                    new ResponseStatus(e.getMessage(), false, ResultSource.NETWORK)));
            }
        });
    }
}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/domain/event/DownloadEvent.java
================================================
package com.kunminx.puremusic.domain.event;

import com.kunminx.puremusic.data.bean.DownloadState;

/**
 * Create by KunMinX at 2022/7/4
 */
public class DownloadEvent {
    public final static int EVENT_DOWNLOAD = 1;
    public final static int EVENT_DOWNLOAD_GLOBAL = 2;

    public final int eventId;
    public final DownloadState downloadState;

    public DownloadEvent(int eventId) {
        this.eventId = eventId;
        this.downloadState = new DownloadState();
    }

    public DownloadEvent(int eventId, DownloadState downloadState) {
        this.eventId = eventId;
        this.downloadState = downloadState;
    }

    public DownloadEvent copy(DownloadState downloadState) {
        return new DownloadEvent(this.eventId, downloadState);
    }
}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/domain/event/Messages.java
================================================
package com.kunminx.puremusic.domain.event;

/**
 * Create by KunMinX at 2022/7/4
 */
public class Messages {
    public final static int EVENT_CLOSE_SLIDE_PANEL_IF_EXPANDED = 1;
    public final static int EVENT_CLOSE_ACTIVITY_IF_ALLOWED = 2;
    public final static int EVENT_OPEN_DRAWER = 3;
    public final static int EVENT_ADD_SLIDE_LISTENER = 4;
    public final static int EVENT_LOGIN_SUCCESS = 5;

    public final int eventId;

    public Messages(int eventId) {
        this.eventId = eventId;
    }
}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/domain/message/DrawerCoordinateManager.java
================================================
/*
 *
 *  * Copyright 2018-present KunMinX
 *  *
 *  * Licensed under the Apache License, Version 2.0 (the "License");
 *  * you may not use this file except in compliance with the License.
 *  * You may obtain a copy of the License at
 *  *
 *  *    http://www.apache.org/licenses/LICENSE-2.0
 *  *
 *  * Unless required by applicable law or agreed to in writing, software
 *  * distributed under the License is distributed on an "AS IS" BASIS,
 *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  * See the License for the specific language governing permissions and
 *  * limitations under the License.
 *
 */

package com.kunminx.puremusic.domain.message;

import androidx.annotation.NonNull;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;

import com.kunminx.architecture.domain.message.MutableResult;
import com.kunminx.architecture.domain.message.Result;

import java.util.ArrayList;
import java.util.List;

/**
 * TODO tip 1:通过 Lifecycle 来实现 "抽屉侧滑禁用与否的判断" 的一致,
 * <p>
 * 每个 "需要注册和监听生命周期来判断" 的视图控制器,无需在各自内部手动书写解绑等操作。
 * 如这么说无体会,详见《为你还原一个真实的 Jetpack Lifecycle》
 * https://xiaozhuanlan.com/topic/3684721950
 * <p>
 * TODO tip 2:与此同时,作为用于 "跨页面通信" 单例,本类也承担 "可信源" 职责,
 * 所有对 Drawer 状态协调相关的请求都交由本单例处理,并统一分发给所有订阅者页面。
 * <p>
 * 如这么说无体会,详见《吃透 LiveData 本质,享用可靠消息鉴权机制》解析。
 * https://xiaozhuanlan.com/topic/6017825943
 * <p>
 * <p>
 * Create by KunMinX at 19/11/3
 */
public class DrawerCoordinateManager implements DefaultLifecycleObserver {

    private static final DrawerCoordinateManager S_HELPER = new DrawerCoordinateManager();

    private DrawerCoordinateManager() {
    }

    public static DrawerCoordinateManager getInstance() {
        return S_HELPER;
    }

    private final List<String> tagOfSecondaryPages = new ArrayList<>();

    private boolean isNoneSecondaryPage() {
        return tagOfSecondaryPages.size() == 0;
    }

    private final MutableResult<Boolean> enableSwipeDrawer = new MutableResult<>();

    public Result<Boolean> isEnableSwipeDrawer() {
        return enableSwipeDrawer;
    }

    public void requestToUpdateDrawerMode(boolean pageOpened, String pageName) {
        if (pageOpened) {
            tagOfSecondaryPages.add(pageName);
        } else {
            tagOfSecondaryPages.remove(pageName);
        }
        enableSwipeDrawer.setValue(isNoneSecondaryPage());
    }

    //TODO tip 3:让 NetworkStateManager 可观察页面生命周期,
    // 从而在进入或离开目标页面时,自动在此登记和处理抽屉的禁用和解禁,避免一系列不可预期问题。

    // 关于 Lifecycle 组件的存在意义,可详见《为你还原一个真实的 Jetpack Lifecycle》解析
    // https://xiaozhuanlan.com/topic/3684721950

    @Override
    public void onCreate(@NonNull LifecycleOwner owner) {

        tagOfSecondaryPages.add(owner.getClass().getSimpleName());

        enableSwipeDrawer.setValue(isNoneSecondaryPage());

    }

    @Override
    public void onDestroy(@NonNull LifecycleOwner owner) {

        tagOfSecondaryPages.remove(owner.getClass().getSimpleName());

        enableSwipeDrawer.setValue(isNoneSecondaryPage());

    }

}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/domain/message/PageMessenger.java
================================================
package com.kunminx.puremusic.domain.message;

import com.kunminx.architecture.domain.dispatch.MviDispatcher;
import com.kunminx.puremusic.domain.event.Messages;

/**
 * TODO:Note 2022.07.04
 *  `
 *  PageMessenger 是一个领域层组件,可用于 "跨页面通信" 场景,
 *  比如跳转到 login 页面完成登录后,login 页面反过来通知其他页面刷新状态,
 * <p>
 * PageMessenger 基于 MVI-Dispatcher 实现可靠的消息回推,
 * 通过消息队列、引用计数等设计,确保 "消息都能被消费,且只消费一次",
 * 通过内聚设计,彻底杜绝 mutable 滥用等问题,
 * <p>
 * 鉴于本项目场景难发挥 MVI-Dispatcher 潜能,故目前仅以改造 DownloadRequester 和 SharedViewModel 为例,
 * 通过对比 SharedViewModel 和 PageMessenger 易得,后者可简洁优雅实现可靠一致的消息分发,
 * <p>
 * <p>
 * 具体可参见专为 MVI-Dispatcher 编写的领域层案例:
 * <p>
 * https://github.com/KunMinX/MVI-Dispatcher
 * <p>
 * Create by KunMinX at 2022/7/4
 */
public class PageMessenger extends MviDispatcher<Messages> {
    @Override
    protected void onHandle(Messages event) {
        sendResult(event);
    }
}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/domain/message/PlayerReceiver.java
================================================
/*
 * Copyright 2018-present KunMinX
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kunminx.puremusic.domain.message;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.view.KeyEvent;

import com.kunminx.puremusic.domain.proxy.PlayerManager;
import com.kunminx.puremusic.ui.widget.PlayerService;

import java.util.Objects;

public class PlayerReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {

        if (Objects.equals(intent.getAction(), Intent.ACTION_MEDIA_BUTTON)) {
            if (intent.getExtras() == null) {
                return;
            }
            KeyEvent keyEvent = (KeyEvent) intent.getExtras().get(Intent.EXTRA_KEY_EVENT);
            if (keyEvent == null) {
                return;
            }
            if (keyEvent.getAction() != KeyEvent.ACTION_DOWN) {
                return;
            }

            switch (keyEvent.getKeyCode()) {
                case KeyEvent.KEYCODE_HEADSETHOOK:
                case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
                    PlayerManager.getInstance().togglePlay();
                    break;
                case KeyEvent.KEYCODE_MEDIA_PLAY:
                    PlayerManager.getInstance().playAudio();
                    break;
                case KeyEvent.KEYCODE_MEDIA_PAUSE:
                    PlayerManager.getInstance().pauseAudio();
                    break;
                case KeyEvent.KEYCODE_MEDIA_STOP:
                    PlayerManager.getInstance().clear();
                    break;
                case KeyEvent.KEYCODE_MEDIA_NEXT:
                    PlayerManager.getInstance().playNext();
                    break;
                case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
                    PlayerManager.getInstance().playPrevious();
                    break;
                default:
            }

        } else {

            if (Objects.requireNonNull(intent.getAction()).equals(PlayerService.NOTIFY_PLAY)) {
                PlayerManager.getInstance().playAudio();
            } else if (intent.getAction().equals(PlayerService.NOTIFY_PAUSE)
                || intent.getAction().equals(android.media.AudioManager.ACTION_AUDIO_BECOMING_NOISY)) {
                PlayerManager.getInstance().pauseAudio();
            } else if (intent.getAction().equals(PlayerService.NOTIFY_NEXT)) {
                PlayerManager.getInstance().playNext();
            } else if (intent.getAction().equals(PlayerService.NOTIFY_CLOSE)) {
                PlayerManager.getInstance().clear();
            } else if (intent.getAction().equals(PlayerService.NOTIFY_PREVIOUS)) {
                PlayerManager.getInstance().playPrevious();
            }
        }
    }
}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/domain/message/SharedViewModel.java
================================================
/*
 * Copyright 2018-present KunMinX
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kunminx.puremusic.domain.message;

import androidx.lifecycle.ViewModel;

import com.kunminx.architecture.domain.message.MutableResult;
import com.kunminx.architecture.domain.message.Result;

/**
 * TODO tip:本类专用于跨页面通信,
 * 本类已被 PageMessenger 类代替,具体可参见 PageMessenger 类说明
 * <p>
 * Create by KunMinX at 19/10/16
 */
@Deprecated
public class SharedViewModel extends ViewModel {

    //TODO tip 2:此处演示 UnPeekLiveData 配合 SharedViewModel 实现 "生命周期安全、可靠一致" 消息分发。

    //TODO tip 3:为便于理解,原 UnPeekLiveData 已改名为 MutableResult;
    // ProtectedUnPeekLiveData 改名 Result;

    private final MutableResult<Boolean> toCloseSlidePanelIfExpanded = new MutableResult<>();

    private final MutableResult<Boolean> toCloseActivityIfAllowed = new MutableResult<>();

    private final MutableResult<Boolean> toOpenOrCloseDrawer = new MutableResult<>();

    //TODO tip 4:可通过构造器方式配置 MutableResult

    private final MutableResult<Boolean> toAddSlideListener =
        new MutableResult.Builder<Boolean>().setAllowNullValue(false).create();

    public Result<Boolean> isToAddSlideListener() {
        return toAddSlideListener;
    }

    public Result<Boolean> isToCloseSlidePanelIfExpanded() {
        return toCloseSlidePanelIfExpanded;
    }

    public Result<Boolean> isToCloseActivityIfAllowed() {
        return toCloseActivityIfAllowed;
    }

    public Result<Boolean> isToOpenOrCloseDrawer() {
        return toOpenOrCloseDrawer;
    }

    public void requestToCloseActivityIfAllowed(boolean allow) {
        toCloseActivityIfAllowed.setValue(allow);
    }

    public void requestToOpenOrCloseDrawer(boolean open) {
        toOpenOrCloseDrawer.setValue(open);
    }

    public void requestToCloseSlidePanelIfExpanded(boolean close) {
        toCloseSlidePanelIfExpanded.setValue(close);
    }

    public void requestToAddSlideListener(boolean add) {
        toAddSlideListener.setValue(add);
    }
}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/domain/proxy/PlayerManager.java
================================================
/*
 * Copyright 2018-2019 KunMinX
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kunminx.puremusic.domain.proxy;

import android.content.Context;
import android.content.Intent;

import androidx.lifecycle.LiveData;

import com.danikula.videocache.HttpProxyCacheServer;
import com.kunminx.player.contract.ICacheProxy;
import com.kunminx.player.contract.IPlayController;
import com.kunminx.player.contract.IServiceNotifier;
import com.kunminx.player.domain.MusicDTO;
import com.kunminx.player.domain.PlayerController;
import com.kunminx.player.domain.PlayingInfoManager;
import com.kunminx.puremusic.data.bean.TestAlbum;
import com.kunminx.puremusic.ui.widget.PlayerService;

import net.steamcrafted.materialiconlib.MaterialDrawableBuilder;

import java.util.List;

/**
 * Create by KunMinX at 19/10/31
 */
public class PlayerManager implements IPlayController<TestAlbum, TestAlbum.TestMusic, TestAlbum.TestArtist> {

    private static final PlayerManager sManager = new PlayerManager();

    private final PlayerController<TestAlbum, TestAlbum.TestMusic, TestAlbum.TestArtist> mController;

    private PlayerManager() {
        mController = new PlayerController<>();
    }

    public static PlayerManager getInstance() {
        return sManager;
    }

    private boolean mIsInit;

    public void init(Context context) {
        if (!mIsInit) {
            init(context, null, null);
            mIsInit = true;
        }
    }

    @Override
    public void init(Context context, IServiceNotifier iServiceNotifier, ICacheProxy iCacheProxy) {
        Context context1 = context.getApplicationContext();

        HttpProxyCacheServer proxy = new HttpProxyCacheServer.Builder(context1)
            .fileNameGenerator(url -> {
                String[] split = url.split("/");
                return split[split.length - 1];
            })
            .maxCacheSize(2147483648L)
            .build();

        mController.init(context1, startOrStop -> {
            Intent intent = new Intent(context1, PlayerService.class);
            if (startOrStop) context1.startService(intent);
            else context1.stopService(intent);
        }, proxy::getProxyUrl);
    }

    @Override
    public void loadAlbum(TestAlbum musicAlbum) {
        TestAlbum album = mController.getAlbum();
        if (album == null || !album.albumId.equals(musicAlbum.albumId)) {
            mController.loadAlbum(musicAlbum);
        }
    }

    @Override
    public void loadAlbum(TestAlbum musicAlbum, int playIndex) {
        mController.loadAlbum(musicAlbum, playIndex);
    }

    @Override
    public void playAudio() {
        mController.playAudio();
    }

    @Override
    public void playAudio(int albumIndex) {
        mController.playAudio(albumIndex);
    }

    @Override
    public void playNext() {
        mController.playNext();
    }

    @Override
    public void playPrevious() {
        mController.playPrevious();
    }

    @Override
    public void playAgain() {
        mController.playAgain();
    }

    @Override
    public void pauseAudio() {
        mController.pauseAudio();
    }

    @Override
    public void resumeAudio() {
        mController.resumeAudio();
    }

    @Override
    public void clear() {
        mController.clear();
    }

    @Override
    public void changeMode() {
        mController.changeMode();
    }

    @Override
    public boolean isPlaying() {
        return mController.isPlaying();
    }

    @Override
    public boolean isPaused() {
        return mController.isPaused();
    }

    @Override
    public boolean isInit() {
        return mController.isInit();
    }

    @Override
    public void setSeek(int progress) {
        mController.setSeek(progress);
    }

    @Override
    public String getTrackTime(int progress) {
        return mController.getTrackTime(progress);
    }

    @Override
    public LiveData<MusicDTO<TestAlbum, TestAlbum.TestMusic, TestAlbum.TestArtist>> getUiStates() {
        return mController.getUiStates();
    }

    @Override
    public TestAlbum getAlbum() {
        return mController.getAlbum();
    }

    @Override
    public List<TestAlbum.TestMusic> getAlbumMusics() {
        return mController.getAlbumMusics();
    }

    @Override
    public void setChangingPlayingMusic(boolean changingPlayingMusic) {
        mController.setChangingPlayingMusic(changingPlayingMusic);
    }

    @Override
    public int getAlbumIndex() {
        return mController.getAlbumIndex();
    }

    @Override
    public Enum<PlayingInfoManager.RepeatMode> getRepeatMode() {
        return mController.getRepeatMode();
    }

    @Override
    public void togglePlay() {
        mController.togglePlay();
    }

    @Override
    public TestAlbum.TestMusic getCurrentPlayingMusic() {
        return mController.getCurrentPlayingMusic();
    }

    public MaterialDrawableBuilder.IconValue getModeIcon(Enum<PlayingInfoManager.RepeatMode> mode) {
        if (mode == PlayingInfoManager.RepeatMode.LIST_CYCLE) {
            return MaterialDrawableBuilder.IconValue.REPEAT;
        } else if (mode == PlayingInfoManager.RepeatMode.SINGLE_CYCLE) {
            return MaterialDrawableBuilder.IconValue.REPEAT_ONCE;
        } else {
            return MaterialDrawableBuilder.IconValue.SHUFFLE;
        }
    }

    public MaterialDrawableBuilder.IconValue getModeIcon() {
        return getModeIcon(getRepeatMode());
    }
}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/domain/request/AccountRequester.java
================================================
/*
 * Copyright 2018-present KunMinX
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kunminx.puremusic.domain.request;

import androidx.annotation.NonNull;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;

import com.kunminx.architecture.data.response.DataResult;
import com.kunminx.architecture.data.response.ResponseStatus;
import com.kunminx.architecture.data.response.ResultSource;
import com.kunminx.architecture.domain.message.MutableResult;
import com.kunminx.architecture.domain.message.Result;
import com.kunminx.architecture.domain.request.Requester;
import com.kunminx.puremusic.data.bean.User;
import com.kunminx.puremusic.data.repository.DataRepository;

import org.jetbrains.annotations.NotNull;

import io.reactivex.Observer;
import io.reactivex.disposables.Disposable;

/**
 * 用户账户 Request
 * <p>
 * TODO tip 1:让 UI 和业务分离,让数据总是从生产者流向消费者
 * <p>
 * UI逻辑和业务逻辑,本质区别在于,前者是数据的消费者,后者是数据的生产者,
 * "领域层组件" 作为数据的生产者,职责应仅限于 "请求调度 和 结果分发",
 * <p>
 * 换言之,"领域层组件" 中应当只关注数据的生成,而不关注数据的使用,
 * 改变 UI 状态的逻辑代码,只应在表现层页面中编写、在 Observer 回调中响应数据的变化,
 * 将来升级到 Jetpack Compose 更是如此,
 * <p>
 * Activity {
 * onCreate(){
 * vm.livedata.observe { result->
 * panel.visible(result.show ? VISIBLE : GONE)
 * tvTitle.setText(result.title)
 * tvContent.setText(result.content)
 * }
 * }
 * <p>
 * 如这么说无体会,详见《Jetpack MVVM 分层设计》解析
 * https://xiaozhuanlan.com/topic/6741932805
 * <p>
 * <p>
 * Create by KunMinX at 20/04/26
 */
public class AccountRequester extends Requester implements DefaultLifecycleObserver {

    //TODO tip 3:👆👆👆 让 accountRequest 可观察页面生命周期,
    // 从而在页面即将退出、且登录请求由于网络延迟尚未完成时,
    // 及时通知数据层取消本次请求,以避免资源浪费和一系列不可预期问题。

    private final MutableResult<DataResult<String>> tokenResult = new MutableResult<>();

    //TODO tip 4:应顺应 "响应式编程",做好 "单向数据流" 开发,
    // MutableResult 应仅限 "鉴权中心" 内部使用,且只暴露 immutable Result 给 UI 层,
    // 通过 "读写分离" 实现数据从 "领域层" 到 "表现层" 的单向流动,

    //如这么说无体会,详见《吃透 LiveData 本质,享用可靠消息鉴权机制》解析。
    //https://xiaozhuanlan.com/topic/6017825943

    public Result<DataResult<String>> getTokenResult() {
        return tokenResult;
    }

    //TODO tip 5:模拟可取消的登录请求:
    //
    // 配合可观察页面生命周期的 accountRequest,
    // 从而在页面即将退出、且登录请求由于网络延迟尚未完成时,
    // 及时通知数据层取消本次请求,以避免资源浪费和一系列不可预期的问题。

    private Disposable mDisposable;

    //TODO tip 6: requester 作为数据的生产者,职责应仅限于 "请求调度 和 结果分发",
    //
    // 换言之,此处只关注数据的生成和回推,不关注数据的使用,
    // 改变 UI 状态的逻辑代码,只应在表现层页面中编写,例如 Jetpack Compose 的使用,

    public void requestLogin(User user) {
        DataRepository.getInstance().login(user).subscribe(new Observer<DataResult<String>>() {
            @Override
            public void onSubscribe(Disposable d) {
                mDisposable = d;
            }
            @Override
            public void onNext(DataResult<String> dataResult) {
                tokenResult.postValue(dataResult);
            }
            @Override
            public void onError(Throwable e) {
                tokenResult.postValue(new DataResult<>(null,
                    new ResponseStatus(e.getMessage(), false, ResultSource.NETWORK)));
            }
            @Override
            public void onComplete() {
                mDisposable = null;
            }
        });
    }

    public void cancelLogin() {
        if (mDisposable != null && !mDisposable.isDisposed()) {
            mDisposable.dispose();
            mDisposable = null;
        }
    }

    //TODO tip 7:让 accountRequest 可观察页面生命周期,
    // 从而在页面即将退出、且登录请求由于网络延迟尚未完成时,
    // 及时通知数据层取消本次请求,以避免资源浪费和一系列不可预期问题。

    // 关于 Lifecycle 组件的存在意义,详见《为你还原一个真实的 Jetpack Lifecycle》解析
    // https://xiaozhuanlan.com/topic/3684721950

    @Override
    public void onStop(@NonNull @NotNull LifecycleOwner owner) {
        cancelLogin();
    }
}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/domain/request/DownloadRequester.java
================================================
package com.kunminx.puremusic.domain.request;

import androidx.annotation.NonNull;
import androidx.lifecycle.LifecycleOwner;

import com.kunminx.architecture.domain.dispatch.MviDispatcher;
import com.kunminx.architecture.domain.request.AsyncTask;
import com.kunminx.puremusic.data.bean.DownloadState;
import com.kunminx.puremusic.data.repository.DataRepository;
import com.kunminx.puremusic.domain.event.DownloadEvent;

import io.reactivex.disposables.Disposable;

/**
 * 数据下载 Request
 * <p>
 * TODO tip 1:让 UI 和业务分离,让数据总是从生产者流向消费者
 * <p>
 * UI逻辑和业务逻辑,本质区别在于,前者是数据的消费者,后者是数据的生产者,
 * "领域层组件" 作为数据的生产者,职责应仅限于 "请求调度 和 结果分发",
 * <p>
 * 换言之,"领域层组件" 中应当只关注数据的生成,而不关注数据的使用,
 * 改变 UI 状态的逻辑代码,只应在表现层页面中编写、在 Observer 回调中响应数据的变化,
 * 将来升级到 Jetpack Compose 更是如此,
 * <p>
 * Activity {
 * onCreate(){
 * vm.livedata.observe { result->
 * panel.visible(result.show ? VISIBLE : GONE)
 * tvTitle.setText(result.title)
 * tvContent.setText(result.content)
 * }
 * }
 * <p>
 * 如这么说无体会,详见《Jetpack MVVM 分层设计》解析
 * https://xiaozhuanlan.com/topic/6741932805
 * <p>
 * <p>
 * Create by KunMinX at 20/03/18
 */
public class DownloadRequester extends MviDispatcher<DownloadEvent> {

    private Disposable mDisposable;

    //TODO Tip 2:基于 "单一职责原则",宜将 Jetpack ViewModel 框架划分为 state-ViewModel 和 result-ViewModel,
    // result-ViewModel 作为领域层组件,仅提取和继承 Jetpack ViewModel 框架中 "作用域管理" 的能力,
    // 使业务实例能根据需要,被单个页面独享,或多个页面共享,例如:
    //
    // mDownloadRequester = getFragmentScopeViewModel(DownloadRequester.class);
    // mGlobalDownloadRequester = getActivityScopeViewModel(DownloadRequester.class);
    //
    // 在本案例中,fragment 级作用域的 mDownloadRequester 只走 DownloadEvent.EVENT_DOWNLOAD 业务,
    // Activity 级作用域的 mGlobalDownloadRequester 只走 DownloadEvent.EVENT_DOWNLOAD_GLOBAL 业务,
    // 二者都为 SearchFragment 所持有,用于对比不同作用域的效果,

    @Override
    protected void onHandle(DownloadEvent event) {
        DataRepository repo = DataRepository.getInstance();
        switch (event.eventId) {
            case DownloadEvent.EVENT_DOWNLOAD:
                repo.downloadFile().subscribe(new AsyncTask.Observer<Integer>() {
                    @Override
                    public void onSubscribe(Disposable d) {
                        mDisposable = d;
                    }
                    @Override
                    public void onNext(Integer integer) {
                        sendResult(event.copy(new DownloadState(true, integer)));
                    }
                });
                break;
            case DownloadEvent.EVENT_DOWNLOAD_GLOBAL:
                repo.downloadFile().subscribe((AsyncTask.Observer<Integer>) integer -> {
                    sendResult(event.copy(new DownloadState(true, integer)));
                });
                break;
        }
    }

    @Override
    public void onStop(@NonNull LifecycleOwner owner) {
        super.onStop(owner);
        if (mDisposable != null && !mDisposable.isDisposed()) {
            mDisposable.dispose();
            mDisposable = null;
        }
    }
}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/domain/request/InfoRequester.java
================================================
/*
 * Copyright 2018-present KunMinX
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kunminx.puremusic.domain.request;

import android.annotation.SuppressLint;

import com.kunminx.architecture.data.response.DataResult;
import com.kunminx.architecture.domain.message.MutableResult;
import com.kunminx.architecture.domain.message.Result;
import com.kunminx.architecture.domain.request.Requester;
import com.kunminx.puremusic.data.bean.LibraryInfo;
import com.kunminx.puremusic.data.repository.DataRepository;

import java.util.List;

/**
 * 信息列表 Request
 * <p>
 * TODO tip 1:让 UI 和业务分离,让数据总是从生产者流向消费者
 * <p>
 * UI逻辑和业务逻辑,本质区别在于,前者是数据的消费者,后者是数据的生产者,
 * "领域层组件" 作为数据的生产者,职责应仅限于 "请求调度 和 结果分发",
 * <p>
 * 换言之,"领域层组件" 中应当只关注数据的生成,而不关注数据的使用,
 * 改变 UI 状态的逻辑代码,只应在表现层页面中编写、在 Observer 回调中响应数据的变化,
 * 将来升级到 Jetpack Compose 更是如此,
 * <p>
 * Activity {
 * onCreate(){
 * vm.livedata.observe { result->
 * panel.visible(result.show ? VISIBLE : GONE)
 * tvTitle.setText(result.title)
 * tvContent.setText(result.content)
 * }
 * }
 * <p>
 * TODO tip 2:Requester 通常按业务划分
 * 一个项目中通常可存在多个 Requester 类,
 * 每个页面可根据业务需要,持有多个不同 Requester 实例,
 * 通过 PublishSubject 回推一次性消息,并在表现层 Observer 中分流,
 * 对于 Event,直接执行,对于 State,使用 BehaviorSubject 通知 View 渲染和兜着状态,
 * <p>
 * Activity {
 * onCreate(){
 * request.observe {result ->
 * is Event ? -> execute one time
 * is State ? -> BehaviorSubject setValue and notify
 * }
 * }
 * <p>
 * 如这么说无体会,详见《Jetpack MVVM 分层设计解析》解析
 * https://xiaozhuanlan.com/topic/6741932805
 * <p>
 * <p>
 * Create by KunMinX at 19/11/2
 */
public class InfoRequester extends Requester {

    private final MutableResult<DataResult<List<LibraryInfo>>> mLibraryResult = new MutableResult<>();

    //TODO tip 4:应顺应 "响应式编程",做好 "单向数据流" 开发,
    // MutableResult 应仅限 "鉴权中心" 内部使用,且只暴露 immutable Result 给 UI 层,
    // 通过 "读写分离" 实现数据从 "领域层" 到 "表现层" 的单向流动,

    //如这么说无体会,详见《吃透 LiveData 本质,享用可靠消息鉴权机制》解析。
    //https://xiaozhuanlan.com/topic/6017825943

    public Result<DataResult<List<LibraryInfo>>> getLibraryResult() {
        return mLibraryResult;
    }

    //TODO tip 5: requester 作为数据的生产者,职责应仅限于 "请求调度 和 结果分发",
    //
    // 换言之,此处只关注数据的生成和回推,不关注数据的使用,
    // 改变 UI 状态的逻辑代码,只应在表现层页面中编写,例如 Jetpack Compose 的使用,

    @SuppressLint("CheckResult")
    public void requestLibraryInfo() {
        if (mLibraryResult.getValue() == null)
            DataRepository.getInstance().getLibraryInfo().subscribe(mLibraryResult::setValue);
    }
}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/domain/request/MusicRequester.java
================================================
/*
 * Copyright 2018-present KunMinX
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kunminx.puremusic.domain.request;

import android.annotation.SuppressLint;

import com.kunminx.architecture.data.response.DataResult;
import com.kunminx.architecture.domain.message.MutableResult;
import com.kunminx.architecture.domain.message.Result;
import com.kunminx.architecture.domain.request.Requester;
import com.kunminx.puremusic.data.bean.TestAlbum;
import com.kunminx.puremusic.data.repository.DataRepository;

/**
 * 音乐资源  Request
 * <p>
 * TODO tip 1:让 UI 和业务分离,让数据总是从生产者流向消费者
 * <p>
 * UI逻辑和业务逻辑,本质区别在于,前者是数据的消费者,后者是数据的生产者,
 * "领域层组件" 作为数据的生产者,职责应仅限于 "请求调度 和 结果分发",
 * <p>
 * 换言之,"领域层组件" 中应当只关注数据的生成,而不关注数据的使用,
 * 改变 UI 状态的逻辑代码,只应在表现层页面中编写、在 Observer 回调中响应数据的变化,
 * 将来升级到 Jetpack Compose 更是如此,
 * <p>
 * Activity {
 * onCreate(){
 * vm.livedata.observe { result->
 * panel.visible(result.show ? VISIBLE : GONE)
 * tvTitle.setText(result.title)
 * tvContent.setText(result.content)
 * }
 * }
 * <p>
 * TODO tip 2:Requester 通常按业务划分
 * 一个项目中通常可存在多个 Requester 类,
 * 每个页面可根据业务需要,持有多个不同 Requester 实例,
 * 通过 PublishSubject 回推一次性消息,并在表现层 Observer 中分流,
 * 对于 Event,直接执行,对于 State,使用 BehaviorSubject 通知 View 渲染和兜着状态,
 * <p>
 * Activity {
 * onCreate(){
 * request.observe {result ->
 * is Event ? -> execute one time
 * is State ? -> BehaviorSubject setValue and notify
 * }
 * }
 * <p>
 * 如这么说无体会,详见《Jetpack MVVM 分层设计解析》解析
 * https://xiaozhuanlan.com/topic/6741932805
 * <p>
 * <p>
 * Create by KunMinX at 19/10/29
 */
public class MusicRequester extends Requester {

    private final MutableResult<DataResult<TestAlbum>> mFreeMusicsResult = new MutableResult<>();

    //TODO tip 4:应顺应 "响应式编程",做好 "单向数据流" 开发,
    // MutableResult 应仅限 "鉴权中心" 内部使用,且只暴露 immutable Result 给 UI 层,
    // 通过 "读写分离" 实现数据从 "领域层" 到 "表现层" 的单向流动,

    //如这么说无体会,详见《吃透 LiveData 本质,享用可靠消息鉴权机制》解析。
    //https://xiaozhuanlan.com/topic/6017825943

    public Result<DataResult<TestAlbum>> getFreeMusicsResult() {
        return mFreeMusicsResult;
    }

    //TODO tip 5: requester 作为数据的生产者,职责应仅限于 "请求调度 和 结果分发",
    //
    // 换言之,此处只关注数据的生成和回推,不关注数据的使用,
    // 改变 UI 状态的逻辑代码,只应在表现层页面中编写,例如 Jetpack Compose 的使用,

    @SuppressLint("CheckResult")
    public void requestFreeMusics() {
        DataRepository.getInstance().getFreeMusic().subscribe(mFreeMusicsResult::setValue);
    }
}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/domain/usecase/CanBeStoppedUseCase.java
================================================
/*
 * Copyright 2018-present KunMinX
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kunminx.puremusic.domain.usecase;

import androidx.annotation.NonNull;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;

import com.kunminx.architecture.data.response.DataResult;
import com.kunminx.architecture.domain.usecase.UseCase;
import com.kunminx.puremusic.data.bean.DownloadState;

/**
 * UseCase 示例,实现 LifeCycle 接口,单独服务于 有 “叫停” 需求 的业务
 * <p>
 * TODO tip:
 * 同样是“下载”,我不是在数据层分别写两个方法,
 * 而是遵循开闭原则,在 ViewModel 和 数据层之间,插入一个 UseCase,来专门负责可叫停的情况,
 * 除了开闭原则,使用 UseCase 还有个考虑就是避免内存泄漏,
 * 具体缘由可详见 https://xiaozhuanlan.com/topic/6257931840 评论区 15 楼
 * 以及《这是一份 “架构模式” 自驾攻略》的解析
 * https://xiaozhuanlan.com/topic/8204519736
 * <p>
 * <p>
 * 现已更换为在 MVI-Dispatcher 中处理,具体可参见 DownloadRequest 实现
 * <p>
 * <p>
 * Create by KunMinX at 19/11/25
 */
@Deprecated
public class CanBeStoppedUseCase extends UseCase<CanBeStoppedUseCase.RequestValues,
    CanBeStoppedUseCase.ResponseValue> implements DefaultLifecycleObserver {

//    private final DownloadState downloadState = new DownloadState();

    //TODO tip:让 CanBeStoppedUseCase 可观察页面生命周期,
    // 从而在页面即将退出、且下载请求尚未完成时,
    // 及时通知数据层取消本次请求,以避免资源浪费和一系列不可预期的问题。

    // 关于 Lifecycle 组件的存在意义,可详见《为你还原一个真实的 Jetpack Lifecycle》篇的解析
    // https://xiaozhuanlan.com/topic/3684721950

    @Override
    public void onStop(@NonNull LifecycleOwner owner) {
        if (getRequestValues() != null) {
//            downloadState.isForgive = true;
//            downloadState.file = null;
//            downloadState.progress = 0;
//            getUseCaseCallback().onError();
        }
    }

    @Override
    protected void executeUseCase(RequestValues requestValues) {

        //访问数据层资源,在 UseCase 中处理带叫停性质的业务

//        DataRepository.getInstance().downloadFile(downloadState, dataResult -> {
//            getUseCaseCallback().onSuccess(new ResponseValue(dataResult));
//        });
    }

    public static final class RequestValues implements UseCase.RequestValues {

    }

    public static final class ResponseValue implements UseCase.ResponseValue {

        private final DataResult<DownloadState> mDataResult;

        public ResponseValue(DataResult<DownloadState> dataResult) {
            mDataResult = dataResult;
        }

        public DataResult<DownloadState> getDataResult() {
            return mDataResult;
        }
    }
}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/domain/usecase/DownloadUseCase.java
================================================
package com.kunminx.puremusic.domain.usecase;

import com.kunminx.architecture.domain.usecase.UseCase;
import com.kunminx.puremusic.data.config.Const;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;

/**
 * Create by KunMinX at 20/03/16
 */
public class DownloadUseCase extends UseCase<DownloadUseCase.RequestValues, DownloadUseCase.ResponseValue> {

    @Override
    protected void executeUseCase(RequestValues requestValues) {
        try {
            URL url = new URL(requestValues.url);
            InputStream is = url.openStream();
            File file = new File(Const.COVER_PATH, requestValues.path);
            OutputStream os = new FileOutputStream(file);
            byte[] buffer = new byte[1024];
            int len = 0;
            while ((len = is.read(buffer)) > 0) {
                os.write(buffer, 0, len);
            }
            is.close();
            os.close();

            getUseCaseCallback().onSuccess(new ResponseValue(file));

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static final class RequestValues implements UseCase.RequestValues {
        private String url;
        private String path;

        public RequestValues(String url, String path) {
            this.url = url;
            this.path = path;
        }

        public String getUrl() {
            return url;
        }

        public void setUrl(String url) {
            this.url = url;
        }

        public String getPath() {
            return path;
        }

        public void setPath(String path) {
            this.path = path;
        }
    }

    public static final class ResponseValue implements UseCase.ResponseValue {
        private File mFile;

        public ResponseValue(File file) {
            mFile = file;
        }

        public File getFile() {
            return mFile;
        }

        public void setFile(File file) {
            mFile = file;
        }
    }
}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/ui/bind/CommonBindingAdapter.java
================================================
/*
 * Copyright 2018-present KunMinX
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kunminx.puremusic.ui.bind;

import android.graphics.drawable.Drawable;
import android.util.Pair;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.databinding.BindingAdapter;

import com.bumptech.glide.Glide;
import com.kunminx.architecture.utils.ClickUtils;

/**
 * Create by KunMinX at 19/9/18
 */
public class CommonBindingAdapter {

    @BindingAdapter(value = {"imageUrl", "placeHolder"}, requireAll = false)
    public static void imageUrl(ImageView view, String url, Drawable placeHolder) {
        Glide.with(view.getContext()).load(url).placeholder(placeHolder).into(view);
    }

    @BindingAdapter(value = {"visible"}, requireAll = false)
    public static void visible(View view, boolean visible) {
        if (visible && view.getVisibility() == View.GONE) {
            view.setVisibility(View.VISIBLE);
        } else if (!visible && view.getVisibility() == View.VISIBLE) {
            view.setVisibility(View.GONE);
        }
    }

    @BindingAdapter(value = {"invisible"}, requireAll = false)
    public static void invisible(View view, boolean visible) {
        if (visible && view.getVisibility() == View.INVISIBLE) {
            view.setVisibility(View.VISIBLE);
        } else if (!visible && view.getVisibility() == View.VISIBLE) {
            view.setVisibility(View.INVISIBLE);
        }
    }

    @BindingAdapter(value = {"size"}, requireAll = false)
    public static void size(View view, Pair<Integer, Integer> size) {
        CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) view.getLayoutParams();
        params.width = size.first;
        params.height = size.second;
        view.setLayoutParams(params);
    }

    @BindingAdapter(value = {"transX"}, requireAll = false)
    public static void translationX(View view, float translationX) {
        view.setTranslationX(translationX);
    }

    @BindingAdapter(value = {"transY"}, requireAll = false)
    public static void translationY(View view, float translationY) {
        view.setTranslationY(translationY);
    }

    @BindingAdapter(value = {"x"}, requireAll = false)
    public static void x(View view, float x) {
        view.setX(x);
    }

    @BindingAdapter(value = {"y"}, requireAll = false)
    public static void y(View view, float y) {
        view.setY(y);
    }

    @BindingAdapter(value = {"alpha"}, requireAll = false)
    public static void alpha(View view, float alpha) {
        view.setAlpha(alpha);
    }

    @BindingAdapter(value = {"textColor"}, requireAll = false)
    public static void setTextColor(TextView textView, int textColorRes) {
        textView.setTextColor(textView.getContext().getColor(textColorRes));
    }

    @BindingAdapter(value = {"selected"}, requireAll = false)
    public static void selected(View view, boolean select) {
        view.setSelected(select);
    }

    @BindingAdapter(value = {"onClickWithDebouncing"}, requireAll = false)
    public static void onClickWithDebouncing(View view, View.OnClickListener clickListener) {
        ClickUtils.applySingleDebouncing(view, clickListener);
    }
}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/ui/bind/DrawerBindingAdapter.java
================================================
package com.kunminx.puremusic.ui.bind;

import androidx.core.view.GravityCompat;
import androidx.databinding.BindingAdapter;
import androidx.drawerlayout.widget.DrawerLayout;

/**
 * Create by KunMinX at 2020/3/13
 */
public class DrawerBindingAdapter {

    @BindingAdapter(value = {"isOpenDrawer"}, requireAll = false)
    public static void openDrawer(DrawerLayout drawerLayout, boolean isOpenDrawer) {
        if (isOpenDrawer && !drawerLayout.isDrawerOpen(GravityCompat.START)) {
            drawerLayout.openDrawer(GravityCompat.START);
        } else {
            drawerLayout.closeDrawer(GravityCompat.START);
        }
    }

    @BindingAdapter(value = {"allowDrawerOpen"}, requireAll = false)
    public static void allowDrawerOpen(DrawerLayout drawerLayout, boolean allowDrawerOpen) {
        drawerLayout.setDrawerLockMode(allowDrawerOpen
            ? DrawerLayout.LOCK_MODE_UNLOCKED
            : DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
    }

    @BindingAdapter(value = {"bindDrawerListener"}, requireAll = false)
    public static void listenDrawerState(DrawerLayout drawerLayout, DrawerLayout.SimpleDrawerListener listener) {
        drawerLayout.addDrawerListener(listener);
    }

}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/ui/bind/IconBindingAdapter.java
================================================
package com.kunminx.puremusic.ui.bind;

import androidx.databinding.BindingAdapter;

import com.kunminx.puremusic.ui.view.PlayPauseView;

import net.steamcrafted.materialiconlib.MaterialDrawableBuilder;
import net.steamcrafted.materialiconlib.MaterialIconView;

/**
 * Create by KunMinX at 2020/3/13
 */
public class IconBindingAdapter {

    @BindingAdapter(value = {"isPlaying"}, requireAll = false)
    public static void isPlaying(PlayPauseView pauseView, boolean isPlaying) {
        if (isPlaying) {
            pauseView.play();
        } else {
            pauseView.pause();
        }
    }

    @BindingAdapter(value = {"mdIcon"}, requireAll = false)
    public static void setIcon(MaterialIconView view, MaterialDrawableBuilder.IconValue iconValue) {
        view.setIcon(iconValue);
    }

    @BindingAdapter(value = {"circleAlpha"}, requireAll = false)
    public static void circleAlpha(PlayPauseView pauseView, int circleAlpha) {
        pauseView.setCircleAlpha(circleAlpha);
    }

    @BindingAdapter(value = {"drawableColor"}, requireAll = false)
    public static void drawableColor(PlayPauseView pauseView, int drawableColor) {
        pauseView.setDrawableColor(drawableColor);
    }
}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/ui/bind/TabPageBindingAdapter.java
================================================
package com.kunminx.puremusic.ui.bind;

import androidx.databinding.BindingAdapter;
import androidx.viewpager.widget.ViewPager;

import com.google.android.material.tabs.TabLayout;
import com.kunminx.architecture.ui.adapter.CommonViewPagerAdapter;
import com.kunminx.puremusic.R;

/**
 * Create by KunMinX at 2020/3/13
 */
public class TabPageBindingAdapter {

    @BindingAdapter(value = {"initTabAndPage"}, requireAll = false)
    public static void initTabAndPage(ViewPager viewPager, boolean initTabAndPage) {
        TabLayout tabLayout = (viewPager.getRootView()).findViewById(R.id.tab_layout);
        int count = tabLayout.getTabCount();
        String[] title = new String[count];
        for (int i = 0; i < count; i++) {
            TabLayout.Tab tab = tabLayout.getTabAt(i);
            if (tab != null && tab.getText() != null) {
                title[i] = tab.getText().toString();
            }
        }
        viewPager.setAdapter(new CommonViewPagerAdapter(false, title));
        tabLayout.setupWithViewPager(viewPager);
    }

    @BindingAdapter(value = {"tabSelectedListener"}, requireAll = false)
    public static void tabSelectedListener(TabLayout tabLayout, TabLayout.OnTabSelectedListener listener) {
        tabLayout.addOnTabSelectedListener(listener);
    }

}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/ui/bind/WebViewBindingAdapter.java
================================================
package com.kunminx.puremusic.ui.bind;

import android.annotation.SuppressLint;
import android.content.Intent;
import android.net.Uri;
import android.view.View;
import android.webkit.WebResourceRequest;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;

import androidx.databinding.BindingAdapter;

import com.kunminx.architecture.utils.Utils;

/**
 * Create by KunMinX at 2020/3/13
 */
public class WebViewBindingAdapter {

    @SuppressLint("SetJavaScriptEnabled")
    @BindingAdapter(value = {"pageAssetPath"}, requireAll = false)
    public static void loadAssetsPage(WebView webView, String assetPath) {
        webView.setWebViewClient(new WebViewClient() {
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
                Uri uri = request.getUrl();
                Intent intent = new Intent(Intent.ACTION_VIEW, uri);
                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                Utils.getApp().startActivity(intent);
                return true;
            }
        });
        webView.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);
        WebSettings webSettings = webView.getSettings();
        webSettings.setJavaScriptEnabled(true);
        webSettings.setDefaultTextEncodingName("UTF-8");
        webSettings.setSupportZoom(true);
        webSettings.setBuiltInZoomControls(true);
        webSettings.setDisplayZoomControls(false);
        webSettings.setUseWideViewPort(true);
        webSettings.setLoadWithOverviewMode(true);
        String url = "file:///android_asset/" + assetPath;
        webView.loadUrl(url);
    }

    @SuppressLint("SetJavaScriptEnabled")
    @BindingAdapter(value = {"loadPage"}, requireAll = false)
    public static void loadPage(WebView webView, String loadPage) {
        webView.setWebViewClient(new WebViewClient());
        webView.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);
        WebSettings webSettings = webView.getSettings();
        webSettings.setJavaScriptEnabled(true);
        webSettings.setDefaultTextEncodingName("UTF-8");
        webSettings.setSupportZoom(true);
        webSettings.setBuiltInZoomControls(true);
        webSettings.setDisplayZoomControls(false);
        webSettings.setUseWideViewPort(true);
        webSettings.setLoadWithOverviewMode(true);
        webView.loadUrl(loadPage);
    }

}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/ui/page/DrawerFragment.java
================================================
/*
 * Copyright 2018-present KunMinX
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kunminx.puremusic.ui.page;

import android.os.Bundle;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.kunminx.architecture.ui.page.BaseFragment;
import com.kunminx.architecture.ui.page.DataBindingConfig;
import com.kunminx.architecture.ui.page.StateHolder;
import com.kunminx.architecture.ui.state.State;
import com.kunminx.puremusic.BR;
import com.kunminx.puremusic.R;
import com.kunminx.puremusic.data.bean.LibraryInfo;
import com.kunminx.puremusic.data.config.Const;
import com.kunminx.puremusic.domain.request.InfoRequester;
import com.kunminx.puremusic.ui.page.adapter.DrawerAdapter;

import java.util.ArrayList;
import java.util.List;

/**
 * Create by KunMinX at 19/10/29
 */
public class DrawerFragment extends BaseFragment {

    //TODO tip 1:基于 "单一职责原则",应将 ViewModel 划分为 state-ViewModel 和 result-ViewModel,
    // state-ViewModel 职责仅限于托管、保存和恢复本页面 state,作用域仅限于本页面,
    // result-ViewModel 职责仅限于 "消息分发" 场景承担 "可信源",作用域依 "数据请求" 或 "跨页通信" 消息分发范围而定

    // 如这么说无体会,详见 https://xiaozhuanlan.com/topic/8204519736

    private DrawerStates mStates;
    private InfoRequester mInfoRequester;

    @Override
    protected void initViewModel() {
        mStates = getFragmentScopeViewModel(DrawerStates.class);
        mInfoRequester = getFragmentScopeViewModel(InfoRequester.class);
    }

    @Override
    protected DataBindingConfig getDataBindingConfig() {

        //TODO tip 2: DataBinding 严格模式:
        // 将 DataBinding 实例限制于 base 页面中,默认不向子类暴露,
        // 通过这方式,彻底解决 View 实例 Null 安全一致性问题,
        // 如此,View 实例 Null 安全性将和基于函数式编程思想的 Jetpack Compose 持平。
        // 而 DataBindingConfig 就是在这样背景下,用于为 base 页面 DataBinding 提供绑定项。

        // 如这么说无体会,详见 https://xiaozhuanlan.com/topic/9816742350 和 https://xiaozhuanlan.com/topic/2356748910

        return new DataBindingConfig(R.layout.fragment_drawer, BR.vm, mStates)
            .addBindingParam(BR.click, new ClickProxy())
            .addBindingParam(BR.adapter, new DrawerAdapter(getContext()));
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        //TODO tip 3: 从 PublishSubject 接收回推的数据,并在回调中响应数据的变化,
        // 也即通过 BehaviorSubject(例如 ObservableField)通知控件属性重新渲染,并为其兜住最后一次状态,

        //如这么说无体会,详见 https://xiaozhuanlan.com/topic/6741932805

        mInfoRequester.getLibraryResult().observe(getViewLifecycleOwner(), dataResult -> {
            if (!dataResult.getResponseStatus().isSuccess()) return;
            if (dataResult.getResult() != null) mStates.list.set(dataResult.getResult());
        });

        mInfoRequester.requestLibraryInfo();
    }

    public class ClickProxy {
        public void logoClick() {
            openUrlInBrowser(Const.PROJECT_LINK);
        }
    }

    //TODO tip 5:基于单一职责原则,抽取 Jetpack ViewModel "状态保存和恢复" 的能力作为 StateHolder,
    // 并使用 ObservableField 的改良版子类 State 来承担 BehaviorSubject,用作所绑定控件的 "可信数据源",
    // 从而在收到来自 PublishSubject 的结果回推后,响应结果数据的变化,也即通知控件属性重新渲染,并为其兜住最后一次状态,

    //如这么说无体会,详见 https://xiaozhuanlan.com/topic/6741932805

    public static class DrawerStates extends StateHolder {
        public final State<List<LibraryInfo>> list = new State<>(new ArrayList<>());
    }
}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/ui/page/LoginFragment.java
================================================
/*
 * Copyright 2018-present KunMinX
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kunminx.puremusic.ui.page;

import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.kunminx.architecture.data.config.utils.KeyValueProvider;
import com.kunminx.architecture.ui.page.BaseFragment;
import com.kunminx.architecture.ui.page.DataBindingConfig;
import com.kunminx.architecture.ui.page.StateHolder;
import com.kunminx.architecture.ui.state.State;
import com.kunminx.architecture.utils.ToastUtils;
import com.kunminx.puremusic.BR;
import com.kunminx.puremusic.R;
import com.kunminx.puremusic.data.bean.User;
import com.kunminx.puremusic.data.config.Configs;
import com.kunminx.puremusic.domain.event.Messages;
import com.kunminx.puremusic.domain.message.DrawerCoordinateManager;
import com.kunminx.puremusic.domain.message.PageMessenger;
import com.kunminx.puremusic.domain.request.AccountRequester;

/**
 * Create by KunMinX at 20/04/26
 */
public class LoginFragment extends BaseFragment {

    //TODO tip 1:基于 "单一职责原则",应将 ViewModel 划分为 state-ViewModel 和 result-ViewModel,
    // state-ViewModel 职责仅限于托管、保存和恢复本页面 state,作用域仅限于本页面,
    // result-ViewModel 职责仅限于 "消息分发" 场景承担 "可信源",作用域依 "数据请求" 或 "跨页通信" 消息分发范围而定

    // 如这么说无体会,详见 https://xiaozhuanlan.com/topic/8204519736

    private LoginStates mStates;
    private AccountRequester mAccountRequester;
    private PageMessenger mMessenger;
    private final Configs mConfigs = KeyValueProvider.get(Configs.class);

    @Override
    protected void initViewModel() {
        mStates = getFragmentScopeViewModel(LoginStates.class);
        mMessenger = getApplicationScopeViewModel(PageMessenger.class);
        mAccountRequester = getFragmentScopeViewModel(AccountRequester.class);
    }

    @Override
    protected DataBindingConfig getDataBindingConfig() {

        //TODO tip 2: DataBinding 严格模式:
        // 将 DataBinding 实例限制于 base 页面中,默认不向子类暴露,
        // 通过这方式,彻底解决 View 实例 Null 安全一致性问题,
        // 如此,View 实例 Null 安全性将和基于函数式编程思想的 Jetpack Compose 持平。
        // 而 DataBindingConfig 就是在这样背景下,用于为 base 页面 DataBinding 提供绑定项。

        // 如这么说无体会,详见 https://xiaozhuanlan.com/topic/9816742350 和 https://xiaozhuanlan.com/topic/2356748910

        return new DataBindingConfig(R.layout.fragment_login, BR.vm, mStates)
            .addBindingParam(BR.click, new ClickProxy());
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        getLifecycle().addObserver(DrawerCoordinateManager.getInstance());

        //TODO tip 3:让 accountRequest 可观察页面生命周期,
        // 从而在页面即将退出、且登录请求由于网络延迟尚未完成时,
        // 及时通知数据层取消本次请求,以避免资源浪费和一系列不可预期问题。

        getLifecycle().addObserver(mAccountRequester);
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        //TODO tip 4: 从可信源 Requester 通过 immutable Result 获取请求结果的只读数据,set 给 mutable State,
        //而非 Result、State 不分,直接在页面 set Result,

        //如这么说无体会,详见《吃透 LiveData 本质,享用可靠消息鉴权机制》解析。
        //https://xiaozhuanlan.com/topic/6017825943

        mAccountRequester.getTokenResult().observe(getViewLifecycleOwner(), dataResult -> {
            if (!dataResult.getResponseStatus().isSuccess()) {
                mStates.loadingVisible.set(false);
                ToastUtils.showLongToast(getString(R.string.network_state_retry));
                return;
            }

            String s = dataResult.getResult();
            if (TextUtils.isEmpty(s)) return;

            //TODO tip:成功获取 token 后,可通过 KeyValueX 框架存储配置,
            // 以及通过作用域为 Application 的 PageMessenger 框架通知其他页面刷新状态,
            // 具体详见 Configs 类和 PageMessenger 类说明

            mConfigs.token().set(s);
            mStates.loadingVisible.set(false);
            mMessenger.input(new Messages(Messages.EVENT_LOGIN_SUCCESS));
            nav().navigateUp();
        });
    }

    public class ClickProxy {

        public void back() {
            nav().navigateUp();
        }

        public void login() {

            //TODO tip 5:通过双向绑定,使能通过 state-ViewModel 中与 xml 控件发生绑定的"可观察数据" 拿到控件数据,
            // 避免直接接触控件实例而埋下 Null 安全一致性隐患。

            //如这么说无体会,详见 https://xiaozhuanlan.com/topic/9816742350

            if (TextUtils.isEmpty(mStates.name.get()) || TextUtils.isEmpty(mStates.password.get())) {
                ToastUtils.showLongToast(getString(R.string.username_or_pwd_incomplete));
                return;
            }
            User user = new User(mStates.name.get(), mStates.password.get());
            mAccountRequester.requestLogin(user);
            mStates.loadingVisible.set(true);
        }
    }

    //TODO tip 6:基于单一职责原则,抽取 Jetpack ViewModel "状态保存和恢复" 的能力作为 StateHolder,
    // 并使用 ObservableField 的改良版子类 State 来承担 BehaviorSubject,用作所绑定控件的 "可信数据源",
    // 从而在收到来自 PublishSubject 的结果回推后,响应结果数据的变化,也即通知控件属性重新渲染,并为其兜住最后一次状态,

    //如这么说无体会,详见 https://xiaozhuanlan.com/topic/6741932805

    public static class LoginStates extends StateHolder {

        public final State<String> name = new State<>("");

        public final State<String> password = new State<>("");

        public final State<Boolean> loadingVisible = new State<>(false);

    }

}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/ui/page/MainFragment.java
================================================
/*
 * Copyright 2018-present KunMinX
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kunminx.puremusic.ui.page;

import android.annotation.SuppressLint;
import android.os.Bundle;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.kunminx.architecture.ui.page.BaseFragment;
import com.kunminx.architecture.ui.page.DataBindingConfig;
import com.kunminx.architecture.ui.page.StateHolder;
import com.kunminx.architecture.ui.state.State;
import com.kunminx.puremusic.BR;
import com.kunminx.puremusic.R;
import com.kunminx.puremusic.data.bean.TestAlbum;
import com.kunminx.puremusic.domain.event.Messages;
import com.kunminx.puremusic.domain.message.PageMessenger;
import com.kunminx.puremusic.domain.proxy.PlayerManager;
import com.kunminx.puremusic.domain.request.MusicRequester;
import com.kunminx.puremusic.ui.page.adapter.PlaylistAdapter;

import java.util.ArrayList;
import java.util.List;

/**
 * Create by KunMinX at 19/10/29
 */
public class MainFragment extends BaseFragment {

    //TODO tip 1:基于 "单一职责原则",应将 ViewModel 划分为 state-ViewModel 和 result-ViewModel,
    // state-ViewModel 职责仅限于托管、保存和恢复本页面 state,作用域仅限于本页面,
    // result-ViewModel 职责仅限于 "消息分发" 场景承担 "可信源",作用域依 "数据请求" 或 "跨页通信" 消息分发范围而定

    // 如这么说无体会,详见 https://xiaozhuanlan.com/topic/8204519736

    private MainStates mStates;
    private PageMessenger mMessenger;
    private MusicRequester mMusicRequester;
    private PlaylistAdapter mAdapter;

    @Override
    protected void initViewModel() {
        mStates = getFragmentScopeViewModel(MainStates.class);
        mMessenger = getApplicationScopeViewModel(PageMessenger.class);
        mMusicRequester = getFragmentScopeViewModel(MusicRequester.class);
    }

    @Override
    protected DataBindingConfig getDataBindingConfig() {

        mAdapter = new PlaylistAdapter(getContext());
        mAdapter.setOnItemClickListener((viewId, item, position) -> {
            PlayerManager.getInstance().playAudio(position);
        });

        //TODO tip 2: DataBinding 严格模式:
        // 将 DataBinding 实例限制于 base 页面中,默认不向子类暴露,
        // 通过这方式,彻底解决 View 实例 Null 安全一致性问题,
        // 如此,View 实例 Null 安全性将和基于函数式编程思想的 Jetpack Compose 持平。
        // 而 DataBindingConfig 就是在这样背景下,用于为 base 页面 DataBinding 提供绑定项。

        // 如这么说无体会,详见 https://xiaozhuanlan.com/topic/9816742350 和 https://xiaozhuanlan.com/topic/2356748910

        return new DataBindingConfig(R.layout.fragment_main, BR.vm, mStates)
            .addBindingParam(BR.click, new ClickProxy())
            .addBindingParam(BR.adapter, mAdapter);
    }

    @SuppressLint("NotifyDataSetChanged")
    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        // TODO tip 3:所有播放状态的改变,皆来自 "可信源" PlayerManager 统一分发,
        //  确保 "消息分发可靠一致",避免不可预期推送和错误。

        // 如这么说无体会,详见 https://xiaozhuanlan.com/topic/6017825943 & https://juejin.cn/post/7117498113983512589

        PlayerManager.getInstance().getUiStates().observe(getViewLifecycleOwner(), uiStates -> {
            mStates.musicId.set(uiStates.getMusicId(), changed -> mAdapter.notifyDataSetChanged());
        });

        //TODO tip 4:
        // getViewLifeCycleOwner 是 2020 年新增特性,
        // 主要为了解决 getView() 生命长度 比 fragment 短(仅存活于 onCreateView 之后和 onDestroyView 之前),
        // 导致某些时候 fragment 其他成员还活着,但 getView() 为 null 的 生命周期安全问题,
        // 也即,在 fragment 场景下,请使用 getViewLifeCycleOwner 作为 liveData 观察者。
        // Activity 则不用改变。

        mMusicRequester.getFreeMusicsResult().observe(getViewLifecycleOwner(), dataResult -> {
            if (!dataResult.getResponseStatus().isSuccess()) return;

            TestAlbum musicAlbum = dataResult.getResult();

            // TODO tip 5:未作 UnPeek 处理的 LiveData,在视图控制器重建时会自动倒灌数据
            // 请记得这一点,因为如果没有妥善处理,这里就可能出现预期外错误(例如收到旧数据推送),
            // 所以,再一次,请记得它在重建时一定会倒灌。

            // 如这么说无体会,详见 https://xiaozhuanlan.com/topic/6719328450

            if (musicAlbum != null && musicAlbum.musics != null) {
                mStates.list.set(musicAlbum.musics);
                PlayerManager.getInstance().loadAlbum(musicAlbum);
            }
        });

        mMessenger.output(this, messages -> {
            switch (messages.eventId) {
                case Messages.EVENT_LOGIN_SUCCESS:
                    //TODO tip:
                    //loginFragment 登录成功后的后续处理,例如刷新页面状态等
                    break;
            }
        });

        if (PlayerManager.getInstance().getAlbum() == null) mMusicRequester.requestFreeMusics();
    }

    // TODO tip 7:此处通过 DataBinding 规避 setOnClickListener 时存在的 View 实例 Null 安全一致性问题,

    // 也即,有视图就绑定,无就无绑定,总之 不会因不一致性造成 View 实例 Null 安全问题。
    // 如这么说无体会,详见 https://xiaozhuanlan.com/topic/9816742350

    public class ClickProxy {

        public void openMenu() {

            // TODO tip 8:此处演示向 "可信源" 发送请求,以便实现 "生命周期安全、消息分发可靠一致" 的通知。

            // 如这么说无体会,详见 https://xiaozhuanlan.com/topic/6017825943 & https://juejin.cn/post/7117498113983512589
            // --------
            // 与此同时,此处传达的另一思想是 "最少知道原则",
            // Activity 内部事情在 Activity 内部消化,不要试图在 fragment 中调用和操纵 Activity 内部东西。
            // 因为 Activity 端的处理后续可能会改变,且可受用于更多 fragment,而不单单是本 fragment。

            mMessenger.input(new Messages(Messages.EVENT_OPEN_DRAWER));
        }

        public void login() {
            nav().navigate(R.id.action_mainFragment_to_loginFragment);
        }

        public void search() {
            nav().navigate(R.id.action_mainFragment_to_searchFragment);
        }

    }

    //TODO tip 9:每个页面都需单独准备一个 state-ViewModel,托管与 "控件属性" 发生绑定的 State,
    // 此外,state-ViewModel 职责仅限于状态托管和保存恢复,不建议在此处理 UI 逻辑,

    // UI 逻辑和业务逻辑,本质区别在于,前者是数据的消费者,后者是数据的生产者,
    // 数据总是来自领域层业务逻辑的处理,并单向回推至 UI 层,在 UI 层中响应数据的变化(也即处理 UI 逻辑),
    // 换言之,UI 逻辑只适合在 Activity/Fragment 等视图控制器中编写,将来升级到 Jetpack Compose 更是如此。

    //如这么说无体会,详见 https://xiaozhuanlan.com/topic/6741932805

    public static class MainStates extends StateHolder {

        //TODO tip 10:此处我们使用 "去除防抖特性" 的 ObservableField 子类 State,用以代替 MutableLiveData,

        //如这么说无体会,详见 https://xiaozhuanlan.com/topic/9816742350

        public final State<String> musicId = new State<>("", true);
        public final State<Boolean> initTabAndPage = new State<>(true);

        public final State<String> pageAssetPath = new State<>("summary.html");

        public final State<List<TestAlbum.TestMusic>> list = new State<>(new ArrayList<>());

    }

}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/ui/page/PlayerFragment.java
================================================
/*
 * Copyright 2018-present KunMinX
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kunminx.puremusic.ui.page;

import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.view.View;
import android.widget.SeekBar;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.kunminx.architecture.ui.page.BaseFragment;
import com.kunminx.architecture.ui.page.DataBindingConfig;
import com.kunminx.architecture.ui.page.StateHolder;
import com.kunminx.architecture.ui.state.State;
import com.kunminx.architecture.utils.Res;
import com.kunminx.architecture.utils.ToastUtils;
import com.kunminx.architecture.utils.Utils;
import com.kunminx.player.domain.PlayingInfoManager;
import com.kunminx.puremusic.BR;
import com.kunminx.puremusic.R;
import com.kunminx.puremusic.databinding.FragmentPlayerBinding;
import com.kunminx.puremusic.domain.event.Messages;
import com.kunminx.puremusic.domain.message.DrawerCoordinateManager;
import com.kunminx.puremusic.domain.message.PageMessenger;
import com.kunminx.puremusic.domain.proxy.PlayerManager;
import com.kunminx.puremusic.ui.page.helper.DefaultInterface;
import com.kunminx.puremusic.ui.view.PlayerSlideListener;
import com.sothree.slidinguppanel.SlidingUpPanelLayout;

import net.steamcrafted.materialiconlib.MaterialDrawableBuilder;

/**
 * Create by KunMinX at 19/10/29
 */
public class PlayerFragment extends BaseFragment {

    //TODO tip 1:基于 "单一职责原则",应将 ViewModel 划分为 state-ViewModel 和 result-ViewModel,
    // state-ViewModel 职责仅限于托管、保存和恢复本页面 state,作用域仅限于本页面,
    // result-ViewModel 职责仅限于 "消息分发" 场景承担 "可信源",作用域依 "数据请求" 或 "跨页通信" 消息分发范围而定

    // 如这么说无体会,详见 https://xiaozhuanlan.com/topic/8204519736

    private PlayerStates mStates;
    private PlayerSlideListener.SlideAnimatorStates mAnimatorStates;
    private PageMessenger mMessenger;
    private PlayerSlideListener mListener;

    @Override
    protected void initViewModel() {
        mStates = getFragmentScopeViewModel(PlayerStates.class);
        mAnimatorStates = getFragmentScopeViewModel(PlayerSlideListener.SlideAnimatorStates.class);
        mMessenger = getApplicationScopeViewModel(PageMessenger.class);
    }

    @Override
    protected DataBindingConfig getDataBindingConfig() {

        //TODO tip 2: DataBinding 严格模式:
        // 将 DataBinding 实例限制于 base 页面中,默认不向子类暴露,
        // 通过这方式,彻底解决 View 实例 Null 安全一致性问题,
        // 如此,View 实例 Null 安全性将和基于函数式编程思想的 Jetpack Compose 持平。
        // 而 DataBindingConfig 就是在这样背景下,用于为 base 页面 DataBinding 提供绑定项。

        // 如这么说无体会,详见 https://xiaozhuanlan.com/topic/9816742350 和 https://xiaozhuanlan.com/topic/2356748910

        return new DataBindingConfig(R.layout.fragment_player, BR.vm, mStates)
            .addBindingParam(BR.panelVm, mAnimatorStates)
            .addBindingParam(BR.click, new ClickProxy())
            .addBindingParam(BR.listener, new ListenerHandler());
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        //TODO tip 3: 此处演示使用 "可信源" MVI-Dispatcher input-output 接口完成消息收发

        //如这么说无体会,详见《领域层设计》篇拆解 https://juejin.cn/post/7117498113983512589

        mMessenger.output(this, messages -> {
            switch (messages.eventId) {
                case Messages.EVENT_ADD_SLIDE_LISTENER:
                    if (view.getParent().getParent() instanceof SlidingUpPanelLayout) {
                        SlidingUpPanelLayout sliding = (SlidingUpPanelLayout) view.getParent().getParent();

                        //TODO tip 4: 警惕使用。非必要情况下,尽可能不在子类中拿到 binding 实例乃至获取 view 实例。使用即埋下隐患。
                        // 目前方案是于 debug 模式,对获取实例情况给予提示。

                        // 如这么说无体会,详见 https://xiaozhuanlan.com/topic/9816742350 和 https://xiaozhuanlan.com/topic/2356748910

                        mListener = new PlayerSlideListener((FragmentPlayerBinding) getBinding(), mAnimatorStates, sliding);
                        sliding.addPanelSlideListener(mListener);
                        sliding.addPanelSlideListener(new DefaultInterface.PanelSlideListener() {
                            @Override
                            public void onPanelStateChanged(
                                View view, SlidingUpPanelLayout.PanelState panelState,
                                SlidingUpPanelLayout.PanelState panelState1) {
                                DrawerCoordinateManager.getInstance().requestToUpdateDrawerMode(
                                    panelState1 == SlidingUpPanelLayout.PanelState.EXPANDED,
                                    this.getClass().getSimpleName()
                                );
                            }
                        });
                    }
                    break;
                case Messages.EVENT_CLOSE_SLIDE_PANEL_IF_EXPANDED:
                    // 按下返回键,如果此时 slide 面板是展开的,那么只对面板进行 slide down

                    if (view.getParent().getParent() instanceof SlidingUpPanelLayout) {
                        SlidingUpPanelLayout sliding = (SlidingUpPanelLayout) view.getParent().getParent();
                        if (sliding.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED) {
                            sliding.setPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED);
                        } else {

                            // TODO tip 5:此处演示向 "可信源" 发送请求,以便实现 "生命周期安全、消息分发可靠一致" 的通知。

                            // 如这么说无体会,详见 https://xiaozhuanlan.com/topic/0168753249
                            // --------
                            // 与此同时,此处传达的另一思想是 "最少知道原则",
                            // Activity 内部事情在 Activity 内部消化,不要试图在 fragment 中调用和操纵 Activity 内部东西。
                            // 因为 Activity 端的处理后续可能会改变,且可受用于更多 fragment,而不单单是本 fragment。

                            // TODO: yes:

                            mMessenger.input(new Messages(Messages.EVENT_CLOSE_ACTIVITY_IF_ALLOWED));

                            // TODO: do not:
                            // mActivity.finish();
                        }
                    } else {
                        mMessenger.input(new Messages(Messages.EVENT_CLOSE_ACTIVITY_IF_ALLOWED));
                    }
                    break;
            }
        });

        // TODO tip 6:所有播放状态的改变,皆来自 getUiStates() 统一分发,
        //  确保 "消息分发可靠一致",避免不可预期推送和错误,

        // 细节 1: uiStates 回调只读,此处只可通过 getter 获取只读数据,避免数据被篡改,
        // 细节 2: uiStates 每次都是整个推送,progress 等属性会造成 uiStates 的高频回推,
        //         故此宜对低频变化属性做防抖处理,仅当属性值变化时,通知相关控件完成一次重绘,

        PlayerManager.getInstance().getUiStates().observe(getViewLifecycleOwner(), uiStates -> {
            mStates.musicId.set(uiStates.getMusicId(), changed -> {
                mStates.title.set(uiStates.getTitle());
                mStates.artist.set(uiStates.getSummary());
                mStates.coverImg.set(uiStates.getImg());
                if (mListener != null) view.post(mListener::calculateTitleAndArtist);
                mStates.maxSeekDuration.set(uiStates.getDuration());
            });
            mStates.currentSeekPosition.set(uiStates.getProgress());
            mStates.isPlaying.set(!uiStates.isPaused());
            mStates.repeatMode.set(uiStates.getRepeatMode(), changed -> {
                mStates.playModeIcon.set(PlayerManager.getInstance().getModeIcon(uiStates.getRepeatMode()));
            });
        });
    }

    // TODO tip 7:此处通过 DataBinding 规避 setOnClickListener 时存在的 View 实例 Null 安全一致性问题,

    // 也即,有视图就绑定,无就无绑定,总之 不会因不一致性造成 View 实例 Null 安全问题。
    // 如这么说无体会,详见 https://xiaozhuanlan.com/topic/9816742350

    public class ClickProxy {

        public void playMode() {
            PlayerManager.getInstance().changeMode();
        }

        public void previous() {
            PlayerManager.getInstance().playPrevious();
        }

        public void togglePlay() {
            PlayerManager.getInstance().togglePlay();
        }

        public void next() {
            PlayerManager.getInstance().playNext();
        }

        public void showPlayList() {
            ToastUtils.showShortToast(getString(R.string.unfinished));
        }

        //TODO tip: 同 tip 3

        public void slideDown() {
            mMessenger.input(new Messages(Messages.EVENT_CLOSE_SLIDE_PANEL_IF_EXPANDED));
        }

        public void more() {
        }
    }

    public static class ListenerHandler implements DefaultInterface.OnSeekBarChangeListener {
        @Override
        public void onStopTrackingTouch(SeekBar seekBar) {
            PlayerManager.getInstance().setSeek(seekBar.getProgress());
        }
    }

    //TODO tip 8:基于单一职责原则,抽取 Jetpack ViewModel "状态保存和恢复" 的能力作为 StateHolder,
    // 并使用 ObservableField 的改良版子类 State 来承担 BehaviorSubject,用作所绑定控件的 "可信数据源",
    // 从而在收到来自 PublishSubject 的结果回推后,响应结果数据的变化,也即通知控件属性重新渲染,并为其兜住最后一次状态,

    //如这么说无体会,详见 https://xiaozhuanlan.com/topic/6741932805

    public static class PlayerStates extends StateHolder {
        public final State<String> musicId = new State<>("", true);
        public final State<Enum<PlayingInfoManager.RepeatMode>> repeatMode = new State<>(PlayingInfoManager.RepeatMode.LIST_CYCLE, true);
        public final State<String> title = new State<>(Utils.getApp().getString(R.string.app_name), true);
        public final State<String> artist = new State<>(Utils.getApp().getString(R.string.app_name), true);
        public final State<String> coverImg = new State<>("", true);
        public final State<Drawable> placeHolder = new State<>(Res.getDrawable(R.drawable.bg_album_default), true);
        public final State<Integer> maxSeekDuration = new State<>(0, true);
        public final State<Integer> currentSeekPosition = new State<>(0, true);
        public final State<Boolean> isPlaying = new State<>(false, true);
        public final State<MaterialDrawableBuilder.IconValue> playModeIcon = new State<>(PlayerManager.getInstance().getModeIcon(), true);
    }

}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/ui/page/SearchFragment.java
================================================
/*
 * Copyright 2018-present KunMinX
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kunminx.puremusic.ui.page;

import android.os.Bundle;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.kunminx.architecture.ui.page.BaseFragment;
import com.kunminx.architecture.ui.page.DataBindingConfig;
import com.kunminx.architecture.ui.page.StateHolder;
import com.kunminx.architecture.ui.state.State;
import com.kunminx.puremusic.BR;
import com.kunminx.puremusic.R;
import com.kunminx.puremusic.data.bean.DownloadState;
import com.kunminx.puremusic.data.config.Const;
import com.kunminx.puremusic.domain.event.DownloadEvent;
import com.kunminx.puremusic.domain.message.DrawerCoordinateManager;
import com.kunminx.puremusic.domain.request.DownloadRequester;

/**
 * Create by KunMinX at 19/10/29
 */
public class SearchFragment extends BaseFragment {

    //TODO tip 1:基于 "单一职责原则",应将 ViewModel 划分为 state-ViewModel 和 result-ViewModel,
    // state-ViewModel 职责仅限于托管、保存和恢复本页面 state,作用域仅限于本页面,
    // result-ViewModel 职责仅限于 "消息分发" 场景承担 "可信源",作用域依 "数据请求" 或 "跨页通信" 消息分发范围而定

    // 如这么说无体会,详见 https://xiaozhuanlan.com/topic/8204519736

    private SearchStates mStates;
    private DownloadRequester mDownloadRequester;
    private DownloadRequester mGlobalDownloadRequester;

    @Override
    protected void initViewModel() {
        mStates = getFragmentScopeViewModel(SearchStates.class);
        mDownloadRequester = getFragmentScopeViewModel(DownloadRequester.class);
        mGlobalDownloadRequester = getActivityScopeViewModel(DownloadRequester.class);
    }

    @Override
    protected DataBindingConfig getDataBindingConfig() {

        //TODO tip 2: DataBinding 严格模式:
        // 将 DataBinding 实例限制于 base 页面中,默认不向子类暴露,
        // 通过这方式,彻底解决 View 实例 Null 安全一致性问题,
        // 如此,View 实例 Null 安全性将和基于函数式编程思想的 Jetpack Compose 持平。
        // 而 DataBindingConfig 就是在这样背景下,用于为 base 页面 DataBinding 提供绑定项。

        // 如这么说无体会,详见 https://xiaozhuanlan.com/topic/9816742350 和 https://xiaozhuanlan.com/topic/2356748910

        return new DataBindingConfig(R.layout.fragment_search, BR.vm, mStates)
            .addBindingParam(BR.click, new ClickProxy());
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        getLifecycle().addObserver(DrawerCoordinateManager.getInstance());

        //TODO tip 3:绑定跟随视图控制器生命周期、可叫停、单独放在 UseCase 中处理的业务
        getLifecycle().addObserver(mDownloadRequester);
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        //TODO tip 8: 此处演示使用 MVI-Dispatcher input-output 接口完成数据请求响应

        //如这么说无体会,详见《领域层设计》篇拆解 https://juejin.cn/post/7117498113983512589

        mDownloadRequester.output(this, downloadEvent -> {
            if (downloadEvent.eventId == DownloadEvent.EVENT_DOWNLOAD) {
                DownloadState state = downloadEvent.downloadState;
                mStates.progress_cancelable.set(state.progress);
                mStates.enableDownload.set(state.progress == 100 || state.progress == 0);
            }
        });

        //TODO tip 9: 此处演示 "同一 Result-ViewModel 类,在不同作用域下实例化,造成的不同结果"

        mGlobalDownloadRequester.output(this, downloadEvent -> {
            if (downloadEvent.eventId == DownloadEvent.EVENT_DOWNLOAD_GLOBAL) {
                DownloadState state = downloadEvent.downloadState;
                mStates.progress.set(state.progress);
                mStates.enableGlobalDownload.set(state.progress == 100 || state.progress == 0);
            }
        });
    }

    // TODO tip 4:此处通过 DataBinding 规避 setOnClickListener 时存在的 View 实例 Null 安全一致性问题,

    // 也即,有视图就绑定,无就无绑定,总之 不会因不一致性造成 View 实例 Null 安全问题。
    // 如这么说无体会,详见 https://xiaozhuanlan.com/topic/9816742350

    public class ClickProxy {

        public void back() {
            nav().navigateUp();
        }

        public void testNav() {
            openUrlInBrowser(Const.COLUMN_LINK);
        }

        public void subscribe() {
            openUrlInBrowser(Const.COLUMN_LINK);
        }

        //TODO tip: 同 tip 8

        public void testDownload() {
            mGlobalDownloadRequester.input(new DownloadEvent(DownloadEvent.EVENT_DOWNLOAD_GLOBAL));
        }

        //TODO tip 5: 在 UseCase 中 执行可跟随生命周期中止的下载任务

        public void testLifecycleDownload() {
            mDownloadRequester.input(new DownloadEvent(DownloadEvent.EVENT_DOWNLOAD));
        }
    }

    //TODO tip 6:基于单一职责原则,抽取 Jetpack ViewModel "状态保存和恢复" 的能力作为 StateHolder,
    // 并使用 ObservableField 的改良版子类 State 来承担 BehaviorSubject,用作所绑定控件的 "可信数据源",
    // 从而在收到来自 PublishSubject 的结果回推后,响应结果数据的变化,也即通知控件属性重新渲染,并为其兜住最后一次状态,

    //如这么说无体会,详见 https://xiaozhuanlan.com/topic/6741932805

    public static class SearchStates extends StateHolder {

        public final State<Integer> progress = new State<>(1);

        public final State<Integer> progress_cancelable = new State<>(1);

        public final State<Boolean> enableDownload = new State<>(true);

        public final State<Boolean> enableGlobalDownload = new State<>(true);
    }
}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/ui/page/adapter/DiffUtils.java
================================================
/*
 * Copyright 2018-present KunMinX
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kunminx.puremusic.ui.page.adapter;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;

import com.kunminx.puremusic.data.bean.LibraryInfo;
import com.kunminx.puremusic.data.bean.TestAlbum;

/**
 * Create by KunMinX at 2020/7/19
 */
public class DiffUtils {

    private DiffUtil.ItemCallback<LibraryInfo> mLibraryInfoItemCallback;

    private DiffUtil.ItemCallback<TestAlbum.TestMusic> mTestMusicItemCallback;

    private DiffUtils() {
    }

    private static final DiffUtils S_DIFF_UTILS = new DiffUtils();

    public static DiffUtils getInstance() {
        return S_DIFF_UTILS;
    }

    public DiffUtil.ItemCallback<LibraryInfo> getLibraryInfoItemCallback() {
        if (mLibraryInfoItemCallback == null) {
            mLibraryInfoItemCallback = new DiffUtil.ItemCallback<LibraryInfo>() {
                @Override
                public boolean areItemsTheSame(@NonNull LibraryInfo oldItem, @NonNull LibraryInfo newItem) {
                    return oldItem.equals(newItem);
                }

                @Override
                public boolean areContentsTheSame(@NonNull LibraryInfo oldItem, @NonNull LibraryInfo newItem) {
                    return oldItem.getTitle().equals(newItem.getTitle());
                }
            };
        }
        return mLibraryInfoItemCallback;
    }

    public DiffUtil.ItemCallback<TestAlbum.TestMusic> getTestMusicItemCallback() {
        if (mTestMusicItemCallback == null) {
            mTestMusicItemCallback = new DiffUtil.ItemCallback<TestAlbum.TestMusic>() {
                @Override
                public boolean areItemsTheSame(@NonNull TestAlbum.TestMusic oldItem, @NonNull TestAlbum.TestMusic newItem) {
                    return oldItem.equals(newItem);
                }

                @Override
                public boolean areContentsTheSame(@NonNull TestAlbum.TestMusic oldItem, @NonNull TestAlbum.TestMusic newItem) {
                    return oldItem.musicId.equals(newItem.musicId);
                }
            };
        }
        return mTestMusicItemCallback;
    }
}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/ui/page/adapter/DrawerAdapter.java
================================================
/*
 * Copyright 2018-present KunMinX
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kunminx.puremusic.ui.page.adapter;

import android.content.Context;
import android.content.Intent;
import android.net.Uri;

import androidx.recyclerview.widget.RecyclerView;

import com.kunminx.binding_recyclerview.adapter.SimpleDataBindingAdapter;
import com.kunminx.puremusic.R;
import com.kunminx.puremusic.data.bean.LibraryInfo;
import com.kunminx.puremusic.databinding.AdapterLibraryBinding;

/**
 * Create by KunMinX at 20/4/19
 */
public class DrawerAdapter extends SimpleDataBindingAdapter<LibraryInfo, AdapterLibraryBinding> {

    public DrawerAdapter(Context context) {
        super(context, R.layout.adapter_library, DiffUtils.getInstance().getLibraryInfoItemCallback());

        //TODO item click 回调可以在 adapter 中实现,也可以在外部实现
        setOnItemClickListener((viewId, item, position) -> {
            Uri uri = Uri.parse(item.getUrl());
            Intent intent = new Intent(Intent.ACTION_VIEW, uri);
            mContext.startActivity(intent);
        });
    }

    @Override
    protected void onBindItem(AdapterLibraryBinding binding, LibraryInfo item, RecyclerView.ViewHolder holder) {
        binding.setInfo(item);
    }
}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/ui/page/adapter/PlaylistAdapter.java
================================================
/*
 * Copyright 2018-present KunMinX
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kunminx.puremusic.ui.page.adapter;

import android.content.Context;
import android.graphics.Color;

import androidx.recyclerview.widget.RecyclerView;

import com.kunminx.binding_recyclerview.adapter.SimpleDataBindingAdapter;
import com.kunminx.puremusic.R;
import com.kunminx.puremusic.data.bean.TestAlbum;
import com.kunminx.puremusic.databinding.AdapterPlayItemBinding;
import com.kunminx.puremusic.domain.proxy.PlayerManager;

/**
 * Create by KunMinX at 20/4/19
 */
public class PlaylistAdapter extends SimpleDataBindingAdapter<TestAlbum.TestMusic, AdapterPlayItemBinding> {

    public PlaylistAdapter(Context context) {
        super(context, R.layout.adapter_play_item, DiffUtils.getInstance().getTestMusicItemCallback());
    }

    @Override
    protected void onBindItem(AdapterPlayItemBinding binding, TestAlbum.TestMusic item, RecyclerView.ViewHolder holder) {
        binding.setAlbum(item);
        int currentIndex = PlayerManager.getInstance().getAlbumIndex();
        binding.ivPlayStatus.setColor(currentIndex == holder.getAbsoluteAdapterPosition()
            ? binding.getRoot().getContext().getColor(R.color.gray) : Color.TRANSPARENT);
    }
}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/ui/page/helper/DefaultInterface.java
================================================
/*
 *
 *  * Copyright 2018-present KunMinX
 *  *
 *  * Licensed under the Apache License, Version 2.0 (the "License");
 *  * you may not use this file except in compliance with the License.
 *  * You may obtain a copy of the License at
 *  *
 *  *    http://www.apache.org/licenses/LICENSE-2.0
 *  *
 *  * Unless required by applicable law or agreed to in writing, software
 *  * distributed under the License is distributed on an "AS IS" BASIS,
 *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  * See the License for the specific language governing permissions and
 *  * limitations under the License.
 *
 */

package com.kunminx.puremusic.ui.page.helper;

import android.view.View;
import android.widget.SeekBar;

import com.sothree.slidinguppanel.SlidingUpPanelLayout;

/**
 * Create by KunMinX at 2020/12/3
 */
public class DefaultInterface {

    public interface OnSeekBarChangeListener extends SeekBar.OnSeekBarChangeListener {
        @Override
        default void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
        }

        @Override
        default void onStartTrackingTouch(SeekBar seekBar) {
        }

        @Override
        default void onStopTrackingTouch(SeekBar seekBar) {
        }
    }

    public interface PanelSlideListener extends SlidingUpPanelLayout.PanelSlideListener {
        @Override
        default void onPanelSlide(View panel, float slideOffset) {
        }

        @Override
        default void onPanelStateChanged(View panel,
                                         SlidingUpPanelLayout.PanelState previousState,
                                         SlidingUpPanelLayout.PanelState newState) {
        }
    }
}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/ui/view/PlayPauseDrawable.java
================================================
package com.kunminx.puremusic.ui.view;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.util.Property;

import androidx.annotation.ColorInt;

public class PlayPauseDrawable extends Drawable {

    private static final Property<PlayPauseDrawable, Float> PROGRESS = new Property<PlayPauseDrawable, Float>(Float.class, "progress") {
        @Override
        public Float get(PlayPauseDrawable d) {
            return d.getProgress();
        }

        @Override
        public void set(PlayPauseDrawable d, Float value) {
            d.setProgress(value);
        }
    };

    private final Path mLeftPauseBar = new Path();
    private final Path mRightPauseBar = new Path();
    private final Paint mPaint = new Paint();
    private final RectF mBounds = new RectF();
    private float mPauseBarWidth;
    private float mPauseBarHeight;
    private float mPauseBarDistance;

    private float mWidth;
    private float mHeight;

    private float mProgress;
    private boolean mIsPlay;

    public PlayPauseDrawable() {
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(Color.BLACK);
    }

    public PlayPauseDrawable(@ColorInt int color) {
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(color);
    }

    private static float interpolate(float a, float b, float t) {
        return a + (b - a) * t;
    }

    public void setIsPlay(boolean isPlay) {
        this.mIsPlay = isPlay;
    }

    @Override
    protected void onBoundsChange(Rect bounds) {
        super.onBoundsChange(bounds);
        mBounds.set(bounds);
        mWidth = mBounds.width();
        mHeight = mBounds.height();

        mPauseBarWidth = mWidth / 8;
        mPauseBarHeight = mHeight * 0.40f;
        mPauseBarDistance = mPauseBarWidth;

    }

    @Override
    public void draw(Canvas canvas) {
        mLeftPauseBar.rewind();
        mRightPauseBar.rewind();

        final float barDist = interpolate(mPauseBarDistance, 0, mProgress);
        final float barWidth = interpolate(mPauseBarWidth, mPauseBarHeight / 2f, mProgress);
        final float firstBarTopLeft = interpolate(0, barWidth, mProgress);
        final float secondBarTopRight = interpolate(2 * barWidth + barDist, barWidth + barDist, mProgress);

        mLeftPauseBar.moveTo(0, 0);
        mLeftPauseBar.lineTo(firstBarTopLeft, -mPauseBarHeight);
        mLeftPauseBar.lineTo(barWidth, -mPauseBarHeight);
        mLeftPauseBar.lineTo(barWidth, 0);
        mLeftPauseBar.close();

        mRightPauseBar.moveTo(barWidth + barDist, 0);
        mRightPauseBar.lineTo(barWidth + barDist, -mPauseBarHeight);
        mRightPauseBar.lineTo(secondBarTopRight, -mPauseBarHeight);
        mRightPauseBar.lineTo(2 * barWidth + barDist, 0);
        mRightPauseBar.close();

        canvas.save();

        canvas.translate(interpolate(0, mPauseBarHeight / 8f, mProgress), 0);

        final float rotationProgress = mIsPlay ? 1 - mProgress : mProgress;
        final float startingRotation = mIsPlay ? 90 : 0;
        canvas.rotate(interpolate(startingRotation, startingRotation + 90, rotationProgress), mWidth / 2f, mHeight / 2f);

        canvas.translate(mWidth / 2f - ((2 * barWidth + barDist) / 2f), mHeight / 2f + (mPauseBarHeight / 2f));

        canvas.drawPath(mLeftPauseBar, mPaint);
        canvas.drawPath(mRightPauseBar, mPaint);

        canvas.restore();
    }

    public Animator getPausePlayAnimator() {
        final Animator anim = ObjectAnimator.ofFloat(this, PROGRESS, mIsPlay ? 1 : 0, mIsPlay ? 0 : 1);
        anim.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                mIsPlay = !mIsPlay;
            }
        });
        return anim;
    }

    public boolean isPlay() {
        return mIsPlay;
    }

    private float getProgress() {
        return mProgress;
    }

    private void setProgress(float progress) {
        mProgress = progress;
        invalidateSelf();
    }

    @Override
    public void setAlpha(int alpha) {
        mPaint.setAlpha(alpha);
        invalidateSelf();
    }

    public void setDrawableColor(@ColorInt int color) {
        mPaint.setColor(color);
        invalidateSelf();
    }

    @Override
    public void setColorFilter(ColorFilter cf) {
        mPaint.setColorFilter(cf);
        invalidateSelf();
    }

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSLUCENT;
    }
}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/ui/view/PlayPauseView.java
================================================
package com.kunminx.puremusic.ui.view;

import android.animation.Animator;
import android.animation.AnimatorSet;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Outline;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewOutlineProvider;
import android.view.animation.DecelerateInterpolator;
import android.widget.FrameLayout;

import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;

import com.kunminx.puremusic.R;

public class PlayPauseView extends FrameLayout {

    private static final long PLAY_PAUSE_ANIMATION_DURATION = 200;
    public final boolean isDrawCircle;
    private final PlayPauseDrawable mDrawable;
    private final Paint mPaint = new Paint();
    public int circleAlpha;
    private int mDrawableColor;
    private AnimatorSet mAnimatorSet;
    private int mBackgroundColor;
    private int mWidth;
    private int mHeight;
    private boolean mIsPlay;

    public PlayPauseView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setWillNotDraw(false);

        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.PlayPauseView);
        isDrawCircle = typedArray.getBoolean(R.styleable.PlayPauseView_isCircleDraw, true);
        circleAlpha = typedArray.getInt(R.styleable.PlayPauseView_circleAlpha, 255);
        mDrawableColor = typedArray.getInt(R.styleable.PlayPauseView_drawableColor, Color.WHITE);
        typedArray.recycle();

        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setAlpha(circleAlpha);
        mPaint.setColor(mBackgroundColor);
        mDrawable = new PlayPauseDrawable(mDrawableColor);
        mDrawable.setCallback(this);

    }

    @Override
    protected void onSizeChanged(final int w, final int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mDrawable.setBounds(0, 0, w, h);
        mWidth = w;
        mHeight = h;

        setOutlineProvider(new ViewOutlineProvider() {
            @Override
            public void getOutline(View view, Outline outline) {
                outline.setOval(0, 0, view.getWidth(), view.getHeight());
            }
        });
        setClipToOutline(true);
    }

    public void setCircleAlpha(int alpah) {
        circleAlpha = alpah;
        invalidate();
    }

    private int getCircleColor() {
        return mBackgroundColor;
    }

    public void setCircleColor(@ColorInt int color) {
        mBackgroundColor = color;
        invalidate();
    }

    public int getDrawableColor() {
        return mDrawableColor;
    }

    public void setDrawableColor(@ColorInt int color) {
        mDrawableColor = color;
        mDrawable.setDrawableColor(color);
        invalidate();
    }

    @Override
    protected boolean verifyDrawable(@NonNull Drawable who) {
        return who == mDrawable || super.verifyDrawable(who);
    }

    @Override
    public boolean hasOverlappingRendering() {
        return false;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPaint.setColor(mBackgroundColor);
        final float radius = Math.min(mWidth, mHeight) / 2f;
        if (isDrawCircle) {
            mPaint.setColor(mBackgroundColor);
            mPaint.setAlpha(circleAlpha);
            canvas.drawCircle(mWidth / 2f, mHeight / 2f, radius, mPaint);
        }
        mDrawable.draw(canvas);
    }

    public boolean isPlay() {
        return mIsPlay;
    }

    public void play() {
        if (mAnimatorSet != null) {
            mAnimatorSet.cancel();
        }
        mAnimatorSet = new AnimatorSet();
        mDrawable.setIsPlay(mIsPlay = true);
        final Animator pausePlayAnim = mDrawable.getPausePlayAnimator();
        mAnimatorSet.setInterpolator(new DecelerateInterpolator());
        mAnimatorSet.setDuration(PLAY_PAUSE_ANIMATION_DURATION);
        pausePlayAnim.start();
    }

    public void pause() {
        if (mAnimatorSet != null) {
            mAnimatorSet.cancel();
        }
        mAnimatorSet = new AnimatorSet();
        mDrawable.setIsPlay(mIsPlay = false);
        final Animator pausePlayAnim = mDrawable.getPausePlayAnimator();
        mAnimatorSet.setInterpolator(new DecelerateInterpolator());
        mAnimatorSet.setDuration(PLAY_PAUSE_ANIMATION_DURATION);
        pausePlayAnim.start();
    }

}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/ui/view/PlayerSlideListener.java
================================================
/*
 * Copyright 2018-present KunMinX
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kunminx.puremusic.ui.view;

import android.animation.ArgbEvaluator;
import android.animation.FloatEvaluator;
import android.animation.IntEvaluator;
import android.graphics.Color;
import android.graphics.Rect;
import android.util.Pair;
import android.view.View;
import android.widget.TextView;

import com.kunminx.architecture.ui.page.StateHolder;
import com.kunminx.architecture.ui.state.State;
import com.kunminx.architecture.utils.DisplayUtils;
import com.kunminx.architecture.utils.ScreenUtils;
import com.kunminx.puremusic.databinding.FragmentPlayerBinding;
import com.sothree.slidinguppanel.SlidingUpPanelLayout;

/**
 * Create by KunMinX at 19/10/29
 */
public class PlayerSlideListener implements SlidingUpPanelLayout.PanelSlideListener {

    private final FragmentPlayerBinding mBinding;
    private final SlidingUpPanelLayout mSlidingUpPanelLayout;
    private final SlideAnimatorStates mStates;

    private int mTitleEndTranslationX;
    private int mArtistEndTranslationX;
    private int mArtistNormalEndTranslationY;
    private int mContentNormalEndTranslationY;

    private final int mModeStartX;
    private final int mPreviousStartX;
    private final int mPlayPauseStartX;
    private final int mNextStartX;
    private final int mPlayQueueStartX;
    private final int mPlayPauseEndX;
    private final int mPreviousEndX;
    private final int mModeEndX;
    private final int mNextEndX;
    private final int mPlayQueueEndX;
    private final int mIconContainerStartY;
    private final int mIconContainerEndY;

    private final int SCREEN_WIDTH;
    private final int SCREEN_HEIGHT;

    private final IntEvaluator INT_EVALUATOR = new IntEvaluator();
    private final FloatEvaluator FLOAT_EVALUATOR = new FloatEvaluator();
    private final ArgbEvaluator COLOR_EVALUATOR = new ArgbEvaluator();

    private final int NOW_PLAYING_CARD_COLOR;
    private final int PLAY_PAUSE_DRAWABLE_COLOR;
    private Status mStatus = Status.COLLAPSED;

    public enum Status {
        EXPANDED,
        COLLAPSED,
    }

    public PlayerSlideListener(FragmentPlayerBinding binding, SlideAnimatorStates states, SlidingUpPanelLayout slidingUpPanelLayout) {
        mBinding = binding;
        mStates = states;
        mSlidingUpPanelLayout = slidingUpPanelLayout;
        SCREEN_WIDTH = ScreenUtils.getScreenWidth();
        SCREEN_HEIGHT = ScreenUtils.getScreenHeight();
        PLAY_PAUSE_DRAWABLE_COLOR = Color.BLACK;
        NOW_PLAYING_CARD_COLOR = Color.WHITE;
        calculateTitleAndArtist();
        mModeStartX = binding.mode != null ? binding.mode.getLeft() : 0;
        mPreviousStartX = binding.previous.getLeft();
        mPlayPauseStartX = binding.playPause.getLeft();
        mNextStartX = binding.next.getLeft();
        mPlayQueueStartX = binding.icPlayList != null ? binding.icPlayList.getLeft() : 0;
        int size = DisplayUtils.dp2px(36);
        int gap = (SCREEN_WIDTH - 5 * (size)) / 6;
        mPlayPauseEndX = (SCREEN_WIDTH / 2) - (size / 2);
        mPreviousEndX = mPlayPauseEndX - gap - size;
        mModeEndX = mPreviousEndX - gap - size;
        mNextEndX = mPlayPauseEndX + gap + size;
        mPlayQueueEndX = mNextEndX + gap + size;
        mIconContainerStartY = binding.iconContainer.getTop();
        int tempImgSize = DisplayUtils.dp2px(55);
        mStates.albumArtSize.set(new Pair<>(tempImgSize, tempImgSize));
        mIconContainerEndY = SCREEN_HEIGHT - 3 * binding.iconContainer.getHeight() - binding.seekBottom.getHeight();
        mStates.playPauseDrawableColor.set(PLAY_PAUSE_DRAWABLE_COLOR);
        mStates.playCircleAlpha.set(INT_EVALUATOR.evaluate(0, 0, 255));
        mStates.nextX.set(mNextStartX);
        mStates.modeX.set(0);
        mStates.previousX.set(0);
        mStates.playPauseX.set(mPlayPauseStartX);
        mStates.iconContainerY.set(mIconContainerStartY);
        mBinding.executePendingBindings();
    }

    @Override
    public void onPanelSlide(View panel, float slideOffset) {
        calculateTitleAndArtist();
        int tempImgSize = INT_EVALUATOR.evaluate(slideOffset, DisplayUtils.dp2px(55), SCREEN_WIDTH);
        mStates.albumArtSize.set(new Pair<>(tempImgSize, tempImgSize));
        mStates.titleTranslationX.set(FLOAT_EVALUATOR.evaluate(slideOffset, 0, mTitleEndTranslationX));
        mStates.artistTranslationX.set(FLOAT_EVALUATOR.evaluate(slideOffset, 0, mArtistEndTranslationX));
        mStates.artistTranslationY.set(FLOAT_EVALUATOR.evaluate(slideOffset, 0, mArtistNormalEndTranslationY));
        mStates.summaryTranslationY.set(FLOAT_EVALUATOR.evaluate(slideOffset, 0, mContentNormalEndTranslationY));
        mStates.playPauseX.set(INT_EVALUATOR.evaluate(slideOffset, mPlayPauseStartX, mPlayPauseEndX));
        mStates.playCircleAlpha.set(INT_EVALUATOR.evaluate(slideOffset, 0, 255));
        mStates.playPauseDrawableColor.set((int) COLOR_EVALUATOR.evaluate(slideOffset, PLAY_PAUSE_DRAWABLE_COLOR, NOW_PLAYING_CARD_COLOR));
        mStates.previousX.set(INT_EVALUATOR.evaluate(slideOffset, mPreviousStartX, mPreviousEndX));
        mStates.modeX.set(INT_EVALUATOR.evaluate(slideOffset, mModeStartX, mModeEndX));
        mStates.nextX.set(INT_EVALUATOR.evaluate(slideOffset, mNextStartX, mNextEndX));
        mStates.icPlayListX.set(INT_EVALUATOR.evaluate(slideOffset, mPlayQueueStartX, mPlayQueueEndX));
        mStates.modeAlpha.set(FLOAT_EVALUATOR.evaluate(slideOffset, 0, 1));
        mStates.previousAlpha.set(FLOAT_EVALUATOR.evaluate(slideOffset, 0, 1));
        mStates.iconContainerY.set(INT_EVALUATOR.evaluate(slideOffset, mIconContainerStartY, mIconContainerEndY));
        mBinding.executePendingBindings();
    }

    @Override
    public void onPanelStateChanged(View panel, SlidingUpPanelLayout.PanelState previousState, SlidingUpPanelLayout.PanelState newState) {
        if (previousState == SlidingUpPanelLayout.PanelState.COLLAPSED) {
            mStates.songProgressNormalVisibility.set(false);
            mStates.modeVisibility.set(true);
            mStates.previousVisibility.set(true);
        }
        if (newState == SlidingUpPanelLayout.PanelState.EXPANDED) {
            mStatus = Status.EXPANDED;
            mStates.customToolbarVisibility.set(true);
        } else if (newState == SlidingUpPanelLayout.PanelState.COLLAPSED) {
            mStatus = Status.COLLAPSED;
            mStates.songProgressNormalVisibility.set(true);
            mStates.modeVisibility.set(false);
            mStates.previousVisibility.set(false);
            mBinding.topContainer.setOnClickListener(v -> {
                if (mSlidingUpPanelLayout.isTouchEnabled())
                    mSlidingUpPanelLayout.setPanelState(SlidingUpPanelLayout.PanelState.EXPANDED);
            });
        } else if (newState == SlidingUpPanelLayout.PanelState.DRAGGING) {
            mStates.customToolbarVisibility.set(false);
        }
    }

    public void calculateTitleAndArtist() {
        int titleWidth = getTextWidth(mBinding.title != null ? mBinding.title : null);
        int artistWidth = getTextWidth(mBinding.artist != null ? mBinding.artist : null);
        mTitleEndTranslationX = (SCREEN_WIDTH / 2) - (titleWidth / 2) - DisplayUtils.dp2px(67);
        mArtistEndTranslationX = (SCREEN_WIDTH / 2) - (artistWidth / 2) - DisplayUtils.dp2px(67);
        mArtistNormalEndTranslationY = DisplayUtils.dp2px(12);
        mContentNormalEndTranslationY = SCREEN_WIDTH + DisplayUtils.dp2px(32);
        mStates.titleTranslationX.set(mStatus == Status.COLLAPSED ? 0f : mTitleEndTranslationX);
        mStates.artistTranslationX.set(mStatus == Status.COLLAPSED ? 0f : mArtistEndTranslationX);
    }

    private int getTextWidth(TextView textView) {
        if (textView == null) return 0;
        Rect artistBounds = new Rect();
        textView.getPaint().getTextBounds(textView.getText().toString(), 0, textView.getText().length(), artistBounds);
        return artistBounds.width();
    }

    /**
     * TODO tip:使用 ObservableField 绑定,尽可能减少 View 实例 Null 安全一致性问题
     * <p>
     *  如这么说无体会,详见 https://xiaozhuanlan.com/topic/9816742350 和 https://xiaozhuanlan.com/topic/2356748910
     */
    public static class SlideAnimatorStates extends StateHolder {
        public final State<Float> titleTranslationX = new State<>(0f);
        public final State<Float> artistTranslationX = new State<>(0f);
        public final State<Float> artistTranslationY = new State<>(0f);
        public final State<Float> summaryTranslationY = new State<>(0f);
        public final State<Integer> playPauseX = new State<>(0);
        public final State<Integer> playCircleAlpha = new State<>(0);
        public final State<Integer> playPauseDrawableColor = new State<>(0);
        public final State<Integer> previousX = new State<>(0);
        public final State<Integer> modeX = new State<>(0);
        public final State<Integer> nextX = new State<>(0);
        public final State<Integer> icPlayListX = new State<>(0);
        public final State<Float> modeAlpha = new State<>(0f);
        public final State<Float> previousAlpha = new State<>(0f);
        public final State<Integer> iconContainerY = new State<>(0);
        public final State<Boolean> songProgressNormalVisibility = new State<>(false);
        public final State<Boolean> modeVisibility = new State<>(false);
        public final State<Boolean> previousVisibility = new State<>(false);
        public final State<Boolean> customToolbarVisibility = new State<>(false);
        public final State<Pair<Integer, Integer>> albumArtSize = new State<>(new Pair<>(0, 0));
    }
}


================================================
FILE: app/src/main/java/com/kunminx/puremusic/ui/widget/PlayerService.java
================================================
/*
 * Copyright 2018-present KunMinX
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kunminx.puremusic.ui.widget;

import android.annotation.SuppressLint;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationChannelGroup;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.Build;
import android.os.IBinder;
import android.view.View;
import android.widget.RemoteViews;

import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;

import com.kunminx.architecture.domain.usecase.UseCaseHandler;
import com.kunminx.architecture.utils.ImageUtils;
import com.kunminx.puremusic.MainActivity;
import com.kunminx.puremusic.R;
import com.kunminx.puremusic.data.bean.TestAlbum;
import com.kunminx.puremusic.data.config.Const;
import com.kunminx.puremusic.domain.proxy.PlayerManager;
import com.kunminx.puremusic.domain.usecase.DownloadUseCase;

import java.io.File;

/**
 * Create by KunMinX at 19/7/17
 */
public class PlayerService extends Service {

    public static final String NOTIFY_PREVIOUS = "pure_music.kunminx.previous";
    public static final String NOTIFY_CLOSE = "pure_music.kunminx.close";
    public static final String NOTIFY_PAUSE = "pure_music.kunminx.pause";
    public static final String NOTIFY_PLAY = "pure_music.kunminx.play";
    public static final String NOTIFY_NEXT = "pure_music.kunminx.next";
    private static final String GROUP_ID = "group_001";
    private static final String CHANNEL_ID = "channel_001";
    private DownloadUseCase mDownloadUseCase;

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        TestAlbum.TestMusic results = PlayerManager.getInstance().getCurrentPlayingMusic();
        if (results == null) {
            stopSelf();
            return START_NOT_STICKY;
        }

        createNotification(results);
        return START_NOT_STICKY;
    }

    private void createNotification(TestAlbum.TestMusic testMusic) {
        try {
            String title = testMusic.title;
            TestAlbum album = PlayerManager.getInstance().getAlbum();
            String summary = album.summary;

            RemoteViews simpleContentView = new RemoteViews(
                getApplicationContext().getPackageName(), R.layout.notify_player_small);

            RemoteViews expandedView;
            expandedView = new RemoteViews(
                getApplicationContext().getPackageName(), R.layout.notify_player_big);

            Intent intent = new Intent(getApplicationContext(), MainActivity.class);
            intent.setAction("showPlayer");

            PendingIntent contentIntent = PendingIntent.getActivity(this, 0, intent,
                Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ? PendingIntent.FLAG_MUTABLE : 0);

            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
                NotificationManager notificationManager = (NotificationManager)
                    getSystemService(Context.NOTIFICATION_SERVICE);

                NotificationChannelGroup playGroup = new NotificationChannelGroup(GROUP_ID, getString(R.string.play));
                notificationManager.createNotificationChannelGroup(playGroup);

                NotificationChannel playChannel = new NotificationChannel(CHANNEL_ID,
                    getString(R.string.notify_of_play), NotificationManager.IMPORTANCE_DEFAULT);
                playChannel.setGroup(GROUP_ID);
                notificationManager.createNotificationChannel(playChannel);
            }

            Notification notification = new NotificationCompat.Builder(
                getApplicationContext(), CHANNEL_ID)
                .setSmallIcon(R.drawable.ic_player)
                .setContentIntent(contentIntent)
                .setOnlyAlertOnce(true)
                .setContentTitle(title).build();

            notification.contentView = simpleContentView;
            notification.bigContentView = expandedView;

            setListeners(simpleContentView);
            setListeners(expandedView);

            notification.contentView.setViewVisibility(R.id.player_progress_bar, View.GONE);
            notification.contentView.setViewVisibility(R.id.player_next, View.VISIBLE);
            notification.contentView.setViewVisibility(R.id.player_previous, View.VISIBLE);
            notification.bigContentView.setViewVisibility(R.id.player_next, View.VISIBLE);
            notification.bigContentView.setViewVisibility(R.id.player_previous, View.VISIBLE);
            notification.bigContentView.setViewVisibility(R.id.player_progress_bar, View.GONE);

            boolean isPaused = PlayerManager.getInstance().isPaused();
            notification.contentView.setViewVisibility(R.id.player_pause, isPaused ? View.GONE : View.VISIBLE);
            notification.contentView.setViewVisibility(R.id.player_play, isPaused ? View.VISIBLE : View.GONE);
            notification.bigContentView.setViewVisibility(R.id.player_pause, isPaused ? View.GONE : View.VISIBLE);
            notification.bigContentView.setViewVisibility(R.id.player_play, isPaused ? View.VISIBLE : View.GONE);

            notification.contentView.setTextViewText(R.id.player_song_name, title);
            notification.contentView.setTextViewText(R.id.player_author_name, summary);
            notification.bigContentView.setTextViewText(R.id.player_song_name, title);
            notification.bigContentView.setTextViewText(R.id.player_author_name, summary);
            notification.flags |= Notification.FLAG_ONGOING_EVENT;

            String coverPath = Const.COVER_PATH + File.separator + testMusic.musicId + ".jpg";
            Bitmap bitmap = ImageUtils.getBitmap(coverPath);

            if (bitmap != null) {
                notification.contentView.setImageViewBitmap(R.id.player_album_art, bitmap);
                notification.bigContentView.setImageViewBitmap(R.id.player_album_art, bitmap);
            } else {
                requestAlbumCover(testMusic.coverImg, testMusic.musicId);
                notification.contentView.setImageViewResource(R.id.player_album_art, R.drawable.bg_album_default);
                notification.bigContentView.setImageViewResource(R.id.player_album_art, R.drawable.bg_album_default);
            }

            startForeground(5, notification);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @SuppressLint("UnspecifiedImmutableFlag")
    public void setListeners(RemoteViews view) {
        int flags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
            ? PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE
            : PendingIntent.FLAG_UPDATE_CURRENT;
        try {
            PendingIntent pendingIntent = PendingIntent.getBroadcast(getApplicationContext(),
                0, new Intent(NOTIFY_PREVIOUS).setPackage(getPackageName()), flags);
            view.setOnClickPendingIntent(R.id.player_previous, pendingIntent);
            pendingIntent = PendingIntent.getBroadcast(getApplicationContext(),
                0, new Intent(NOTIFY_CLOSE).setPackage(getPackageName()), flags);
            view.setOnClickPendingIntent(R.id.player_close, pendingIntent);
            pendingIntent = PendingIntent.getBroadcast(getApplicationContext(),
                0, new Intent(NOTIFY_PAUSE).setPackage(getPackageName()), flags);
            view.setOnClickPendingIntent(R.id.player_pause, pendingIntent);
            pendingIntent = PendingIntent.getBroadcast(getApplicationContext(),
                0, new Intent(NOTIFY_NEXT).setPackage(getPackageName()), flags);
            view.setOnClickPendingIntent(R.id.player_next, pendingIntent);
            pendingIntent = PendingIntent.getBroadcast(getApplicationContext(),
                0, new Intent(NOTIFY_PLAY).setPackage(getPackageName()), flags);
            view.setOnClickPendingIntent(R.id.player_play, pendingIntent);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void requestAlbumCover(String coverUrl, String musicId) {
        if (mDownloadUseCase == null) {
            mDownloadUseCase = new DownloadUseCase();
        }

        UseCaseHandler.getInstance().execute(mDownloadUseCase,
            new DownloadUseCase.RequestValues(coverUrl, musicId + ".jpg"),
            response -> startService(new Intent(getApplicationContext(), PlayerService.class)));
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }


}


================================================
FILE: app/src/main/res/anim/h_fragment_enter.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="150">
    <translate
        android:fromXDelta="10%p"
        android:interpolator="@android:anim/decelerate_interpolator"
        android:toXDelta="0" />

    <alpha
        android:fromAlpha="0"
        android:toAlpha="1.0" />
</set>

================================================
FILE: app/src/main/res/anim/h_fragment_exit.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="200">
    <translate
        android:fromXDelta="0"
        android:interpolator="@android:anim/accelerate_interpolator"
        android:toXDelta="-10%p" />
</set>

================================================
FILE: app/src/main/res/anim/h_fragment_pop_enter.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="200">
    <translate
        android:fromXDelta="-10%p"
        android:toXDelta="0" />

</set>

================================================
FILE: app/src/main/res/anim/h_fragment_pop_exit.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="200">
    <translate
        android:fromXDelta="0"
        android:toXDelta="10%p" />

    <alpha
        android:fromAlpha="1.0"
        android:toAlpha="0" />
</set>

================================================
FILE: app/src/main/res/drawable/bar_selector_white.xml
================================================
<!--
  ~ Copyright 2018-present KunMinX
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~    http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true">
        <shape android:shape="rectangle">
            <solid android:color="#40ffffff" />
        </shape>
    </item>
    <item android:state_focused="true">
        <shape android:shape="rectangle">
            <solid android:color="#40ffffff" />
        </shape>
    </item>
    <item android:state_selected="true">
        <shape android:shape="rectangle">
            <solid android:color="#40ffffff" />
        </shape>
    </item>
    <item android:drawable="@color/transparent" />
</selector>

================================================
FILE: app/src/main/res/drawable/ic_menu_black_48dp.xml
================================================
<vector android:height="48dp"
    android:tint="#666666"
    android:viewportHeight="24.0"
    android:viewportWidth="24.0"
    android:width="48dp"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <path
        android:fillColor="#FF000000"
        android:pathData="M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z" />
</vector>


================================================
FILE: app/src/main/res/drawable/ic_music_note_black_48dp.xml
================================================
<vector android:height="48dp"
    android:tint="#666666"
    android:viewportHeight="24.0"
    android:viewportWidth="24.0"
    android:width="48dp"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <path
        android:fillColor="#FF000000"
        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" />
</vector>


================================================
FILE: app/src/main/res/drawable/ic_search_black_48dp.xml
================================================
<vector android:height="48dp"
    android:tint="#666666"
    android:viewportHeight="24.0"
    android:viewportWidth="24.0"
    android:width="48dp"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <path
        android:fillColor="#FF000000"
        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" />
</vector>


================================================
FILE: app/src/main/res/drawable/loading_animation.xml
================================================
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/ic_progress"
    android:fromDegrees="0"
    android:pivotX="50%"
    android:pivotY="50%"
    android:toDegrees="360" />

================================================
FILE: app/src/main/res/drawable/progressbar_color.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@+id/progress">
        <scale
            android:scaleWidth="100%"
            android:scaleGravity
Download .txt
gitextract_yu3lvodp/

├── .editorconfig
├── .gitattributes
├── .github/
│   ├── dependabot.yml
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── README.md
├── app/
│   ├── build.gradle
│   ├── libs/
│   │   ├── materialiconlib-1.1.5.aar
│   │   └── videocache-2.7.1.aar
│   ├── proguard-rules.pro
│   └── src/
│       ├── androidTest/
│       │   └── java/
│       │       └── com/
│       │           └── kunminx/
│       │               └── puremusic/
│       │                   └── ExampleInstrumentedTest.java
│       ├── main/
│       │   ├── AndroidManifest.xml
│       │   ├── assets/
│       │   │   └── summary.html
│       │   ├── java/
│       │   │   └── com/
│       │   │       └── kunminx/
│       │   │           └── puremusic/
│       │   │               ├── MainActivity.java
│       │   │               ├── data/
│       │   │               │   ├── api/
│       │   │               │   │   ├── APIs.java
│       │   │               │   │   └── AccountService.java
│       │   │               │   ├── bean/
│       │   │               │   │   ├── DownloadState.java
│       │   │               │   │   ├── LibraryInfo.java
│       │   │               │   │   ├── TestAlbum.java
│       │   │               │   │   └── User.java
│       │   │               │   ├── config/
│       │   │               │   │   ├── Configs.java
│       │   │               │   │   └── Const.java
│       │   │               │   └── repository/
│       │   │               │       └── DataRepository.java
│       │   │               ├── domain/
│       │   │               │   ├── event/
│       │   │               │   │   ├── DownloadEvent.java
│       │   │               │   │   └── Messages.java
│       │   │               │   ├── message/
│       │   │               │   │   ├── DrawerCoordinateManager.java
│       │   │               │   │   ├── PageMessenger.java
│       │   │               │   │   ├── PlayerReceiver.java
│       │   │               │   │   └── SharedViewModel.java
│       │   │               │   ├── proxy/
│       │   │               │   │   └── PlayerManager.java
│       │   │               │   ├── request/
│       │   │               │   │   ├── AccountRequester.java
│       │   │               │   │   ├── DownloadRequester.java
│       │   │               │   │   ├── InfoRequester.java
│       │   │               │   │   └── MusicRequester.java
│       │   │               │   └── usecase/
│       │   │               │       ├── CanBeStoppedUseCase.java
│       │   │               │       └── DownloadUseCase.java
│       │   │               └── ui/
│       │   │                   ├── bind/
│       │   │                   │   ├── CommonBindingAdapter.java
│       │   │                   │   ├── DrawerBindingAdapter.java
│       │   │                   │   ├── IconBindingAdapter.java
│       │   │                   │   ├── TabPageBindingAdapter.java
│       │   │                   │   └── WebViewBindingAdapter.java
│       │   │                   ├── page/
│       │   │                   │   ├── DrawerFragment.java
│       │   │                   │   ├── LoginFragment.java
│       │   │                   │   ├── MainFragment.java
│       │   │                   │   ├── PlayerFragment.java
│       │   │                   │   ├── SearchFragment.java
│       │   │                   │   ├── adapter/
│       │   │                   │   │   ├── DiffUtils.java
│       │   │                   │   │   ├── DrawerAdapter.java
│       │   │                   │   │   └── PlaylistAdapter.java
│       │   │                   │   └── helper/
│       │   │                   │       └── DefaultInterface.java
│       │   │                   ├── view/
│       │   │                   │   ├── PlayPauseDrawable.java
│       │   │                   │   ├── PlayPauseView.java
│       │   │                   │   └── PlayerSlideListener.java
│       │   │                   └── widget/
│       │   │                       └── PlayerService.java
│       │   └── res/
│       │       ├── anim/
│       │       │   ├── h_fragment_enter.xml
│       │       │   ├── h_fragment_exit.xml
│       │       │   ├── h_fragment_pop_enter.xml
│       │       │   └── h_fragment_pop_exit.xml
│       │       ├── drawable/
│       │       │   ├── bar_selector_white.xml
│       │       │   ├── ic_menu_black_48dp.xml
│       │       │   ├── ic_music_note_black_48dp.xml
│       │       │   ├── ic_search_black_48dp.xml
│       │       │   ├── loading_animation.xml
│       │       │   └── progressbar_color.xml
│       │       ├── layout/
│       │       │   ├── activity_main.xml
│       │       │   ├── adapter_library.xml
│       │       │   ├── adapter_play_item.xml
│       │       │   ├── fragment_drawer.xml
│       │       │   ├── fragment_login.xml
│       │       │   ├── fragment_main.xml
│       │       │   ├── fragment_player.xml
│       │       │   ├── fragment_search.xml
│       │       │   ├── notify_player_big.xml
│       │       │   └── notify_player_small.xml
│       │       ├── layout-land/
│       │       │   ├── activity_main.xml
│       │       │   ├── fragment_main.xml
│       │       │   └── fragment_player.xml
│       │       ├── navigation/
│       │       │   ├── nav_drawer.xml
│       │       │   ├── nav_main.xml
│       │       │   └── nav_slide.xml
│       │       ├── values/
│       │       │   ├── attrs.xml
│       │       │   ├── colors.xml
│       │       │   ├── dimen.xml
│       │       │   ├── strings.xml
│       │       │   └── styles.xml
│       │       └── xml/
│       │           └── network_security_config.xml
│       └── test/
│           └── java/
│               └── com/
│                   └── kunminx/
│                       └── puremusic/
│                           └── ExampleUnitTest.java
├── architecture/
│   ├── build.gradle
│   └── src/
│       ├── androidTest/
│       │   └── java/
│       │       └── com/
│       │           └── kunminx/
│       │               └── architecture/
│       │                   └── ExampleInstrumentedTest.java
│       ├── main/
│       │   ├── AndroidManifest.xml
│       │   ├── java/
│       │   │   └── com/
│       │   │       └── kunminx/
│       │   │           └── architecture/
│       │   │               ├── data/
│       │   │               │   └── response/
│       │   │               │       ├── DataResult.java
│       │   │               │       ├── ResponseStatus.java
│       │   │               │       ├── ResultSource.java
│       │   │               │       └── manager/
│       │   │               │           ├── NetworkStateManager.java
│       │   │               │           └── NetworkStateReceive.java
│       │   │               ├── domain/
│       │   │               │   ├── request/
│       │   │               │   │   ├── AsyncTask.java
│       │   │               │   │   └── Requester.java
│       │   │               │   └── usecase/
│       │   │               │       ├── UseCase.java
│       │   │               │       ├── UseCaseHandler.java
│       │   │               │       ├── UseCaseScheduler.java
│       │   │               │       └── UseCaseThreadPoolScheduler.java
│       │   │               ├── ui/
│       │   │               │   ├── adapter/
│       │   │               │   │   └── CommonViewPagerAdapter.java
│       │   │               │   ├── bind/
│       │   │               │   │   └── DrawablesBindingAdapter.java
│       │   │               │   └── page/
│       │   │               │       ├── BaseActivity.java
│       │   │               │       ├── BaseFragment.java
│       │   │               │       └── StateHolder.java
│       │   │               └── utils/
│       │   │                   ├── AdaptScreenUtils.java
│       │   │                   ├── BarUtils.java
│       │   │                   ├── ClickUtils.java
│       │   │                   ├── DisplayUtils.java
│       │   │                   ├── ImageUtils.java
│       │   │                   ├── NetworkUtils.java
│       │   │                   ├── Res.java
│       │   │                   ├── ScreenUtils.java
│       │   │                   ├── ToastUtils.java
│       │   │                   └── Utils.java
│       │   └── res/
│       │       ├── values/
│       │       │   └── strings.xml
│       │       └── xml/
│       │           └── file_paths.xml
│       └── test/
│           └── java/
│               └── com/
│                   └── kunminx/
│                       └── architecture/
│                           └── ExampleUnitTest.java
├── build.gradle
├── gradle/
│   └── wrapper/
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
Download .txt
SYMBOL INDEX (519 symbols across 71 files)

FILE: app/src/androidTest/java/com/kunminx/puremusic/ExampleInstrumentedTest.java
  class ExampleInstrumentedTest (line 18) | @RunWith(AndroidJUnit4.class)
    method useAppContext (line 20) | @Test

FILE: app/src/main/java/com/kunminx/puremusic/MainActivity.java
  class MainActivity (line 39) | public class MainActivity extends BaseActivity {
    method initViewModel (line 51) | @Override
    method getDataBindingConfig (line 57) | @Override
    method onCreate (line 72) | @Override
    method onWindowFocusChanged (line 140) | @Override
    method onBackPressed (line 159) | @Override
    class ListenerHandler (line 167) | public class ListenerHandler extends DrawerLayout.SimpleDrawerListener {
      method onDrawerOpened (line 168) | @Override
      method onDrawerClosed (line 174) | @Override
    class MainActivityStates (line 188) | public static class MainActivityStates extends StateHolder {

FILE: app/src/main/java/com/kunminx/puremusic/data/api/APIs.java
  class APIs (line 6) | public class APIs {

FILE: app/src/main/java/com/kunminx/puremusic/data/api/AccountService.java
  type AccountService (line 11) | public interface AccountService {
    method login (line 13) | @POST("xxx/login")

FILE: app/src/main/java/com/kunminx/puremusic/data/bean/DownloadState.java
  class DownloadState (line 10) | public class DownloadState {
    method DownloadState (line 14) | public DownloadState() {
    method DownloadState (line 19) | public DownloadState(boolean isForgive, int progress) {

FILE: app/src/main/java/com/kunminx/puremusic/data/bean/LibraryInfo.java
  class LibraryInfo (line 26) | public class LibraryInfo {
    method LibraryInfo (line 31) | public LibraryInfo(String title, String summary, String url) {
    method getTitle (line 37) | public String getTitle() {
    method getSummary (line 41) | public String getSummary() {
    method getUrl (line 45) | public String getUrl() {

FILE: app/src/main/java/com/kunminx/puremusic/data/bean/TestAlbum.java
  class TestAlbum (line 32) | public class TestAlbum extends BaseAlbumItem<TestAlbum.TestMusic, TestAl...
    method TestAlbum (line 35) | public TestAlbum(String albumId, String title, String summary, TestArt...
    method getAlbumMid (line 39) | public String getAlbumMid() {
    class TestMusic (line 43) | public static class TestMusic extends BaseMusicItem<TestArtist> {
      method TestMusic (line 46) | public TestMusic(String musicId, String coverImg, String url, String...
      method getSongMid (line 50) | public String getSongMid() {
    class TestArtist (line 55) | public static class TestArtist extends BaseArtistItem {
      method TestArtist (line 58) | public TestArtist(String name) {
      method getBirthday (line 62) | public String getBirthday() {

FILE: app/src/main/java/com/kunminx/puremusic/data/bean/User.java
  class User (line 26) | public class User {
    method User (line 30) | public User(String name, String password) {
    method getName (line 35) | public String getName() {
    method getPassword (line 39) | public String getPassword() {

FILE: app/src/main/java/com/kunminx/puremusic/data/config/Configs.java
  type Configs (line 32) | @KeyValueX
    method token (line 34) | KeyValueString token();
    method isLogin (line 35) | KeyValueBoolean isLogin();
    method alive (line 36) | KeyValueInteger alive();
    method user (line 37) | KeyValueSerializable<User> user();

FILE: app/src/main/java/com/kunminx/puremusic/data/config/Const.java
  class Const (line 10) | public class Const {

FILE: app/src/main/java/com/kunminx/puremusic/data/repository/DataRepository.java
  class DataRepository (line 53) | public class DataRepository {
    method DataRepository (line 57) | private DataRepository() {
    method getInstance (line 60) | public static DataRepository getInstance() {
    method getFreeMusic (line 85) | public Observable<DataResult<TestAlbum>> getFreeMusic() {
    method getLibraryInfo (line 95) | public Observable<DataResult<List<LibraryInfo>>> getLibraryInfo() {
    method downloadFile (line 108) | @SuppressLint("CheckResult")
    method login (line 132) | public Observable<DataResult<String>> login(User user) {

FILE: app/src/main/java/com/kunminx/puremusic/domain/event/DownloadEvent.java
  class DownloadEvent (line 8) | public class DownloadEvent {
    method DownloadEvent (line 15) | public DownloadEvent(int eventId) {
    method DownloadEvent (line 20) | public DownloadEvent(int eventId, DownloadState downloadState) {
    method copy (line 25) | public DownloadEvent copy(DownloadState downloadState) {

FILE: app/src/main/java/com/kunminx/puremusic/domain/event/Messages.java
  class Messages (line 6) | public class Messages {
    method Messages (line 15) | public Messages(int eventId) {

FILE: app/src/main/java/com/kunminx/puremusic/domain/message/DrawerCoordinateManager.java
  class DrawerCoordinateManager (line 47) | public class DrawerCoordinateManager implements DefaultLifecycleObserver {
    method DrawerCoordinateManager (line 51) | private DrawerCoordinateManager() {
    method getInstance (line 54) | public static DrawerCoordinateManager getInstance() {
    method isNoneSecondaryPage (line 60) | private boolean isNoneSecondaryPage() {
    method isEnableSwipeDrawer (line 66) | public Result<Boolean> isEnableSwipeDrawer() {
    method requestToUpdateDrawerMode (line 70) | public void requestToUpdateDrawerMode(boolean pageOpened, String pageN...
    method onCreate (line 85) | @Override
    method onDestroy (line 94) | @Override

FILE: app/src/main/java/com/kunminx/puremusic/domain/message/PageMessenger.java
  class PageMessenger (line 26) | public class PageMessenger extends MviDispatcher<Messages> {
    method onHandle (line 27) | @Override

FILE: app/src/main/java/com/kunminx/puremusic/domain/message/PlayerReceiver.java
  class PlayerReceiver (line 29) | public class PlayerReceiver extends BroadcastReceiver {
    method onReceive (line 31) | @Override

FILE: app/src/main/java/com/kunminx/puremusic/domain/message/SharedViewModel.java
  class SharedViewModel (line 30) | @Deprecated
    method isToAddSlideListener (line 49) | public Result<Boolean> isToAddSlideListener() {
    method isToCloseSlidePanelIfExpanded (line 53) | public Result<Boolean> isToCloseSlidePanelIfExpanded() {
    method isToCloseActivityIfAllowed (line 57) | public Result<Boolean> isToCloseActivityIfAllowed() {
    method isToOpenOrCloseDrawer (line 61) | public Result<Boolean> isToOpenOrCloseDrawer() {
    method requestToCloseActivityIfAllowed (line 65) | public void requestToCloseActivityIfAllowed(boolean allow) {
    method requestToOpenOrCloseDrawer (line 69) | public void requestToOpenOrCloseDrawer(boolean open) {
    method requestToCloseSlidePanelIfExpanded (line 73) | public void requestToCloseSlidePanelIfExpanded(boolean close) {
    method requestToAddSlideListener (line 77) | public void requestToAddSlideListener(boolean add) {

FILE: app/src/main/java/com/kunminx/puremusic/domain/proxy/PlayerManager.java
  class PlayerManager (line 41) | public class PlayerManager implements IPlayController<TestAlbum, TestAlb...
    method PlayerManager (line 47) | private PlayerManager() {
    method getInstance (line 51) | public static PlayerManager getInstance() {
    method init (line 57) | public void init(Context context) {
    method init (line 64) | @Override
    method loadAlbum (line 83) | @Override
    method loadAlbum (line 91) | @Override
    method playAudio (line 96) | @Override
    method playAudio (line 101) | @Override
    method playNext (line 106) | @Override
    method playPrevious (line 111) | @Override
    method playAgain (line 116) | @Override
    method pauseAudio (line 121) | @Override
    method resumeAudio (line 126) | @Override
    method clear (line 131) | @Override
    method changeMode (line 136) | @Override
    method isPlaying (line 141) | @Override
    method isPaused (line 146) | @Override
    method isInit (line 151) | @Override
    method setSeek (line 156) | @Override
    method getTrackTime (line 161) | @Override
    method getUiStates (line 166) | @Override
    method getAlbum (line 171) | @Override
    method getAlbumMusics (line 176) | @Override
    method setChangingPlayingMusic (line 181) | @Override
    method getAlbumIndex (line 186) | @Override
    method getRepeatMode (line 191) | @Override
    method togglePlay (line 196) | @Override
    method getCurrentPlayingMusic (line 201) | @Override
    method getModeIcon (line 206) | public MaterialDrawableBuilder.IconValue getModeIcon(Enum<PlayingInfoM...
    method getModeIcon (line 216) | public MaterialDrawableBuilder.IconValue getModeIcon() {

FILE: app/src/main/java/com/kunminx/puremusic/domain/request/AccountRequester.java
  class AccountRequester (line 64) | public class AccountRequester extends Requester implements DefaultLifecy...
    method getTokenResult (line 79) | public Result<DataResult<String>> getTokenResult() {
    method requestLogin (line 96) | public void requestLogin(User user) {
    method cancelLogin (line 118) | public void cancelLogin() {
    method onStop (line 132) | @Override

FILE: app/src/main/java/com/kunminx/puremusic/domain/request/DownloadRequester.java
  class DownloadRequester (line 41) | public class DownloadRequester extends MviDispatcher<DownloadEvent> {
    method onHandle (line 56) | @Override
    method onStop (line 80) | @Override

FILE: app/src/main/java/com/kunminx/puremusic/domain/request/InfoRequester.java
  class InfoRequester (line 71) | public class InfoRequester extends Requester {
    method getLibraryResult (line 82) | public Result<DataResult<List<LibraryInfo>>> getLibraryResult() {
    method requestLibraryInfo (line 91) | @SuppressLint("CheckResult")

FILE: app/src/main/java/com/kunminx/puremusic/domain/request/MusicRequester.java
  class MusicRequester (line 69) | public class MusicRequester extends Requester {
    method getFreeMusicsResult (line 80) | public Result<DataResult<TestAlbum>> getFreeMusicsResult() {
    method requestFreeMusics (line 89) | @SuppressLint("CheckResult")

FILE: app/src/main/java/com/kunminx/puremusic/domain/usecase/CanBeStoppedUseCase.java
  class CanBeStoppedUseCase (line 44) | @Deprecated
    method onStop (line 57) | @Override
    method executeUseCase (line 67) | @Override
    class RequestValues (line 77) | public static final class RequestValues implements UseCase.RequestValu...
    class ResponseValue (line 81) | public static final class ResponseValue implements UseCase.ResponseVal...
      method ResponseValue (line 85) | public ResponseValue(DataResult<DownloadState> dataResult) {
      method getDataResult (line 89) | public DataResult<DownloadState> getDataResult() {

FILE: app/src/main/java/com/kunminx/puremusic/domain/usecase/DownloadUseCase.java
  class DownloadUseCase (line 16) | public class DownloadUseCase extends UseCase<DownloadUseCase.RequestValu...
    method executeUseCase (line 18) | @Override
    class RequestValues (line 40) | public static final class RequestValues implements UseCase.RequestValu...
      method RequestValues (line 44) | public RequestValues(String url, String path) {
      method getUrl (line 49) | public String getUrl() {
      method setUrl (line 53) | public void setUrl(String url) {
      method getPath (line 57) | public String getPath() {
      method setPath (line 61) | public void setPath(String path) {
    class ResponseValue (line 66) | public static final class ResponseValue implements UseCase.ResponseVal...
      method ResponseValue (line 69) | public ResponseValue(File file) {
      method getFile (line 73) | public File getFile() {
      method setFile (line 77) | public void setFile(File file) {

FILE: app/src/main/java/com/kunminx/puremusic/ui/bind/CommonBindingAdapter.java
  class CommonBindingAdapter (line 34) | public class CommonBindingAdapter {
    method imageUrl (line 36) | @BindingAdapter(value = {"imageUrl", "placeHolder"}, requireAll = false)
    method visible (line 41) | @BindingAdapter(value = {"visible"}, requireAll = false)
    method invisible (line 50) | @BindingAdapter(value = {"invisible"}, requireAll = false)
    method size (line 59) | @BindingAdapter(value = {"size"}, requireAll = false)
    method translationX (line 67) | @BindingAdapter(value = {"transX"}, requireAll = false)
    method translationY (line 72) | @BindingAdapter(value = {"transY"}, requireAll = false)
    method x (line 77) | @BindingAdapter(value = {"x"}, requireAll = false)
    method y (line 82) | @BindingAdapter(value = {"y"}, requireAll = false)
    method alpha (line 87) | @BindingAdapter(value = {"alpha"}, requireAll = false)
    method setTextColor (line 92) | @BindingAdapter(value = {"textColor"}, requireAll = false)
    method selected (line 97) | @BindingAdapter(value = {"selected"}, requireAll = false)
    method onClickWithDebouncing (line 102) | @BindingAdapter(value = {"onClickWithDebouncing"}, requireAll = false)

FILE: app/src/main/java/com/kunminx/puremusic/ui/bind/DrawerBindingAdapter.java
  class DrawerBindingAdapter (line 10) | public class DrawerBindingAdapter {
    method openDrawer (line 12) | @BindingAdapter(value = {"isOpenDrawer"}, requireAll = false)
    method allowDrawerOpen (line 21) | @BindingAdapter(value = {"allowDrawerOpen"}, requireAll = false)
    method listenDrawerState (line 28) | @BindingAdapter(value = {"bindDrawerListener"}, requireAll = false)

FILE: app/src/main/java/com/kunminx/puremusic/ui/bind/IconBindingAdapter.java
  class IconBindingAdapter (line 13) | public class IconBindingAdapter {
    method isPlaying (line 15) | @BindingAdapter(value = {"isPlaying"}, requireAll = false)
    method setIcon (line 24) | @BindingAdapter(value = {"mdIcon"}, requireAll = false)
    method circleAlpha (line 29) | @BindingAdapter(value = {"circleAlpha"}, requireAll = false)
    method drawableColor (line 34) | @BindingAdapter(value = {"drawableColor"}, requireAll = false)

FILE: app/src/main/java/com/kunminx/puremusic/ui/bind/TabPageBindingAdapter.java
  class TabPageBindingAdapter (line 13) | public class TabPageBindingAdapter {
    method initTabAndPage (line 15) | @BindingAdapter(value = {"initTabAndPage"}, requireAll = false)
    method tabSelectedListener (line 30) | @BindingAdapter(value = {"tabSelectedListener"}, requireAll = false)

FILE: app/src/main/java/com/kunminx/puremusic/ui/bind/WebViewBindingAdapter.java
  class WebViewBindingAdapter (line 19) | public class WebViewBindingAdapter {
    method loadAssetsPage (line 21) | @SuppressLint("SetJavaScriptEnabled")
    method loadPage (line 47) | @SuppressLint("SetJavaScriptEnabled")

FILE: app/src/main/java/com/kunminx/puremusic/ui/page/DrawerFragment.java
  class DrawerFragment (line 42) | public class DrawerFragment extends BaseFragment {
    method initViewModel (line 53) | @Override
    method getDataBindingConfig (line 59) | @Override
    method onViewCreated (line 75) | @Override
    class ClickProxy (line 92) | public class ClickProxy {
      method logoClick (line 93) | public void logoClick() {
    class DrawerStates (line 104) | public static class DrawerStates extends StateHolder {

FILE: app/src/main/java/com/kunminx/puremusic/ui/page/LoginFragment.java
  class LoginFragment (line 44) | public class LoginFragment extends BaseFragment {
    method initViewModel (line 57) | @Override
    method getDataBindingConfig (line 64) | @Override
    method onCreate (line 79) | @Override
    method onViewCreated (line 92) | @Override
    class ClickProxy (line 123) | public class ClickProxy {
      method back (line 125) | public void back() {
      method login (line 129) | public void login() {
    class LoginStates (line 152) | public static class LoginStates extends StateHolder {

FILE: app/src/main/java/com/kunminx/puremusic/ui/page/MainFragment.java
  class MainFragment (line 45) | public class MainFragment extends BaseFragment {
    method initViewModel (line 58) | @Override
    method getDataBindingConfig (line 65) | @Override
    method onViewCreated (line 86) | @SuppressLint("NotifyDataSetChanged")
    class ClickProxy (line 141) | public class ClickProxy {
      method openMenu (line 143) | public void openMenu() {
      method login (line 156) | public void login() {
      method search (line 160) | public void search() {
    class MainStates (line 175) | public static class MainStates extends StateHolder {

FILE: app/src/main/java/com/kunminx/puremusic/ui/page/PlayerFragment.java
  class PlayerFragment (line 51) | public class PlayerFragment extends BaseFragment {
    method initViewModel (line 64) | @Override
    method getDataBindingConfig (line 71) | @Override
    method onViewCreated (line 88) | @Override
    class ClickProxy (line 181) | public class ClickProxy {
      method playMode (line 183) | public void playMode() {
      method previous (line 187) | public void previous() {
      method togglePlay (line 191) | public void togglePlay() {
      method next (line 195) | public void next() {
      method showPlayList (line 199) | public void showPlayList() {
      method slideDown (line 205) | public void slideDown() {
      method more (line 209) | public void more() {
    class ListenerHandler (line 213) | public static class ListenerHandler implements DefaultInterface.OnSeek...
      method onStopTrackingTouch (line 214) | @Override
    class PlayerStates (line 226) | public static class PlayerStates extends StateHolder {

FILE: app/src/main/java/com/kunminx/puremusic/ui/page/SearchFragment.java
  class SearchFragment (line 40) | public class SearchFragment extends BaseFragment {
    method initViewModel (line 52) | @Override
    method getDataBindingConfig (line 59) | @Override
    method onCreate (line 74) | @Override
    method onViewCreated (line 84) | @Override
    class ClickProxy (line 116) | public class ClickProxy {
      method back (line 118) | public void back() {
      method testNav (line 122) | public void testNav() {
      method subscribe (line 126) | public void subscribe() {
      method testDownload (line 132) | public void testDownload() {
      method testLifecycleDownload (line 138) | public void testLifecycleDownload() {
    class SearchStates (line 149) | public static class SearchStates extends StateHolder {

FILE: app/src/main/java/com/kunminx/puremusic/ui/page/adapter/DiffUtils.java
  class DiffUtils (line 28) | public class DiffUtils {
    method DiffUtils (line 34) | private DiffUtils() {
    method getInstance (line 39) | public static DiffUtils getInstance() {
    method getLibraryInfoItemCallback (line 43) | public DiffUtil.ItemCallback<LibraryInfo> getLibraryInfoItemCallback() {
    method getTestMusicItemCallback (line 60) | public DiffUtil.ItemCallback<TestAlbum.TestMusic> getTestMusicItemCall...

FILE: app/src/main/java/com/kunminx/puremusic/ui/page/adapter/DrawerAdapter.java
  class DrawerAdapter (line 33) | public class DrawerAdapter extends SimpleDataBindingAdapter<LibraryInfo,...
    method DrawerAdapter (line 35) | public DrawerAdapter(Context context) {
    method onBindItem (line 46) | @Override

FILE: app/src/main/java/com/kunminx/puremusic/ui/page/adapter/PlaylistAdapter.java
  class PlaylistAdapter (line 33) | public class PlaylistAdapter extends SimpleDataBindingAdapter<TestAlbum....
    method PlaylistAdapter (line 35) | public PlaylistAdapter(Context context) {
    method onBindItem (line 39) | @Override

FILE: app/src/main/java/com/kunminx/puremusic/ui/page/helper/DefaultInterface.java
  class DefaultInterface (line 29) | public class DefaultInterface {
    type OnSeekBarChangeListener (line 31) | public interface OnSeekBarChangeListener extends SeekBar.OnSeekBarChan...
      method onProgressChanged (line 32) | @Override
      method onStartTrackingTouch (line 36) | @Override
      method onStopTrackingTouch (line 40) | @Override
    type PanelSlideListener (line 45) | public interface PanelSlideListener extends SlidingUpPanelLayout.Panel...
      method onPanelSlide (line 46) | @Override
      method onPanelStateChanged (line 50) | @Override

FILE: app/src/main/java/com/kunminx/puremusic/ui/view/PlayPauseDrawable.java
  class PlayPauseDrawable (line 19) | public class PlayPauseDrawable extends Drawable {
    method get (line 22) | @Override
    method set (line 27) | @Override
    method PlayPauseDrawable (line 47) | public PlayPauseDrawable() {
    method PlayPauseDrawable (line 53) | public PlayPauseDrawable(@ColorInt int color) {
    method interpolate (line 59) | private static float interpolate(float a, float b, float t) {
    method setIsPlay (line 63) | public void setIsPlay(boolean isPlay) {
    method onBoundsChange (line 67) | @Override
    method draw (line 80) | @Override
    method getPausePlayAnimator (line 118) | public Animator getPausePlayAnimator() {
    method isPlay (line 129) | public boolean isPlay() {
    method getProgress (line 133) | private float getProgress() {
    method setProgress (line 137) | private void setProgress(float progress) {
    method setAlpha (line 142) | @Override
    method setDrawableColor (line 148) | public void setDrawableColor(@ColorInt int color) {
    method setColorFilter (line 153) | @Override
    method getOpacity (line 159) | @Override

FILE: app/src/main/java/com/kunminx/puremusic/ui/view/PlayPauseView.java
  class PlayPauseView (line 23) | public class PlayPauseView extends FrameLayout {
    method PlayPauseView (line 37) | public PlayPauseView(Context context, AttributeSet attrs) {
    method onSizeChanged (line 56) | @Override
    method setCircleAlpha (line 72) | public void setCircleAlpha(int alpah) {
    method getCircleColor (line 77) | private int getCircleColor() {
    method setCircleColor (line 81) | public void setCircleColor(@ColorInt int color) {
    method getDrawableColor (line 86) | public int getDrawableColor() {
    method setDrawableColor (line 90) | public void setDrawableColor(@ColorInt int color) {
    method verifyDrawable (line 96) | @Override
    method hasOverlappingRendering (line 101) | @Override
    method onDraw (line 106) | @Override
    method isPlay (line 119) | public boolean isPlay() {
    method play (line 123) | public void play() {
    method pause (line 135) | public void pause() {

FILE: app/src/main/java/com/kunminx/puremusic/ui/view/PlayerSlideListener.java
  class PlayerSlideListener (line 38) | public class PlayerSlideListener implements SlidingUpPanelLayout.PanelSl...
    type Status (line 73) | public enum Status {
    method PlayerSlideListener (line 78) | public PlayerSlideListener(FragmentPlayerBinding binding, SlideAnimato...
    method onPanelSlide (line 113) | @Override
    method onPanelStateChanged (line 135) | @Override
    method calculateTitleAndArtist (line 159) | public void calculateTitleAndArtist() {
    method getTextWidth (line 170) | private int getTextWidth(TextView textView) {
    class SlideAnimatorStates (line 182) | public static class SlideAnimatorStates extends StateHolder {

FILE: app/src/main/java/com/kunminx/puremusic/ui/widget/PlayerService.java
  class PlayerService (line 51) | public class PlayerService extends Service {
    method onStartCommand (line 62) | @Override
    method createNotification (line 74) | private void createNotification(TestAlbum.TestMusic testMusic) {
    method setListeners (line 157) | @SuppressLint("UnspecifiedImmutableFlag")
    method requestAlbumCover (line 183) | private void requestAlbumCover(String coverUrl, String musicId) {
    method onDestroy (line 193) | @Override
    method onBind (line 198) | @Nullable

FILE: app/src/test/java/com/kunminx/puremusic/ExampleUnitTest.java
  class ExampleUnitTest (line 12) | public class ExampleUnitTest {
    method addition_isCorrect (line 13) | @Test

FILE: architecture/src/androidTest/java/com/kunminx/architecture/ExampleInstrumentedTest.java
  class ExampleInstrumentedTest (line 34) | @RunWith(AndroidJUnit4.class)
    method useAppContext (line 36) | @Test

FILE: architecture/src/main/java/com/kunminx/architecture/data/response/DataResult.java
  class DataResult (line 33) | public class DataResult<T> {
    method DataResult (line 38) | public DataResult(T entity, ResponseStatus responseStatus) {
    method DataResult (line 43) | public DataResult(T entity) {
    method getResult (line 48) | public T getResult() {
    method getResponseStatus (line 52) | public ResponseStatus getResponseStatus() {
    type Result (line 56) | public interface Result<T> {
      method onResult (line 57) | void onResult(DataResult<T> dataResult);

FILE: architecture/src/main/java/com/kunminx/architecture/data/response/ResponseStatus.java
  class ResponseStatus (line 24) | public class ResponseStatus {
    method ResponseStatus (line 30) | public ResponseStatus() {
    method ResponseStatus (line 33) | public ResponseStatus(String responseCode, boolean success) {
    method ResponseStatus (line 38) | public ResponseStatus(String responseCode, boolean success, Enum<Resul...
    method getResponseCode (line 43) | public String getResponseCode() {
    method isSuccess (line 47) | public boolean isSuccess() {
    method getSource (line 51) | public Enum<ResultSource> getSource() {

FILE: architecture/src/main/java/com/kunminx/architecture/data/response/ResultSource.java
  type ResultSource (line 6) | public enum ResultSource {

FILE: architecture/src/main/java/com/kunminx/architecture/data/response/manager/NetworkStateManager.java
  class NetworkStateManager (line 31) | public class NetworkStateManager implements DefaultLifecycleObserver {
    method NetworkStateManager (line 36) | private NetworkStateManager() {
    method getInstance (line 39) | public static NetworkStateManager getInstance() {
    method onResume (line 50) | @Override
    method onPause (line 56) | @Override

FILE: architecture/src/main/java/com/kunminx/architecture/data/response/manager/NetworkStateReceive.java
  class NetworkStateReceive (line 33) | public class NetworkStateReceive extends BroadcastReceiver {
    method onReceive (line 35) | @Override

FILE: architecture/src/main/java/com/kunminx/architecture/domain/request/AsyncTask.java
  class AsyncTask (line 16) | public class AsyncTask {
    method doIO (line 18) | @SuppressLint("CheckResult")
    method doCalculate (line 25) | @SuppressLint("CheckResult")
    type Action (line 32) | public interface Action<T> {
      method onEmit (line 33) | void onEmit(ObservableEmitter<T> emitter);
    type Observer (line 36) | public interface Observer<T> extends io.reactivex.Observer<T> {
      method onSubscribe (line 37) | default void onSubscribe(@NonNull Disposable d) {
      method onNext (line 40) | void onNext(@NonNull T t);
      method onError (line 42) | default void onError(@NonNull Throwable e) {
      method onComplete (line 45) | default void onComplete() {

FILE: architecture/src/main/java/com/kunminx/architecture/domain/request/Requester.java
  class Requester (line 48) | public class Requester extends ViewModel {

FILE: architecture/src/main/java/com/kunminx/architecture/domain/usecase/UseCase.java
  class UseCase (line 9) | public abstract class UseCase<Q extends UseCase.RequestValues, P extends...
    method getRequestValues (line 15) | public Q getRequestValues() {
    method setRequestValues (line 19) | public void setRequestValues(Q requestValues) {
    method getUseCaseCallback (line 23) | public UseCaseCallback<P> getUseCaseCallback() {
    method setUseCaseCallback (line 27) | public void setUseCaseCallback(UseCaseCallback<P> useCaseCallback) {
    method run (line 31) | void run() {
    method executeUseCase (line 35) | protected abstract void executeUseCase(Q requestValues);
    type RequestValues (line 40) | public interface RequestValues {
    type ResponseValue (line 46) | public interface ResponseValue {
    type UseCaseCallback (line 49) | public interface UseCaseCallback<R> {
      method onSuccess (line 50) | void onSuccess(R response);
      method onError (line 52) | default void onError() {

FILE: architecture/src/main/java/com/kunminx/architecture/domain/usecase/UseCaseHandler.java
  class UseCaseHandler (line 6) | public class UseCaseHandler {
    method UseCaseHandler (line 12) | public UseCaseHandler(UseCaseScheduler useCaseScheduler) {
    method getInstance (line 16) | public static UseCaseHandler getInstance() {
    method execute (line 23) | public <T extends UseCase.RequestValues, R extends UseCase.ResponseVal...
    method notifyResponse (line 39) | private <V extends UseCase.ResponseValue> void notifyResponse(final V ...
    method notifyError (line 44) | private <V extends UseCase.ResponseValue> void notifyError(
    class UiCallbackWrapper (line 49) | private static final class UiCallbackWrapper<V extends UseCase.Respons...
      method UiCallbackWrapper (line 54) | public UiCallbackWrapper(UseCase.UseCaseCallback<V> callback,
      method onSuccess (line 60) | @Override
      method onError (line 65) | @Override

FILE: architecture/src/main/java/com/kunminx/architecture/domain/usecase/UseCaseScheduler.java
  type UseCaseScheduler (line 6) | public interface UseCaseScheduler {
    method execute (line 8) | void execute(Runnable runnable);
    method notifyResponse (line 10) | <V extends UseCase.ResponseValue> void notifyResponse(final V response,
    method onError (line 13) | <V extends UseCase.ResponseValue> void onError(

FILE: architecture/src/main/java/com/kunminx/architecture/domain/usecase/UseCaseThreadPoolScheduler.java
  class UseCaseThreadPoolScheduler (line 16) | public class UseCaseThreadPoolScheduler implements UseCaseScheduler {
    method UseCaseThreadPoolScheduler (line 28) | public UseCaseThreadPoolScheduler() {
    method execute (line 33) | @Override
    method notifyResponse (line 38) | @Override
    method onError (line 48) | @Override

FILE: architecture/src/main/java/com/kunminx/architecture/ui/adapter/CommonViewPagerAdapter.java
  class CommonViewPagerAdapter (line 29) | public class CommonViewPagerAdapter extends PagerAdapter {
    method CommonViewPagerAdapter (line 35) | public CommonViewPagerAdapter(boolean enableDestroyItem, String[] titl...
    method getCount (line 41) | @Override
    method isViewFromObject (line 46) | @Override
    method instantiateItem (line 51) | @NonNull
    method destroyItem (line 57) | @Override
    method getPageTitle (line 64) | @Nullable

FILE: architecture/src/main/java/com/kunminx/architecture/ui/bind/DrawablesBindingAdapter.java
  class DrawablesBindingAdapter (line 26) | public class DrawablesBindingAdapter {
    method setViewBackground (line 33) | @BindingAdapter(value = {
    method create (line 772) | public static Drawable create(
    method validShapeMode (line 854) | private static int validShapeMode(@ShapeMode int shapeMode) {
    method mapOrientation (line 859) | private static GradientDrawable.Orientation mapOrientation(@Orientatio...
    method setRingValue (line 883) | private static void setRingValue(GradientDrawable drawable,
    method dip2px (line 918) | private static int dip2px(float dipValue) {
    class ProxyDrawable (line 986) | public static class ProxyDrawable extends StateListDrawable {
      method addState (line 990) | @Override
      method getOriginDrawable (line 998) | Drawable getOriginDrawable() {

FILE: architecture/src/main/java/com/kunminx/architecture/ui/page/BaseActivity.java
  class BaseActivity (line 40) | public abstract class BaseActivity extends DataBindingActivity {
    method onCreate (line 44) | @Override
    method getActivityScopeViewModel (line 69) | protected <T extends ViewModel> T getActivityScopeViewModel(@NonNull C...
    method getApplicationScopeViewModel (line 73) | protected <T extends ViewModel> T getApplicationScopeViewModel(@NonNul...
    method getResources (line 77) | @Override
    method toggleSoftInput (line 86) | protected void toggleSoftInput() {
    method openUrlInBrowser (line 91) | protected void openUrlInBrowser(String url) {

FILE: architecture/src/main/java/com/kunminx/architecture/ui/page/BaseFragment.java
  class BaseFragment (line 35) | public abstract class BaseFragment extends DataBindingFragment {
    method getFragmentScopeViewModel (line 53) | protected <T extends ViewModel> T getFragmentScopeViewModel(@NonNull C...
    method getActivityScopeViewModel (line 57) | protected <T extends ViewModel> T getActivityScopeViewModel(@NonNull C...
    method getApplicationScopeViewModel (line 61) | protected <T extends ViewModel> T getApplicationScopeViewModel(@NonNul...
    method nav (line 65) | protected NavController nav() {
    method toggleSoftInput (line 69) | protected void toggleSoftInput() {
    method openUrlInBrowser (line 74) | protected void openUrlInBrowser(String url) {
    method getApplicationContext (line 80) | protected Context getApplicationContext() {

FILE: architecture/src/main/java/com/kunminx/architecture/ui/page/StateHolder.java
  class StateHolder (line 8) | public class StateHolder extends ViewModel {

FILE: architecture/src/main/java/com/kunminx/architecture/utils/AdaptScreenUtils.java
  class AdaptScreenUtils (line 17) | public final class AdaptScreenUtils {
    method adaptWidth (line 25) | public static Resources adaptWidth(Resources resources, int designWidt...
    method adaptHeight (line 35) | public static Resources adaptHeight(Resources resources, int designHei...
    method closeAdapt (line 46) | public static Resources closeAdapt(Resources resources) {
    method pt2Px (line 59) | public static int pt2Px(float ptValue) {
    method px2Pt (line 70) | public static int px2Pt(float pxValue) {
    method setAppDmXdpi (line 75) | private static void setAppDmXdpi(final float xdpi) {
    method getDisplayMetrics (line 79) | private static DisplayMetrics getDisplayMetrics(Resources resources) {
    method getMiuiTmpMetrics (line 87) | private static DisplayMetrics getMiuiTmpMetrics(Resources resources) {

FILE: architecture/src/main/java/com/kunminx/architecture/utils/BarUtils.java
  class BarUtils (line 38) | public final class BarUtils {
    method BarUtils (line 48) | private BarUtils() {
    method getStatusBarHeight (line 57) | public static int getStatusBarHeight() {
    method setStatusBarVisibility (line 69) | public static void setStatusBarVisibility(@NonNull final AppCompatActi...
    method setStatusBarVisibility (line 80) | public static void setStatusBarVisibility(@NonNull final Window window,
    method isStatusBarVisible (line 99) | public static boolean isStatusBarVisible(@NonNull final AppCompatActiv...
    method setStatusBarLightMode (line 110) | public static void setStatusBarLightMode(@NonNull final AppCompatActiv...
    method setStatusBarLightMode (line 121) | public static void setStatusBarLightMode(@NonNull final Window window,
    method isStatusBarLightMode (line 143) | public static boolean isStatusBarLightMode(@NonNull final AppCompatAct...
    method isStatusBarLightMode (line 153) | public static boolean isStatusBarLightMode(@NonNull final Window windo...
    method addMarginTopEqualStatusBarHeight (line 169) | public static void addMarginTopEqualStatusBarHeight(@NonNull View view) {
    method subtractMarginTopEqualStatusBarHeight (line 188) | public static void subtractMarginTopEqualStatusBarHeight(@NonNull View...
    method addMarginTopEqualStatusBarHeight (line 201) | private static void addMarginTopEqualStatusBarHeight(final Window wind...
    method subtractMarginTopEqualStatusBarHeight (line 209) | private static void subtractMarginTopEqualStatusBarHeight(final Window...
    method setStatusBarColor (line 223) | public static View setStatusBarColor(@NonNull final AppCompatActivity ...
    method setStatusBarColor (line 236) | public static View setStatusBarColor(@NonNull final AppCompatActivity ...
    method setStatusBarColor (line 249) | public static void setStatusBarColor(@NonNull final View fakeStatusBar,
    method setStatusBarCustom (line 268) | public static void setStatusBarCustom(@NonNull final View fakeStatusBa...
    method setStatusBarColor4Drawer (line 296) | public static void setStatusBarColor4Drawer(@NonNull final DrawerLayou...
    method setStatusBarColor4Drawer (line 311) | public static void setStatusBarColor4Drawer(@NonNull final DrawerLayou...
    method applyStatusBarColor (line 332) | private static View applyStatusBarColor(final AppCompatActivity activity,
    method hideStatusBarView (line 351) | private static void hideStatusBarView(final AppCompatActivity activity) {
    method hideStatusBarView (line 355) | private static void hideStatusBarView(final Window window) {
    method showStatusBarView (line 364) | private static void showStatusBarView(final Window window) {
    method createStatusBarView (line 373) | private static View createStatusBarView(final AppCompatActivity activity,
    method transparentStatusBar (line 383) | private static void transparentStatusBar(final AppCompatActivity activ...
    method getActionBarHeight (line 405) | public static int getActionBarHeight() {
    method setNotificationBarVisibility (line 425) | @RequiresPermission(EXPAND_STATUS_BAR)
    method invokePanels (line 436) | private static void invokePanels(final String methodName) {
    method getNavBarHeight (line 458) | public static int getNavBarHeight() {
    method setNavBarVisibility (line 474) | public static void setNavBarVisibility(@NonNull final AppCompatActivit...
    method setNavBarVisibility (line 484) | public static void setNavBarVisibility(@NonNull final Window window, b...
    method isNavBarVisible (line 515) | public static boolean isNavBarVisible(@NonNull final AppCompatActivity...
    method isNavBarVisible (line 526) | public static boolean isNavBarVisible(@NonNull final Window window) {
    method setNavBarColor (line 556) | @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
    method setNavBarColor (line 567) | @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
    method getNavBarColor (line 578) | @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
    method getNavBarColor (line 589) | @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
    method isSupportNavBar (line 599) | public static boolean isSupportNavBar() {
    method getActivityByView (line 612) | private static AppCompatActivity getActivityByView(@NonNull final View...

FILE: architecture/src/main/java/com/kunminx/architecture/utils/ClickUtils.java
  class ClickUtils (line 16) | public class ClickUtils {
    method ClickUtils (line 21) | private ClickUtils() {
    method applySingleDebouncing (line 31) | public static void applySingleDebouncing(final View view, final View.O...
    method applySingleDebouncing (line 41) | public static void applySingleDebouncing(final View[] views, final Vie...
    method applySingleDebouncing (line 52) | public static void applySingleDebouncing(final View[] views,
    method applyDebouncing (line 58) | private static void applyDebouncing(final View[] views,
    class OnDebouncingClickListener (line 78) | public static abstract class OnDebouncingClickListener implements View...
      method OnDebouncingClickListener (line 86) | public OnDebouncingClickListener() {
      method OnDebouncingClickListener (line 90) | public OnDebouncingClickListener(final boolean isGlobal) {
      method OnDebouncingClickListener (line 94) | public OnDebouncingClickListener(final long duration) {
      method OnDebouncingClickListener (line 98) | public OnDebouncingClickListener(final boolean isGlobal, final long ...
      method isValid (line 103) | private static boolean isValid(@NonNull final View view, final long ...
      method onDebouncingClick (line 118) | public abstract void onDebouncingClick(View v);
      method onClick (line 120) | @Override

FILE: architecture/src/main/java/com/kunminx/architecture/utils/DisplayUtils.java
  class DisplayUtils (line 23) | public class DisplayUtils {
    method px2dp (line 30) | public static int px2dp(float pxValue) {
    method dp2px (line 40) | public static int dp2px(float dipValue) {
    method px2sp (line 50) | public static int px2sp(float pxValue) {
    method sp2px (line 60) | public static int sp2px(float spValue) {

FILE: architecture/src/main/java/com/kunminx/architecture/utils/ImageUtils.java
  class ImageUtils (line 14) | public final class ImageUtils {
    method getBitmap (line 22) | public static Bitmap getBitmap(final String filePath) {
    method isSpace (line 29) | private static boolean isSpace(final String s) {

FILE: architecture/src/main/java/com/kunminx/architecture/utils/NetworkUtils.java
  class NetworkUtils (line 19) | public final class NetworkUtils {
    method isConnected (line 27) | @RequiresPermission(ACCESS_NETWORK_STATE)
    method getActiveNetworkInfo (line 33) | @RequiresPermission(ACCESS_NETWORK_STATE)

FILE: architecture/src/main/java/com/kunminx/architecture/utils/Res.java
  class Res (line 11) | public class Res {
    method getDrawable (line 12) | public static Drawable getDrawable(int resId) {

FILE: architecture/src/main/java/com/kunminx/architecture/utils/ScreenUtils.java
  class ScreenUtils (line 32) | public final class ScreenUtils {
    method ScreenUtils (line 34) | private ScreenUtils() {
    method getScreenWidth (line 43) | public static int getScreenWidth() {
    method getScreenHeight (line 55) | public static int getScreenHeight() {
    method getScreenDensity (line 67) | public static float getScreenDensity() {
    method getScreenDensityDpi (line 76) | public static int getScreenDensityDpi() {
    method setFullScreen (line 85) | public static void setFullScreen(@NonNull final AppCompatActivity acti...
    method setNonFullScreen (line 94) | public static void setNonFullScreen(@NonNull final AppCompatActivity a...
    method toggleFullScreen (line 103) | public static void toggleFullScreen(@NonNull final AppCompatActivity a...
    method isFullScreen (line 121) | public static boolean isFullScreen(@NonNull final AppCompatActivity ac...
    method isLandscape (line 131) | public static boolean isLandscape() {
    method setLandscape (line 141) | public static void setLandscape(@NonNull final AppCompatActivity activ...
    method isPortrait (line 150) | public static boolean isPortrait() {
    method setPortrait (line 160) | public static void setPortrait(@NonNull final AppCompatActivity activi...
    method getScreenRotation (line 170) | @SuppressLint("SwitchIntDef")
    method screenShot (line 190) | public static Bitmap screenShot(@NonNull final AppCompatActivity activ...
    method screenShot (line 201) | public static Bitmap screenShot(@NonNull final AppCompatActivity activ...
    method isScreenLock (line 235) | public static boolean isScreenLock() {
    method getSleepDuration (line 246) | public static int getSleepDuration() {
    method setSleepDuration (line 264) | @RequiresPermission(WRITE_SETTINGS)
    method isTablet (line 278) | public static boolean isTablet() {

FILE: architecture/src/main/java/com/kunminx/architecture/utils/ToastUtils.java
  class ToastUtils (line 8) | public class ToastUtils {
    method showLongToast (line 10) | public static void showLongToast(String text) {
    method showShortToast (line 14) | public static void showShortToast(String text) {

FILE: architecture/src/main/java/com/kunminx/architecture/utils/Utils.java
  class Utils (line 43) | public final class Utils {
    method Utils (line 53) | private Utils() {
    method init (line 63) | public static void init(final Context context) {
    method init (line 77) | public static void init(final Application app) {
    method getApp (line 100) | public static Application getApp() {
    method getApplicationByReflect (line 109) | private static Application getApplicationByReflect() {
    type OnAppStatusChangedListener (line 125) | public interface OnAppStatusChangedListener {
      method onForeground (line 126) | void onForeground();
      method onBackground (line 128) | void onBackground();
    type OnActivityDestroyedListener (line 131) | public interface OnActivityDestroyedListener {
      method onActivityDestroyed (line 132) | void onActivityDestroyed(Activity activity);
    class ActivityLifecycleImpl (line 139) | static class ActivityLifecycleImpl implements ActivityLifecycleCallbac...
      method fixSoftInputLeaks (line 149) | private static void fixSoftInputLeaks(final Activity activity) {
      method onActivityCreated (line 180) | @Override
      method onActivityStarted (line 185) | @Override
      method onActivityResumed (line 197) | @Override
      method onActivityPaused (line 206) | @Override
      method onActivityStopped (line 211) | @Override
      method onActivitySaveInstanceState (line 224) | @Override
      method onActivityDestroyed (line 227) | @Override
      method getTopActivity (line 234) | Activity getTopActivity() {
      method setTopActivity (line 248) | private void setTopActivity(final Activity activity) {
      method addOnAppStatusChangedListener (line 262) | void addOnAppStatusChangedListener(final Object object,
      method removeOnAppStatusChangedListener (line 267) | void removeOnAppStatusChangedListener(final Object object) {
      method removeOnActivityDestroyedListener (line 271) | void removeOnActivityDestroyedListener(final Activity activity) {
      method addOnActivityDestroyedListener (line 278) | void addOnActivityDestroyedListener(final Activity activity,
      method postStatus (line 296) | private void postStatus(final boolean isForeground) {
      method consumeOnActivityDestroyedListener (line 312) | private void consumeOnActivityDestroyedListener(Activity activity) {
      method getTopActivityByReflect (line 327) | private Activity getTopActivityByReflect() {
    class FileProvider4UtilCode (line 355) | public static final class FileProvider4UtilCode extends FileProvider {
      method onCreate (line 357) | @Override

FILE: architecture/src/test/java/com/kunminx/architecture/ExampleUnitTest.java
  class ExampleUnitTest (line 28) | public class ExampleUnitTest {
    method addition_isCorrect (line 29) | @Test
Condensed preview — 125 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (441K chars).
[
  {
    "path": ".editorconfig",
    "chars": 182,
    "preview": "[*]\ncharset = utf-8\nij_java_use_single_class_imports = true\nindent_size = 4\nindent_style = space\ninsert_final_newline = "
  },
  {
    "path": ".gitattributes",
    "chars": 52,
    "preview": "* text=auto eol=lf\n\n*.bat text eol=crlf\n*.jar binary"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 183,
    "preview": "version: 2\nupdates:\n  - package-ecosystem: gradle\n    directory: \"/\"\n    schedule:\n      interval: daily\n      time: \"21"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 548,
    "preview": "name: CI\n\non: [ push, pull_request ]\n\njobs:\n  build:\n    name: Build\n    runs-on: ubuntu-latest\n    strategy:\n      matr"
  },
  {
    "path": ".gitignore",
    "chars": 262,
    "preview": ".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\nmav"
  },
  {
    "path": "README.md",
    "chars": 6990,
    "preview": "![](https://images.xiaozhuanlan.com/photo/2021/b106fd65d34a4a724244e7c5b42a2372.jpg)\n\n[《重学安卓》](https://xiaozhuanlan.com/"
  },
  {
    "path": "app/build.gradle",
    "chars": 1665,
    "preview": "apply plugin: \"com.android.application\"\n\nandroid {\n    namespace \"com.kunminx.puremusic\"\n    compileSdk appTargetSdk\n   "
  },
  {
    "path": "app/proguard-rules.pro",
    "chars": 3444,
    "preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
  },
  {
    "path": "app/src/androidTest/java/com/kunminx/puremusic/ExampleInstrumentedTest.java",
    "chars": 769,
    "preview": "package com.kunminx.puremusic;\n\nimport static org.junit.Assert.assertEquals;\n\nimport android.content.Context;\n\nimport an"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "chars": 1974,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:to"
  },
  {
    "path": "app/src/main/assets/summary.html",
    "chars": 35187,
    "preview": "<!doctype html>\n<html>\n<head>\n    <meta charset='UTF-8'>\n    <meta content='width=device-width initial-scale=1' name='vi"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/MainActivity.java",
    "chars": 7060,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/data/api/APIs.java",
    "chars": 167,
    "preview": "package com.kunminx.puremusic.data.api;\n\n/**\n * Create by KunMinX at 2021/6/3\n */\npublic class APIs {\n    public final s"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/data/api/AccountService.java",
    "chars": 400,
    "preview": "package com.kunminx.puremusic.data.api;\n\nimport retrofit2.Call;\nimport retrofit2.http.Field;\nimport retrofit2.http.FormU"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/data/bean/DownloadState.java",
    "chars": 485,
    "preview": "package com.kunminx.puremusic.data.bean;\n\n/**\n * Create by KunMinX at 2022/7/15\n * <p>\n * bean,原始数据,只读,\n * Java 我们通过移除 s"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/data/bean/LibraryInfo.java",
    "chars": 1215,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/data/bean/TestAlbum.java",
    "chars": 1935,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/data/bean/User.java",
    "chars": 1080,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/data/config/Configs.java",
    "chars": 1376,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/data/config/Const.java",
    "chars": 545,
    "preview": "package com.kunminx.puremusic.data.config;\n\nimport android.os.Environment;\n\nimport com.kunminx.architecture.utils.Utils;"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/data/repository/DataRepository.java",
    "chars": 5393,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/domain/event/DownloadEvent.java",
    "chars": 764,
    "preview": "package com.kunminx.puremusic.domain.event;\n\nimport com.kunminx.puremusic.data.bean.DownloadState;\n\n/**\n * Create by Kun"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/domain/event/Messages.java",
    "chars": 513,
    "preview": "package com.kunminx.puremusic.domain.event;\n\n/**\n * Create by KunMinX at 2022/7/4\n */\npublic class Messages {\n    public"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/domain/message/DrawerCoordinateManager.java",
    "chars": 3029,
    "preview": "/*\n *\n *  * Copyright 2018-present KunMinX\n *  *\n *  * Licensed under the Apache License, Version 2.0 (the \"License\");\n "
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/domain/message/PageMessenger.java",
    "chars": 861,
    "preview": "package com.kunminx.puremusic.domain.message;\n\nimport com.kunminx.architecture.domain.dispatch.MviDispatcher;\nimport com"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/domain/message/PlayerReceiver.java",
    "chars": 3311,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/domain/message/SharedViewModel.java",
    "chars": 2504,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/domain/proxy/PlayerManager.java",
    "chars": 5927,
    "preview": "/*\n * Copyright 2018-2019 KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not u"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/domain/request/AccountRequester.java",
    "chars": 4262,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/domain/request/DownloadRequester.java",
    "chars": 3000,
    "preview": "package com.kunminx.puremusic.domain.request;\n\nimport androidx.annotation.NonNull;\nimport androidx.lifecycle.LifecycleOw"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/domain/request/InfoRequester.java",
    "chars": 2950,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/domain/request/MusicRequester.java",
    "chars": 2869,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/domain/usecase/CanBeStoppedUseCase.java",
    "chars": 2936,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/domain/usecase/DownloadUseCase.java",
    "chars": 2073,
    "preview": "package com.kunminx.puremusic.domain.usecase;\n\nimport com.kunminx.architecture.domain.usecase.UseCase;\nimport com.kunmin"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/bind/CommonBindingAdapter.java",
    "chars": 3800,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/bind/DrawerBindingAdapter.java",
    "chars": 1204,
    "preview": "package com.kunminx.puremusic.ui.bind;\n\nimport androidx.core.view.GravityCompat;\nimport androidx.databinding.BindingAdap"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/bind/IconBindingAdapter.java",
    "chars": 1209,
    "preview": "package com.kunminx.puremusic.ui.bind;\n\nimport androidx.databinding.BindingAdapter;\n\nimport com.kunminx.puremusic.ui.vie"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/bind/TabPageBindingAdapter.java",
    "chars": 1291,
    "preview": "package com.kunminx.puremusic.ui.bind;\n\nimport androidx.databinding.BindingAdapter;\nimport androidx.viewpager.widget.Vie"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/bind/WebViewBindingAdapter.java",
    "chars": 2426,
    "preview": "package com.kunminx.puremusic.ui.bind;\n\nimport android.annotation.SuppressLint;\nimport android.content.Intent;\nimport an"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/page/DrawerFragment.java",
    "chars": 3876,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/page/LoginFragment.java",
    "chars": 5867,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/page/MainFragment.java",
    "chars": 7006,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/page/PlayerFragment.java",
    "chars": 10460,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/page/SearchFragment.java",
    "chars": 5721,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/page/adapter/DiffUtils.java",
    "chars": 2703,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/page/adapter/DrawerAdapter.java",
    "chars": 1753,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/page/adapter/PlaylistAdapter.java",
    "chars": 1776,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/page/helper/DefaultInterface.java",
    "chars": 1720,
    "preview": "/*\n *\n *  * Copyright 2018-present KunMinX\n *  *\n *  * Licensed under the Apache License, Version 2.0 (the \"License\");\n "
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/view/PlayPauseDrawable.java",
    "chars": 4897,
    "preview": "package com.kunminx.puremusic.ui.view;\n\nimport android.animation.Animator;\nimport android.animation.AnimatorListenerAdap"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/view/PlayPauseView.java",
    "chars": 4545,
    "preview": "package com.kunminx.puremusic.ui.view;\n\nimport android.animation.Animator;\nimport android.animation.AnimatorSet;\nimport "
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/view/PlayerSlideListener.java",
    "chars": 10151,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "app/src/main/java/com/kunminx/puremusic/ui/widget/PlayerService.java",
    "chars": 9285,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "app/src/main/res/anim/h_fragment_enter.xml",
    "chars": 362,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:durat"
  },
  {
    "path": "app/src/main/res/anim/h_fragment_exit.xml",
    "chars": 288,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:durat"
  },
  {
    "path": "app/src/main/res/anim/h_fragment_pop_enter.xml",
    "chars": 220,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:durat"
  },
  {
    "path": "app/src/main/res/anim/h_fragment_pop_exit.xml",
    "chars": 293,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:durat"
  },
  {
    "path": "app/src/main/res/drawable/bar_selector_white.xml",
    "chars": 1226,
    "preview": "<!--\n  ~ Copyright 2018-present KunMinX\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you "
  },
  {
    "path": "app/src/main/res/drawable/ic_menu_black_48dp.xml",
    "chars": 359,
    "preview": "<vector android:height=\"48dp\"\n    android:tint=\"#666666\"\n    android:viewportHeight=\"24.0\"\n    android:viewportWidth=\"24"
  },
  {
    "path": "app/src/main/res/drawable/ic_music_note_black_48dp.xml",
    "chars": 401,
    "preview": "<vector android:height=\"48dp\"\n    android:tint=\"#666666\"\n    android:viewportHeight=\"24.0\"\n    android:viewportWidth=\"24"
  },
  {
    "path": "app/src/main/res/drawable/ic_search_black_48dp.xml",
    "chars": 550,
    "preview": "<vector android:height=\"48dp\"\n    android:tint=\"#666666\"\n    android:viewportHeight=\"24.0\"\n    android:viewportWidth=\"24"
  },
  {
    "path": "app/src/main/res/drawable/loading_animation.xml",
    "chars": 220,
    "preview": "<rotate xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:drawable=\"@drawable/ic_progress\"\n    andr"
  },
  {
    "path": "app/src/main/res/drawable/progressbar_color.xml",
    "chars": 1149,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item"
  },
  {
    "path": "app/src/main/res/layout/activity_main.xml",
    "chars": 2569,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app="
  },
  {
    "path": "app/src/main/res/layout/adapter_library.xml",
    "chars": 2113,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018-present KunMinX\n  ~\n  ~ Licensed under the Apache License,"
  },
  {
    "path": "app/src/main/res/layout/adapter_play_item.xml",
    "chars": 3578,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018-present KunMinX\n  ~\n  ~ Licensed under the Apache License,"
  },
  {
    "path": "app/src/main/res/layout/fragment_drawer.xml",
    "chars": 4891,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018-present KunMinX\n  ~\n  ~ Licensed under the Apache License,"
  },
  {
    "path": "app/src/main/res/layout/fragment_login.xml",
    "chars": 5486,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app="
  },
  {
    "path": "app/src/main/res/layout/fragment_main.xml",
    "chars": 9679,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018-present KunMinX\n  ~\n  ~ Licensed under the Apache License,"
  },
  {
    "path": "app/src/main/res/layout/fragment_player.xml",
    "chars": 10897,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018-present KunMinX\n  ~\n  ~ Licensed under the Apache License,"
  },
  {
    "path": "app/src/main/res/layout/fragment_search.xml",
    "chars": 8460,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018-present KunMinX\n  ~\n  ~ Licensed under the Apache License,"
  },
  {
    "path": "app/src/main/res/layout/notify_player_big.xml",
    "chars": 6408,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018-present KunMinX\n  ~\n  ~ Licensed under the Apache License,"
  },
  {
    "path": "app/src/main/res/layout/notify_player_small.xml",
    "chars": 5353,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018-present KunMinX\n  ~\n  ~ Licensed under the Apache License,"
  },
  {
    "path": "app/src/main/res/layout-land/activity_main.xml",
    "chars": 1989,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018-present KunMinX\n  ~\n  ~ Licensed under the Apache License,"
  },
  {
    "path": "app/src/main/res/layout-land/fragment_main.xml",
    "chars": 4848,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018-present KunMinX\n  ~\n  ~ Licensed under the Apache License,"
  },
  {
    "path": "app/src/main/res/layout-land/fragment_player.xml",
    "chars": 5438,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018-present KunMinX\n  ~\n  ~ Licensed under the Apache License,"
  },
  {
    "path": "app/src/main/res/navigation/nav_drawer.xml",
    "chars": 1139,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018-present KunMinX\n  ~\n  ~ Licensed under the Apache License,"
  },
  {
    "path": "app/src/main/res/navigation/nav_main.xml",
    "chars": 2282,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018-present KunMinX\n  ~\n  ~ Licensed under the Apache License,"
  },
  {
    "path": "app/src/main/res/navigation/nav_slide.xml",
    "chars": 1138,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018-present KunMinX\n  ~\n  ~ Licensed under the Apache License,"
  },
  {
    "path": "app/src/main/res/values/attrs.xml",
    "chars": 985,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018-present KunMinX\n  ~\n  ~ Licensed under the Apache License,"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "chars": 410,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"colorPrimary\">#008577</color>\n    <color name=\"color"
  },
  {
    "path": "app/src/main/res/values/dimen.xml",
    "chars": 784,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018-present KunMinX\n  ~\n  ~ Licensed under the Apache License,"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "chars": 6825,
    "preview": "<resources>\n    <string name=\"app_name\">PureMusic</string>\n    <string name=\"app_name_debug\">PureMusic-Debug</string>\n  "
  },
  {
    "path": "app/src/main/res/values/styles.xml",
    "chars": 370,
    "preview": "<resources>\n\n    <!-- Base application theme. -->\n    <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.NoActionBar\">"
  },
  {
    "path": "app/src/main/res/xml/network_security_config.xml",
    "chars": 249,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<network-security-config xmlns:tools=\"http://schemas.android.com/tools\">\n    <bas"
  },
  {
    "path": "app/src/test/java/com/kunminx/puremusic/ExampleUnitTest.java",
    "chars": 394,
    "preview": "package com.kunminx.puremusic;\n\nimport static org.junit.Assert.assertEquals;\n\nimport org.junit.Test;\n\n/**\n * Example loc"
  },
  {
    "path": "architecture/build.gradle",
    "chars": 2285,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "architecture/src/androidTest/java/com/kunminx/architecture/ExampleInstrumentedTest.java",
    "chars": 1379,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "architecture/src/main/AndroidManifest.xml",
    "chars": 1318,
    "preview": "<!--\n  ~ Copyright 2018-present KunMinX\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you "
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/data/response/DataResult.java",
    "chars": 1587,
    "preview": "/*\n *\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/data/response/ResponseStatus.java",
    "chars": 1476,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/data/response/ResultSource.java",
    "chars": 155,
    "preview": "package com.kunminx.architecture.data.response;\n\n/**\n * Create by KunMinX at 2020/11/30\n */\npublic enum ResultSource {\n "
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/data/response/manager/NetworkStateManager.java",
    "chars": 1984,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/data/response/manager/NetworkStateReceive.java",
    "chars": 1406,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/domain/request/AsyncTask.java",
    "chars": 1290,
    "preview": "package com.kunminx.architecture.domain.request;\n\nimport android.annotation.SuppressLint;\n\nimport io.reactivex.Observabl"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/domain/request/Requester.java",
    "chars": 1224,
    "preview": "package com.kunminx.architecture.domain.request;\n\nimport androidx.lifecycle.ViewModel;\n/**\n * TODO tip 1:\n * 基于单一职责原则,抽取"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/domain/usecase/UseCase.java",
    "chars": 1193,
    "preview": "package com.kunminx.architecture.domain.usecase;\n\n/**\n * Use cases are the entry points to the domain layer.\n *\n * @para"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/domain/usecase/UseCaseHandler.java",
    "chars": 2545,
    "preview": "package com.kunminx.architecture.domain.usecase;\n\n/**\n * Runs {@link UseCase}s using a {@link UseCaseScheduler}.\n */\npub"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/domain/usecase/UseCaseScheduler.java",
    "chars": 501,
    "preview": "package com.kunminx.architecture.domain.usecase;\n\n/**\n * Interface for schedulers, see {@link UseCaseThreadPoolScheduler"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/domain/usecase/UseCaseThreadPoolScheduler.java",
    "chars": 1762,
    "preview": "package com.kunminx.architecture.domain.usecase;\n\nimport android.os.Handler;\n\nimport java.util.concurrent.Executors;\nimp"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/ui/adapter/CommonViewPagerAdapter.java",
    "chars": 1933,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/ui/bind/DrawablesBindingAdapter.java",
    "chars": 37009,
    "preview": "package com.kunminx.architecture.ui.bind;\n\nimport static java.lang.annotation.ElementType.FIELD;\nimport static java.lang"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/ui/page/BaseActivity.java",
    "chars": 3442,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/ui/page/BaseFragment.java",
    "chars": 2991,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/ui/page/StateHolder.java",
    "chars": 546,
    "preview": "package com.kunminx.architecture.ui.page;\n\nimport androidx.lifecycle.ViewModel;\n\n/**\n * Create by KunMinX at 2022/8/11\n "
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/utils/AdaptScreenUtils.java",
    "chars": 3655,
    "preview": "package com.kunminx.architecture.utils;\n\nimport android.content.res.Resources;\nimport android.util.DisplayMetrics;\nimpor"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/utils/BarUtils.java",
    "chars": 23106,
    "preview": "package com.kunminx.architecture.utils;\n\nimport static android.Manifest.permission.EXPAND_STATUS_BAR;\n\nimport android.an"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/utils/ClickUtils.java",
    "chars": 4284,
    "preview": "package com.kunminx.architecture.utils;\n\nimport android.view.View;\n\nimport androidx.annotation.IntRange;\nimport androidx"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/utils/DisplayUtils.java",
    "chars": 1823,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/utils/ImageUtils.java",
    "chars": 891,
    "preview": "package com.kunminx.architecture.utils;\n\nimport android.graphics.Bitmap;\nimport android.graphics.BitmapFactory;\n\n/**\n * "
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/utils/NetworkUtils.java",
    "chars": 1222,
    "preview": "package com.kunminx.architecture.utils;\n\nimport static android.Manifest.permission.ACCESS_NETWORK_STATE;\n\nimport android"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/utils/Res.java",
    "chars": 365,
    "preview": "package com.kunminx.architecture.utils;\n\nimport android.graphics.drawable.Drawable;\n\nimport androidx.core.content.Contex"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/utils/ScreenUtils.java",
    "chars": 8827,
    "preview": "package com.kunminx.architecture.utils;\n\nimport static android.Manifest.permission.WRITE_SETTINGS;\n\nimport android.annot"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/utils/ToastUtils.java",
    "chars": 453,
    "preview": "package com.kunminx.architecture.utils;\n\nimport android.widget.Toast;\n\n/**\n * Create by KunMinX at 2021/8/19\n */\npublic "
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/utils/Utils.java",
    "chars": 13666,
    "preview": "package com.kunminx.architecture.utils;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport and"
  },
  {
    "path": "architecture/src/main/res/values/strings.xml",
    "chars": 744,
    "preview": "<!--\n  ~ Copyright 2018-present KunMinX\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you "
  },
  {
    "path": "architecture/src/main/res/xml/file_paths.xml",
    "chars": 167,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <paths>\n        <external-path\n            name=\"camera_photos\"\n "
  },
  {
    "path": "architecture/src/test/java/com/kunminx/architecture/ExampleUnitTest.java",
    "chars": 996,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "build.gradle",
    "chars": 821,
    "preview": "buildscript {\n    ext {\n        appTargetSdk = 33\n        appMinSdk = 23\n        appVersionCode = 50500\n        appVersi"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "chars": 230,
    "preview": "#Tue May 10 12:54:34 CST 2022\ndistributionBase=GRADLE_USER_HOME\ndistributionUrl=https\\://services.gradle.org/distributio"
  },
  {
    "path": "gradle.properties",
    "chars": 234,
    "preview": "android.enableJetifier=true\nandroid.injected.testOnly=false\nandroid.useAndroidX=true\norg.gradle.caching=true\norg.gradle."
  },
  {
    "path": "gradlew",
    "chars": 5766,
    "preview": "#!/usr/bin/env sh\n\n#\n# Copyright 2015 the original author or authors.\n#\n# Licensed under the Apache License, Version 2.0"
  },
  {
    "path": "gradlew.bat",
    "chars": 2763,
    "preview": "@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 (th"
  },
  {
    "path": "settings.gradle",
    "chars": 63,
    "preview": "include ':app', ':architecture'\nrootProject.name = 'PureMusic'\n"
  }
]

// ... and 2 more files (download for full content)

About this extraction

This page contains the full source code of the KunMinX/Jetpack-MVVM-Best-Practice GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 125 files (403.4 KB), approximately 105.3k tokens, and a symbol index with 519 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!