├── .github └── workflows │ ├── android.yml │ └── releases.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── .gitignore │ └── main │ ├── AndroidManifest.xml │ ├── assets │ ├── xhook.js │ └── xposed_init │ ├── java │ └── me │ │ └── kofua │ │ └── qmhelper │ │ ├── QMPackage.kt │ │ ├── XposedInit.kt │ │ ├── data │ │ ├── StorageVolume.kt │ │ └── hookinfo.kt │ │ ├── hook │ │ ├── BaseHook.kt │ │ ├── CgiHook.kt │ │ ├── CommonAdsHook.kt │ │ ├── CopyHook.kt │ │ ├── DebugHook.kt │ │ ├── HomePageHook.kt │ │ ├── HomeTopTabHook.kt │ │ ├── JceHook.kt │ │ ├── MiscHook.kt │ │ ├── SSLHook.kt │ │ ├── SettingsHook.kt │ │ ├── SplashHook.kt │ │ ├── WebLoginHook.kt │ │ └── WebViewHook.kt │ │ ├── setting │ │ ├── Setting.kt │ │ └── SettingPack.kt │ │ └── utils │ │ ├── BannerTips.kt │ │ ├── Decryptor.kt │ │ ├── DexHelper.kt │ │ ├── Log.kt │ │ ├── api.kt │ │ ├── boolean.kt │ │ ├── file.kt │ │ ├── hash.kt │ │ ├── json.kt │ │ ├── proxy.kt │ │ ├── ui.kt │ │ ├── uimode.kt │ │ ├── utils.kt │ │ ├── view.kt │ │ ├── xposed.kt │ │ └── xposedx.kt │ ├── jni │ ├── CMakeLists.txt │ └── qmhelper.cc │ └── res │ └── values │ ├── arrays.xml │ └── strings.xml ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── renovate.json └── settings.gradle.kts /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Build CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | name: Build Ci 12 | runs-on: ubuntu-latest 13 | env: 14 | CCACHE_DIR: ${{ github.workspace }}/.ccache 15 | CCACHE_COMPILERCHECK: "%compiler% -dumpmachine; %compiler% -dumpversion" 16 | CCACHE_NOHASHDIR: true 17 | CCACHE_MAXSIZE: 1G 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | with: 23 | submodules: 'recursive' 24 | fetch-depth: 0 25 | 26 | - name: Setup JDK 17 27 | uses: actions/setup-java@v4 28 | with: 29 | distribution: 'temurin' 30 | java-version: '17' 31 | cache: 'gradle' 32 | 33 | - name: Set up ccache 34 | uses: hendrikmuhs/ccache-action@v1.2 35 | with: 36 | key: ${{ runner.os }}-${{ github.sha }} 37 | restore-keys: ${{ runner.os }} 38 | 39 | # - name: Generate signing config 40 | # run: | 41 | # key_base64="${{ secrets.SIGNING_KEY }}" 42 | # echo -n "$key_base64" | base64 -d > signing.jks 43 | # echo "releaseStoreFile=signing.jks" >> gradle.properties 44 | # echo "releaseStorePassword=android" >> gradle.properties 45 | # echo "releaseKeyAlias=androiddebugkey" >> gradle.properties 46 | # echo "releaseKeyPassword=android" >> gradle.properties 47 | 48 | - name: Build with Gradle 49 | run: | 50 | echo 'org.gradle.caching=true' >> gradle.properties 51 | echo 'org.gradle.parallel=true' >> gradle.properties 52 | echo 'org.gradle.vfs.watch=true' >> gradle.properties 53 | echo 'android.native.buildOutput=verbose' >> gradle.properties 54 | ./gradlew -PbuildWithGitSuffix=true assembleRelease assembleDebug 55 | 56 | - name: Upload built apk 57 | if: success() 58 | uses: actions/upload-artifact@v4 59 | with: 60 | name: snapshot 61 | path: | 62 | ${{ github.workspace }}/app/build/outputs/apk 63 | ${{ github.workspace }}/app/build/outputs/mapping 64 | 65 | - name: Post to channel 66 | if: github.event_name != 'pull_request' && github.ref == 'refs/heads/master' 67 | env: 68 | CHANNEL_ID: ${{ secrets.TELEGRAM_TO }} 69 | BOT_TOKEN: ${{ secrets.TELEGRAM_TOKEN }} 70 | COMMIT_MESSAGE: |+ 71 | ${{ github.event.head_commit.message }} 72 | by `${{ github.event.head_commit.author.name }}` [detail](${{ github.event.head_commit.url }}) 73 | run: | 74 | export release=$(find app/release/ -name "QMHelper-*.apk") 75 | ESCAPED=`python3 -c 'import json,os,urllib.parse; print(urllib.parse.quote(json.dumps(os.environ["COMMIT_MESSAGE"].replace(".","\.").replace("#","\\#"))))'` 76 | curl -v "https://api.telegram.org/bot${BOT_TOKEN}/sendMediaGroup?chat_id=${CHANNEL_ID}&media=%5B%7B%22type%22:%22document%22,%20%22media%22:%22attach://release%22,%22parse_mode%22:%22MarkdownV2%22,%22caption%22:${ESCAPED}%7D%5D" -F release="@$release" 77 | -------------------------------------------------------------------------------- /.github/workflows/releases.yml: -------------------------------------------------------------------------------- 1 | name: Build Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: ubuntu-latest 12 | env: 13 | CCACHE_DIR: ${{ github.workspace }}/.ccache 14 | CCACHE_COMPILERCHECK: "%compiler% -dumpmachine; %compiler% -dumpversion" 15 | CCACHE_NOHASHDIR: true 16 | CCACHE_MAXSIZE: 1G 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | with: 22 | submodules: 'recursive' 23 | fetch-depth: 0 24 | 25 | - name: Setup JDK 17 26 | uses: actions/setup-java@v4 27 | with: 28 | distribution: 'temurin' 29 | java-version: '17' 30 | cache: 'gradle' 31 | 32 | - name: Set up ccache 33 | uses: hendrikmuhs/ccache-action@v1.2 34 | with: 35 | key: ${{ runner.os }}-${{ github.sha }} 36 | restore-keys: ${{ runner.os }} 37 | 38 | # - name: Generate signing config 39 | # run: | 40 | # key_base64="${{ secrets.SIGNING_KEY }}" 41 | # echo -n "$key_base64" | base64 -d > signing.jks 42 | # echo "releaseStoreFile=signing.jks" >> gradle.properties 43 | # echo "releaseStorePassword=android" >> gradle.properties 44 | # echo "releaseKeyAlias=androiddebugkey" >> gradle.properties 45 | # echo "releaseKeyPassword=android" >> gradle.properties 46 | 47 | - name: Build with Gradle 48 | run: | 49 | echo 'org.gradle.caching=true' >> gradle.properties 50 | echo 'org.gradle.parallel=true' >> gradle.properties 51 | echo 'org.gradle.vfs.watch=true' >> gradle.properties 52 | echo 'android.native.buildOutput=verbose' >> gradle.properties 53 | ./gradlew -PbuildWithGitSuffix=true assembleRelease assembleDebug 54 | 55 | - name: Rename Apks 56 | run: | 57 | mv app/build/outputs/apk/release/app-release.apk QMHelper-v${{ github.ref_name }}.apk 58 | 59 | 60 | - name: Releases 61 | uses: softprops/action-gh-release@v2 62 | with: 63 | body: Bump Version 64 | files: | 65 | QMHelper-v${{ github.ref_name }}.apk 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | *.apk 10 | signing.properties 11 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "app/src/main/jni/dex_builder"] 2 | path = app/src/main/jni/dex_builder 3 | url = https://github.com/LSPosed/DexBuilder.git 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Q音助手 2 | 3 | 还你一个干净的Q音 Android 客户端。 4 | 5 | [![Build](https://github.com/zjns/QMHelper/workflows/Build/badge.svg)](https://github.com/zjns/QMHelper/actions) 6 | [![Channel](https://img.shields.io/badge/Follow-Telegram-blue?logo=telegram)](https://t.me/bb_show) 7 | [![Download](https://img.shields.io/github/v/release/zjns/QMHelper?color=critical&label=Download&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAF2UlEQVRIiaVWS2xTRxQ9d2bs5y+O4zgEYkgoShcESIG2EQJRREFAKmABRFCpbOiioumqrNiQCmjFiiB1Q6kqwR6k0NIKUBdFLBAt31BBoUqC8xEhgXwcO7bfezPVTJyQkBA+vdKT5fdm7rn3zL3nDilMtlt1dRiOx+G3bSjO4TIGSLkOrrtJMfYBgEoA0cKmAVKqHUR/EXBBSnmJ53JQHg9UIIDA06dY3NwMmoAgMI2NLZDAXuW6XwGogQaeahHFWIUCPiKlvgZjLVKI7wn4gdSLqYzaFC96oSJ612HsiqvUjwZsJlMKE5wvkV7vCVeIq4poEU0I/jlgKATzhMOAEADRZunx3FVEq15c/DpmwIlq80LcsYGthhnLArxe85DasMFEqT/0BAIb7oVCFy3GQFK+Bdxzk4xB2jbmSVkXFOI3WWBBdEmpKYRDNK8rGr3Iddr5vHk3TjPnsAcH4aTTsEpKwDwenQVkLodcXx9EOAzPrFlQrju+h7suyONBq8/366yBgYWW67YaSnuKi/EkGkVnWdkvOifvRDAiEGPIJJPwRqMoWbUKJISJXIMxvx+l69bBE4kg/egRSO8r7NU+NEteXbVCnBfDw+CpFPiemhpIzj8lxvZ5HGdyZoxhuK0NsdpaLG5sxNy6OqQePMBASwucTAbFK1Zg0YEDiK9ejZGuLgzcuQNvUdEkarlScBgryVhW+0godJvpKIjoWzZanZNo1FHHVq5EzdGjhkpzBsGgoU4pNUotYL4tPXIEpWvXIqMz5XzcjyoUEvd4vrOIwPyMrVZEFeqFvrGHhoyjJY2Nk4vBtk3mmr6JZ6Zt8cGD8CcSyPf3T3pPpnvUHJVOf8wcxrabs5qQmTsygv6bN1G+dSu43z9ps/D7IR3HPMLnm+yYc1Ts2oX8s2fTFS6Uz7dDuMCH42BCINvdDR4KoaqhAXO3bDHvc6kUnnZ0AJyjv70dVjhsMhzo6EDX/fsg10VxeTl8RUWILl9uisgUle6/Md9SwhVihQBRhVELzjHS3Y1AeTmqDx5EsKJifPFQMokLu3fDF4thTiyGcDxuziadTOJKQwNSnZ3YfOoUymtr4S0uNi2SevgQwfnzIXS7OM5o9SpVzj9fuvQb3Q0ymzXOlx8/bkAnWjAeR0Sf69WrCCUScHW0uuQtCyKZRM2ePajcscM41YWkqzdYWYnBlha46bQpNJOULvwxucv29qJs40b4Zs+eSj4R3tm3DyXr1yPV2mrYYEIg1daGotpaVO3fj4nirsHm19djyeHDUDq4QjIoiPegOVDbRmjBgmkPe8x0FfrmzEH28WOjMN5IBEsOHXrp+kh1tendbE/P2KsUg5SPUFAIO5OZEZAHAqbfck+eIN3aasD0mc1k4YULTTIY7fMuRkL8qXvQikTQcfnyjJu1hauqsOzYMSxrakJRzcyTS1umr8/QrRjT+nqdsWz2jEa3YjEM3LiB66dPv9JJfM0alOkp8wpLp9N42NyMoFYpzWI2e4Ypy7pMQnS4SiGeSCB58iT+aGpCX0cHpp/ZrzatP49u3cLvDQ3g/f3gWl+l7FFCXKKr9fX6z2fSsk5zIUC2ja72duRLShBMJEw1vskg1kE62SwybW3Q6htNJJB1XXhcdy9X6ie6tnOn4dj2+/9WjrNIEMHDGHLpNNLDw6as3xSQcY5wURG4ZSHrOGC53L/efL5K0yr8paWGX18+/8mAZbXpsaOVgfl8iLygo28CqgPNOQ7cYBBWMFjH9KDXzT9SWWkW6QnwJB6va6uuPq/n4v+9YuhRZ+dyqLSsbdFY7NzYZJlyL729bduWodLSZjEyQm9ziRrL0A4EEO7t3b7s3LmzBR0136ZcE0U2+zPL55cCuIa3gCxcLW5wovc452ehM9PirX9ddyqg1NNayrtcqVqu1BcA7r0OSCG4f8hxvmSOs4KA29O11bQ377HMSKkTRHRCEW0iKTcpovcBzNMyWVipdbiTue51kvICgPPm5F/GDID/AISQbRffDZUGAAAAAElFTkSuQmCC)](https://github.com/zjns/QMHelper/releases/latest) 8 | [![Star](https://img.shields.io/github/stars/zjns/QMHelper?label=Star&color=important&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAdCAYAAAC5UQwxAAADKUlEQVRIib2WX0iTURTAz737tpWEissM6c9cOUUkCqmHpYEtfAiFyl4yCoQefOmhHozAECGweskMw+hRitRQsD8ULSl0H1mUIDRr5myVpgvJQvP7f+P7Nsfc9s3dMTpw4ePee87vnHvvOeeD/y2IEEKNFOYHMom8lGHedGKWVjcloDJu7QLxRz7exTtpdRlqmurlot+KAHAqutRKsu/YeaQABgIge/e30upTR2hY6K8FEzhADfE3q9DqU0Uo+uoaQFCpQU01UmXS2UJjg+7RjCI3EHBoQFUIABFhGO0lFcmaSDpC6cuZ01p0kZcQilL21TQmayfpCMkoGkIA5TEuKlqkLL/dVWG2ONe80xggH7iXj4XPdiz5rUicKgDBZ8OC36Y+EsDggGj/1HlZ+2KJectXhSnwEaN1Ckw2n8zs8JrzTn1ftZ2bbjeb5i42gwHKkLy0QVNWwBE2hiNGIlEixopTGFjtvg0Zf4kEb+W8C1e1CCVP2XXm1/t9kAGO1NI5gajwJWBJVqEXlXrrNfNMybtzYu6RXuCBTTMOgAOW5FYOqjCIfKVGe3+baDnaC8tphC4Dq+Q4Xcg+eGllatUBGgv72kRLbXdaoBrskAvbXc2R0zE3Zix80C5Zjgeh9I0kAlb1DNufN0cv6eahOFnXYFzoPgmMUk4FE9Gwkl39EO8cuBZvOWHiK2NZj7H053C4lK0lMgDBxpdot1CptzNhEmCymKnlYrKiWiNiwg6kC+R/9uWAqGCqvEQASAIszHYWUwOx4CkNVxwaIeBAwoSdGogEb6wSClUOtWvwoe/oI1cbszBeqmdX97yR4C2KcYcL1kcpt/4O4PUcE7h1VqudplBJDDmAhU9F9EDxY3EYKGiFmZWzK11SXlOLOftgsA1t67gvT9Q0GhYeaUcJ5tDfgOS36tkFNS3iDWUUhsgbIOQ1uGXPnhtcoGej3l5u/sk6yeNoJSPgJiNAyDtwc/MvcLy98Q3MdJSQIXArY9YubqbTrgeKHnzgbr78oeQ2eQVu8VtTVbw9cRNfnL58APFzmxnbzR7do0kg4lRjNWGwZNp65Wkq+ukTAPgHIIGzcZjmG+EAAAAASUVORK5CYII=)](https://github.com/zjns/QMHelper) 9 | 10 | ## 功能 11 | 12 | - 自由复制 13 | - 自动签到 14 | - 净化开屏页 15 | - 首页顶部标签管理 16 | - 首页屏蔽直播版块 17 | - 净化红点 18 | - 净化搜索 19 | - 净化播放页 20 | - 净化我的页面 21 | - 净化评论页 22 | 23 | ## 下载 24 | 25 | [GitHub Release](https://github.com/zjns/QMHelper/releases/latest) 26 | 27 | [Xposed Repo](https://modules.lsposed.org/module/me.kofua.qmhelper) 28 | 29 | ## Licence 30 | 31 | [GNU General Public License, version 3](https://choosealicense.com/licenses/gpl-3.0/) 32 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /release 3 | .cxx 4 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | import org.gradle.configurationcache.extensions.capitalized 4 | import java.io.ByteArrayOutputStream 5 | import java.util.Properties 6 | 7 | plugins { 8 | alias(libs.plugins.agp.app) 9 | alias(libs.plugins.kotlin) 10 | alias(libs.plugins.lsplugin.cmaker) 11 | alias(libs.plugins.lsplugin.resopt) 12 | } 13 | 14 | val releaseStoreFile: String? by rootProject 15 | val releaseStorePassword: String? by rootProject 16 | val releaseKeyAlias: String? by rootProject 17 | val releaseKeyPassword: String? by rootProject 18 | 19 | val appVerName: String by rootProject 20 | val buildWithGitSuffix: String by rootProject 21 | 22 | val gitCommitCount = "git rev-list HEAD --count".execute().toInt() 23 | val gitCommitHash = "git rev-parse --verify --short HEAD".execute() 24 | 25 | fun String.execute(currentWorkingDir: File = file("./")): String { 26 | val byteOut = ByteArrayOutputStream() 27 | exec { 28 | workingDir = currentWorkingDir 29 | commandLine = split(' ') 30 | standardOutput = byteOut 31 | } 32 | return String(byteOut.toByteArray()).trim() 33 | } 34 | 35 | cmaker { 36 | default { 37 | targets("qmhelper") 38 | abiFilters("armeabi-v7a", "arm64-v8a", "x86") 39 | arguments += "-DANDROID_STL=none" 40 | cppFlags += "-Wno-c++2b-extensions" 41 | } 42 | 43 | buildTypes { 44 | arguments += "-DDEBUG_SYMBOLS_PATH=${project.buildDir.absolutePath}/symbols/${it.name}" 45 | } 46 | } 47 | 48 | android { 49 | namespace = "me.kofua.qmhelper" 50 | compileSdk = 33 51 | ndkVersion = "25.1.8937393" 52 | 53 | defaultConfig { 54 | applicationId = "me.kofua.qmhelper" 55 | minSdk = 23 56 | targetSdk = 33 57 | versionCode = gitCommitCount 58 | versionName = appVerName 59 | 60 | if (buildWithGitSuffix.toBoolean()) 61 | versionNameSuffix = ".r$gitCommitCount.$gitCommitHash" 62 | } 63 | 64 | signingConfigs { 65 | releaseStoreFile?.let { 66 | create("release") { 67 | storeFile = rootProject.file(it) 68 | storePassword = releaseStorePassword 69 | keyAlias = releaseKeyAlias 70 | keyPassword = releaseKeyPassword 71 | } 72 | } ?: run { 73 | val keystore = rootProject.file("signing.properties") 74 | .takeIf { it.isFile } ?: return@run 75 | create("release") { 76 | val prop = Properties().apply { 77 | keystore.inputStream().use(this::load) 78 | } 79 | storeFile = rootProject.file(prop.getProperty("keystore.path")) 80 | storePassword = prop.getProperty("keystore.password") 81 | keyAlias = prop.getProperty("key.alias") 82 | keyPassword = prop.getProperty("key.password") 83 | } 84 | } 85 | } 86 | 87 | buildFeatures { 88 | prefab = true 89 | buildConfig = true 90 | } 91 | 92 | buildTypes { 93 | all { 94 | signingConfig = signingConfigs.run { 95 | find { it.name == "release" } ?: find { it.name == "debug" } 96 | } 97 | } 98 | release { 99 | isMinifyEnabled = true 100 | isShrinkResources = true 101 | proguardFiles("proguard-rules.pro") 102 | } 103 | } 104 | 105 | compileOptions { 106 | sourceCompatibility = JavaVersion.VERSION_11 107 | targetCompatibility = JavaVersion.VERSION_11 108 | } 109 | 110 | kotlinOptions { 111 | jvmTarget = "11" 112 | freeCompilerArgs = listOf( 113 | "-language-version=2.0", 114 | "-Xno-param-assertions", 115 | "-Xno-call-assertions", 116 | "-Xno-receiver-assertions", 117 | "-opt-in=kotlin.RequiresOptIn", 118 | "-Xcontext-receivers", 119 | "-opt-in=kotlin.time.ExperimentalTime", 120 | "-opt-in=kotlin.contracts.ExperimentalContracts", 121 | ) 122 | } 123 | 124 | packaging { 125 | resources { 126 | excludes += "**" 127 | } 128 | } 129 | 130 | lint { 131 | checkReleaseBuilds = false 132 | } 133 | 134 | dependenciesInfo { 135 | includeInApk = false 136 | } 137 | 138 | androidResources { 139 | additionalParameters += arrayOf("--allow-reserved-package-id", "--package-id", "0x33") 140 | } 141 | 142 | externalNativeBuild { 143 | cmake { 144 | path("src/main/jni/CMakeLists.txt") 145 | version = "3.22.1+" 146 | } 147 | } 148 | } 149 | 150 | afterEvaluate { 151 | tasks.getByPath("installDebug").finalizedBy(restartHost) 152 | android.applicationVariants.forEach { variant -> 153 | if (variant.name != "release") return@forEach 154 | val variantCapped = variant.name.capitalized() 155 | val packageTask = tasks["package$variantCapped"] 156 | 157 | task("sync${variantCapped}Apk") { 158 | into(variant.name) 159 | from(packageTask.outputs) { 160 | include("*.apk") 161 | rename(".*\\.apk", "QMHelper-v${variant.versionName}.apk") 162 | } 163 | }.let { packageTask.finalizedBy(it) } 164 | } 165 | } 166 | 167 | configurations.all { 168 | exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk7") 169 | exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk8") 170 | } 171 | 172 | dependencies { 173 | compileOnly(libs.xposed) 174 | implementation(libs.kotlin.stdlib) 175 | implementation(libs.kotlin.coroutines.android) 176 | implementation(libs.kotlin.coroutines.jdk) 177 | implementation(libs.androidx.annotation) 178 | implementation(libs.cxx) 179 | implementation(libs.dexmaker) 180 | } 181 | 182 | val restartHost: Task = task("restartHost").doLast { 183 | val adb: String = androidComponents.sdkComponents.adb.get().asFile.absolutePath 184 | exec { 185 | commandLine(adb, "shell", "am", "force-stop", "com.tencent.qqmusic") 186 | } 187 | exec { 188 | Thread.sleep(2000) 189 | commandLine( 190 | adb, "shell", "am", "start", 191 | "$(pm resolve-activity --components com.tencent.qqmusic)" 192 | ) 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -repackageclasses "qmhelper" 2 | 3 | -keep class me.kofua.qmhelper.XposedInit { 4 | (); 5 | } 6 | 7 | -keepclasseswithmembers class me.kofua.qmhelper.utils.DexHelper { 8 | native ; 9 | long token; 10 | java.lang.ClassLoader classLoader; 11 | } 12 | 13 | -keepattributes RuntimeVisible*Annotations 14 | 15 | -keepclassmembers class * { 16 | @android.webkit.JavascriptInterface ; 17 | } 18 | 19 | -keepclassmembers class * implements android.os.Parcelable { 20 | public static final ** CREATOR; 21 | } 22 | 23 | -keepclassmembernames class me.kofua.qmhelper.data.* implements java.io.Serializable { 24 | ; 25 | } 26 | 27 | #-keepclassmembers class me.kofua.qmhelper.MainActivity$Companion { 28 | # boolean isModuleActive(); 29 | #} 30 | 31 | -assumenosideeffects class kotlin.jvm.internal.Intrinsics { 32 | public static void check*(...); 33 | public static void throw*(...); 34 | } 35 | 36 | -assumenosideeffects class java.util.Objects { 37 | public static ** requireNonNull(...); 38 | } 39 | 40 | -allowaccessmodification 41 | -overloadaggressively 42 | -------------------------------------------------------------------------------- /app/src/.gitignore: -------------------------------------------------------------------------------- 1 | generated 2 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 19 | 20 | 23 | 26 | 29 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/assets/xhook.js: -------------------------------------------------------------------------------- 1 | (function(a,b){var c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H=[].indexOf||function(a){for(var b=0,c=this.length;b=0||(f=f===b?d[a].length:f,d[a].splice(f,0,c))},c[m]=function(a,c){var f;if(a===b)return void(d={});c===b&&(d[a]=[]),f=e(a).indexOf(c),f!==-1&&e(a).splice(f,1)},c[h]=function(){var b,d,f,g,h,i,j,k;for(b=D(arguments),d=b.shift(),a||(b[0]=z(b[0],y(d))),g=c["on"+d],g&&g.apply(c,b),k=e(d).concat(e("*")),f=i=0,j=k.length;i2)throw"invalid hook";return F[n](d,a,b)},F[c]=function(a,b){if(a.length<2||a.length>3)throw"invalid hook";return F[n](c,a,b)},F.enable=function(){q[u]=t,"function"==typeof r&&(q[g]=r),k&&(q[i]=s)},F.disable=function(){q[u]=F[u],q[g]=F[g],k&&(q[i]=k)},v=F.headers=function(a,b){var c,d,e,f,g,h,i,j,k;switch(null==b&&(b={}),typeof a){case"object":d=[];for(e in a)g=a[e],f=e.toLowerCase(),d.push(f+":\t"+g);return d.join("\n")+"\n";case"string":for(d=a.split("\n"),i=0,j=d.length;ib&&b<4;)k[o]=++b,1===b&&k[h]("loadstart",{}),2===b&&G(),4===b&&(G(),E()),k[h]("readystatechange",{}),4===b&&(t.async===!1?g():setTimeout(g,0))},g=function(){l||k[h]("load",{}),k[h]("loadend",{}),l&&(k[o]=0)},b=0,x=function(a){var b,d;if(4!==a)return void i(a);b=F.listeners(c),(d=function(){var a;return b.length?(a=b.shift(),2===a.length?(a(t,w),d()):3===a.length&&t.async?a(t,w,d):d()):i(4)})()},k=t.xhr=f(),I.onreadystatechange=function(a){try{2===I[o]&&r()}catch(a){}4===I[o]&&(D=!1,r(),q()),x(I[o])},m=function(){l=!0},k[n]("error",m),k[n]("timeout",m),k[n]("abort",m),k[n]("progress",function(){b<3?x(3):k[h]("readystatechange",{})}),("withCredentials"in I||F.addWithCredentials)&&(k.withCredentials=!1),k.status=0,L=e.concat(p);for(J=0,K=L.length;J { 34 | if (shouldSaveLog) startLog() 35 | currentContext.addModuleAssets() 36 | 37 | Log.d("QQMusic process launched ...") 38 | Log.d("QMHelper version: ${BuildConfig.VERSION_NAME}(${BuildConfig.VERSION_CODE}) from $modulePath${if (isBuiltIn) " (BuiltIn)" else ""}") 39 | Log.d("QQMusic version: ${getPackageVersion(lpparam.packageName)} (${if (is64) "64" else "32"}bit)") 40 | Log.d("SDK: ${Build.VERSION.RELEASE}(${Build.VERSION.SDK_INT}); Phone: ${Build.BRAND} ${Build.MODEL}") 41 | Log.d("Config: ${sPrefs.all}") 42 | 43 | preloadProxyClasses() 44 | val debugHooks = if (BuildConfig.DEBUG) { 45 | listOf(SSLHook, DebugHook) 46 | } else listOf() 47 | val buildInHooks = if (isBuiltIn) { 48 | listOf(WebLoginHook) 49 | } else listOf() 50 | val normalHooks = listOf( 51 | SettingsHook, 52 | SplashHook, 53 | HomeTopTabHook, 54 | HomePageHook, 55 | CgiHook, 56 | CopyHook, 57 | MiscHook, 58 | CommonAdsHook, 59 | WebViewHook, 60 | JceHook, 61 | ) 62 | val allHooks = buildList { 63 | addAll(debugHooks) 64 | addAll(buildInHooks) 65 | addAll(normalHooks) 66 | } 67 | startHook(allHooks) 68 | } 69 | } 70 | } 71 | } 72 | 73 | private fun startHook(hooks: List) { 74 | hooks.forEach { 75 | try { 76 | it.hook() 77 | } catch (t: Throwable) { 78 | Log.e(t) 79 | val errorMessage = t.message ?: "" 80 | BannerTips.error(string(R.string.hook_error, errorMessage)) 81 | } 82 | } 83 | } 84 | 85 | private fun startLog() = try { 86 | if (logFile.exists()) { 87 | if (oldLogFile.exists()) 88 | oldLogFile.delete() 89 | logFile.renameTo(oldLogFile) 90 | } 91 | logFile.delete() 92 | logFile.createNewFile() 93 | Runtime.getRuntime().exec(arrayOf("logcat", "-T", "100", "-f", logFile.absolutePath)) 94 | } catch (e: Throwable) { 95 | Log.e(e) 96 | null 97 | } 98 | 99 | private fun disableTinker() { 100 | "com.tencent.tinker.loader.app.TinkerApplication".from(classLoader) 101 | ?.hookBeforeAllConstructors { it.args[0] = 0 } 102 | } 103 | 104 | companion object { 105 | lateinit var modulePath: String 106 | lateinit var moduleRes: Resources 107 | lateinit var classLoader: ClassLoader 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /app/src/main/java/me/kofua/qmhelper/data/StorageVolume.kt: -------------------------------------------------------------------------------- 1 | package me.kofua.qmhelper.data 2 | 3 | import me.kofua.qmhelper.qmPackage 4 | import me.kofua.qmhelper.utils.new 5 | 6 | data class StorageVolume( 7 | val storageId: Int, 8 | val path: String, 9 | val descriptionId: Int, 10 | val primary: Boolean, 11 | val removable: Boolean, 12 | val emulated: Boolean, 13 | val state: String 14 | ) { 15 | companion object { 16 | private val volumeRegex = 17 | Regex("""^StorageVolume\s\[mStorageId=(\d+),\smPath=(.*),\smDescriptionId=(\d+),\smPrimary=(true|false),\smRemovable=(true|false),\smEmulated=(true|false),\smState=(.*)]$""") 18 | 19 | fun Any.toMockVolume(): StorageVolume? { 20 | return volumeRegex.matchEntire(toString())?.run { 21 | groupValues.let { 22 | StorageVolume( 23 | it[1].toInt(), 24 | it[2], 25 | it[3].toInt(), 26 | it[4].toBoolean(), 27 | it[5].toBoolean(), 28 | it[6].toBoolean(), 29 | it[7] 30 | ) 31 | } 32 | } 33 | } 34 | 35 | fun StorageVolume.toRealVolume(): Any? { 36 | return qmPackage.storageVolumeClass?.new(path, primary, removable, state) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/me/kofua/qmhelper/data/hookinfo.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UNCHECKED_CAST") 2 | 3 | package me.kofua.qmhelper.data 4 | 5 | import java.io.ObjectInput 6 | import java.io.Serializable 7 | 8 | fun ObjectInput.readAny() = readObject() as T 9 | 10 | fun clazz(action: Class.() -> Unit) = Class().apply(action) 11 | fun method(action: Method.() -> Unit) = Method().apply(action) 12 | fun field(action: Field.() -> Unit) = Field().apply(action) 13 | 14 | val defClazz = clazz { } 15 | val defMethod = method { } 16 | val defField = field { } 17 | 18 | sealed class Element(var name: String = "") : Serializable 19 | class Class : Element() 20 | class Field : Element() 21 | class Method(var paramTypes: Array = arrayOf()) : Element() 22 | 23 | sealed class ClassInfo(var clazz: Class = defClazz) : Serializable 24 | 25 | class BaseFragment(var resume: Method = defMethod) : ClassInfo() 26 | 27 | class HomePageFragment(var initTabFragment: Method = defMethod) : ClassInfo() 28 | 29 | class MainDesktopHeader : ClassInfo() { 30 | var addTabById: Method = defMethod 31 | var addTabByName: Method = defMethod 32 | var showMusicWorld: Method = defMethod 33 | } 34 | 35 | class AdManager(var get: Method = defMethod) : ClassInfo() 36 | 37 | class Setting : ClassInfo() { 38 | var with: Method = defMethod 39 | var type: Field = defField 40 | var title: Field = defField 41 | var rightDesc: Field = defField 42 | var redDotListener: Field = defField 43 | var builder: SettingBuilder = SettingBuilder() 44 | var switchListener: SwitchListener = SwitchListener() 45 | var baseSettingFragment: BaseSettingFragment = BaseSettingFragment() 46 | var baseSettingPack: BaseSettingPack = BaseSettingPack() 47 | var baseSettingProvider: BaseSettingProvider = BaseSettingProvider() 48 | var drawerSettingPack: DrawerSettingPack = DrawerSettingPack() 49 | } 50 | 51 | class SettingBuilder : ClassInfo() { 52 | var build: Method = defMethod 53 | var type: Field = defField 54 | var title: Field = defField 55 | var rightDesc: Field = defField 56 | var summary: Field = defField 57 | var switchListener: Field = defField 58 | var clickListener: Field = defField 59 | } 60 | 61 | class SwitchListener : ClassInfo() { 62 | var isSwitchOn: Method = defMethod 63 | var onSwitchStatusChange: Method = defMethod 64 | } 65 | 66 | class BaseSettingFragment : ClassInfo() { 67 | var settingPackage: Method = defMethod 68 | var title: Method = defMethod 69 | } 70 | 71 | class BaseSettingPack : ClassInfo() { 72 | var createSettingProvider: Method = defMethod 73 | var host: Field = defField 74 | } 75 | 76 | class BaseSettingProvider : ClassInfo() { 77 | var create: Method = defMethod 78 | var getSetting: Method = defMethod 79 | } 80 | 81 | class DrawerSettingPack : ClassInfo() { 82 | var createSettingProvider: Method = defMethod 83 | var initKolEnter: Method = defMethod 84 | } 85 | 86 | class PersonalEntryView : ClassInfo() { 87 | var update: Method = defMethod 88 | var rightDescView: Field = defField 89 | var redDotView: Field = defField 90 | } 91 | 92 | class BannerTips(var showStyledToast: Method = defMethod) : ClassInfo() 93 | 94 | class SettingFragment : ClassInfo() { 95 | var resume: Method = defMethod 96 | var settingList: Field = defField 97 | } 98 | 99 | class UserInfoHolder(var showBubble: Method = defMethod) : ClassInfo() 100 | 101 | class StrategyModule(var getStrategyId: Method = defMethod) : ClassInfo() 102 | 103 | class TopAreaDelegate : ClassInfo() { 104 | var initLiveGuide: Method = defMethod 105 | var showCurListen: Method = defMethod 106 | var showShareGuide: Method = defMethod 107 | } 108 | 109 | class PlayerViewModel : ClassInfo() { 110 | var postCanSlide: Method = defMethod 111 | var setCanSlide: Method = defMethod 112 | } 113 | 114 | class SpManager(var get: Method = defMethod) : ClassInfo() 115 | 116 | class AppStarterActivity : ClassInfo() { 117 | var doOnCreate: Method = defMethod 118 | var addSecondFragment: Method = defMethod 119 | var showMessageDialog: Method = defMethod 120 | } 121 | 122 | class AuthAgent(var startActionActivity: Method = defMethod) : ClassInfo() 123 | 124 | class JsonRespParser(var parseModuleItem: Method = defMethod) : ClassInfo() 125 | 126 | class Gson : ClassInfo() { 127 | var fromJson: Method = defMethod 128 | var toJson: Method = defMethod 129 | var jsonObject: Class = defClazz 130 | } 131 | 132 | class UiModeManager(var isThemeForbid: Method = defMethod) : ClassInfo() 133 | 134 | class ApkDownloadAdBar(var methods: List = listOf()) : ClassInfo() 135 | 136 | class AudioStreamEKeyManager : ClassInfo() { 137 | var instance: Field = defField 138 | var getFileEKey: Method = defMethod 139 | } 140 | 141 | class EKeyDecryptor : ClassInfo() { 142 | var instance: Field = defField 143 | var decryptFile: Method = defMethod 144 | } 145 | 146 | class VipDownloadHelper(var decryptFile: Method = defMethod) : ClassInfo() 147 | 148 | class BottomTipController(var updateBottomTips: Method = defMethod) : ClassInfo() 149 | 150 | class VideoViewDelegate(var onResult: Method = defMethod) : ClassInfo() 151 | 152 | class GenreViewDelegate(var onBind: Method = defMethod) : ClassInfo() 153 | 154 | class UserGuideViewDelegate(var showUserGuide: Method = defMethod) : ClassInfo() 155 | 156 | class TopSongViewDelegate(var onBind: Method = defMethod) : ClassInfo() 157 | 158 | class DataPlugin : ClassInfo() { 159 | var handleJsRequest: Method = defMethod 160 | var runtime: Field = defField 161 | var activity: Method = defMethod 162 | } 163 | 164 | class AlbumIntroViewHolder : ClassInfo() { 165 | var onHolderCreated: Method = defMethod 166 | var tvAlbumDetail: Field = defField 167 | var lastTextContent: Field = defField 168 | } 169 | 170 | class AlbumTagViewHolder(var onHolderCreated: Method = defMethod) : ClassInfo() 171 | 172 | class SettingView : ClassInfo() { 173 | var setSetting: Method = defMethod 174 | var setLastClickTime: Method = defMethod 175 | } 176 | 177 | class FileUtils(var toValidFilename: Method = defMethod) : ClassInfo() 178 | 179 | class StorageUtils(var getVolumes: Method = defMethod) : ClassInfo() 180 | 181 | class SkinManager(var getSkinId: Method = defMethod) : ClassInfo() 182 | 183 | class AdResponseDataItem(var getAds: List = listOf()) : ClassInfo() 184 | 185 | class AdResponseData(var item: List = listOf()) : Serializable 186 | 187 | class JceResponseConverter(var parse: Method = defMethod) : ClassInfo() 188 | 189 | class WebRequestHeaders : ClassInfo() { 190 | var instance: Field = defField 191 | var getCookies: Method = defMethod 192 | var getUA: Method = defMethod 193 | } 194 | 195 | class UserManager : ClassInfo() { 196 | var get: Method = defMethod 197 | var getMusicUin: Method = defMethod 198 | var isLogin: Method = defMethod 199 | } 200 | 201 | class BannerManager(var requestAd: Method = defMethod) : ClassInfo() 202 | 203 | class MusicWorldPullEntrance(var showButton: Method = defMethod) : ClassInfo() 204 | 205 | class HookInfo : Serializable { 206 | var lastUpdateTime: Long = 0L 207 | var clientVersionCode: Int = 0 208 | var moduleVersionCode: Int = 0 209 | var moduleVersionName: String = "" 210 | var baseFragment: BaseFragment = BaseFragment() 211 | var splashAdapter: Class = defClazz 212 | var homePageFragment: HomePageFragment = HomePageFragment() 213 | var mainDesktopHeader: MainDesktopHeader = MainDesktopHeader() 214 | var adManager: AdManager = AdManager() 215 | var setting: Setting = Setting() 216 | var personalEntryView: PersonalEntryView = PersonalEntryView() 217 | var bannerTips: BannerTips = BannerTips() 218 | var settingFragment: SettingFragment = SettingFragment() 219 | var userInfoHolder: UserInfoHolder = UserInfoHolder() 220 | var strategyModule: StrategyModule = StrategyModule() 221 | var topAreaDelegate: TopAreaDelegate = TopAreaDelegate() 222 | var playViewModel: PlayerViewModel = PlayerViewModel() 223 | var spManager: SpManager = SpManager() 224 | var appStarterActivity: AppStarterActivity = AppStarterActivity() 225 | var modeFragment: Class = defClazz 226 | var authAgent: AuthAgent = AuthAgent() 227 | var jsonRespParser: JsonRespParser = JsonRespParser() 228 | var gson: Gson = Gson() 229 | var uiModeManager: UiModeManager = UiModeManager() 230 | var adBar: ApkDownloadAdBar = ApkDownloadAdBar() 231 | var musicWorldTouchListener: Class = defClazz 232 | var eKeyManager: AudioStreamEKeyManager = AudioStreamEKeyManager() 233 | var eKeyDecryptor: EKeyDecryptor = EKeyDecryptor() 234 | var vipDownloadHelper: VipDownloadHelper = VipDownloadHelper() 235 | var bottomTipController: BottomTipController = BottomTipController() 236 | var videoViewDelegate: VideoViewDelegate = VideoViewDelegate() 237 | var genreViewDelegate: GenreViewDelegate = GenreViewDelegate() 238 | var userGuideViewDelegate: UserGuideViewDelegate = UserGuideViewDelegate() 239 | var topSongViewDelegate: TopSongViewDelegate = TopSongViewDelegate() 240 | var dataPlugin: DataPlugin = DataPlugin() 241 | var albumIntroViewHolder: AlbumIntroViewHolder = AlbumIntroViewHolder() 242 | var albumTagViewHolder: AlbumTagViewHolder = AlbumTagViewHolder() 243 | var settingView: SettingView = SettingView() 244 | var fileUtils: FileUtils = FileUtils() 245 | var storageVolume: Class = defClazz 246 | var storageUtils: StorageUtils = StorageUtils() 247 | var vipAdBarData: Class = defClazz 248 | var skinManager: SkinManager = SkinManager() 249 | var adResponseData: AdResponseData = AdResponseData() 250 | var jceRespConverter: JceResponseConverter = JceResponseConverter() 251 | var webRequestHeaders: WebRequestHeaders = WebRequestHeaders() 252 | var userManager: UserManager = UserManager() 253 | var bannerManager: BannerManager = BannerManager() 254 | var musicWorldPullEntrance: MusicWorldPullEntrance = MusicWorldPullEntrance() 255 | } 256 | -------------------------------------------------------------------------------- /app/src/main/java/me/kofua/qmhelper/hook/BaseHook.kt: -------------------------------------------------------------------------------- 1 | package me.kofua.qmhelper.hook 2 | 3 | import me.kofua.qmhelper.XposedInit 4 | 5 | interface BaseHook { 6 | val classLoader: ClassLoader 7 | get() = XposedInit.classLoader 8 | 9 | fun hook() 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/me/kofua/qmhelper/hook/CgiHook.kt: -------------------------------------------------------------------------------- 1 | package me.kofua.qmhelper.hook 2 | 3 | import me.kofua.qmhelper.BuildConfig 4 | import me.kofua.qmhelper.hookInfo 5 | import me.kofua.qmhelper.qmPackage 6 | import me.kofua.qmhelper.utils.* 7 | import org.json.JSONArray 8 | import org.json.JSONObject 9 | 10 | object CgiHook : BaseHook { 11 | override fun hook() { 12 | val hidden = sPrefs.getBoolean("hidden", false) 13 | val blockLive = sPrefs.getBoolean("block_live", false) 14 | val purifySearch = sPrefs.getStringSet("purify_search", null) ?: setOf() 15 | val blockCommentBanners = sPrefs.getBoolean("block_comment_banners", false) 16 | val removeCommentRecommend = sPrefs.getBoolean("remove_comment_recommend", false) 17 | val removeMineKol = sPrefs.getBoolean("remove_mine_kol", false) 18 | val moveDownRecently = sPrefs.getBoolean("move_down_recently", false) 19 | val unlockTheme = sPrefs.getBoolean("unlock_theme", false) 20 | val unlockFont = sPrefs.getBoolean("unlock_font", false) 21 | val unlockLyricKinetic = sPrefs.getBoolean("unlock_lyric_kinetic", false) 22 | val blockCommonAds = sPrefs.getBoolean("block_common_ads", false) 23 | val purifyRedDots = sPrefs.getBoolean("purify_red_dots", false) 24 | val hideSongListGuide = sPrefs.getBoolean("hide_song_list_guide", false) 25 | 26 | hookInfo.jsonRespParser.hookBeforeMethod({ parseModuleItem }) out@{ param -> 27 | val path = param.args[1] as? String 28 | if (BuildConfig.DEBUG && path != "traceid") { 29 | val json = param.args[2]?.toString() ?: return@out 30 | Log.d("net.cgi, path: $path, json: $json") 31 | } 32 | if (path == "music.recommend.RecommendFeed.get_recommend_feed" && blockLive) { 33 | val json = param.args[2]?.toString() ?: return@out 34 | val jo = json.runCatchingOrNull { toJSONObject() } ?: return@out 35 | val shelfList = jo.optJSONObject(path)?.optJSONObject("data") 36 | ?.optJSONArray("v_shelf") ?: return@out 37 | var position = -1 38 | for ((idx, item) in shelfList.asSequence().withIndex()) { 39 | if (item.optString("title_content").contains("直播")) { 40 | position = idx 41 | break 42 | } 43 | } 44 | if (position != -1) { 45 | shelfList.remove(position) 46 | param.args[2] = jo.toString().fromJson(qmPackage.jsonObjectClass) ?: return@out 47 | } 48 | } else if (path == "music.musicsearch.HotkeyService.GetHotkeyAllBusinessForQQMusicMobile" 49 | && purifySearch.any { it != "scroll" } 50 | ) { 51 | val json = param.args[2]?.toString() ?: return@out 52 | val jo = json.runCatchingOrNull { toJSONObject() } ?: return@out 53 | val data = jo.optJSONObject(path)?.optJSONObject("data") ?: return@out 54 | if (purifySearch.contains("ads")) 55 | data.remove("ads") 56 | if (purifySearch.contains("rcmd")) 57 | data.remove("vec_reckey") 58 | if (purifySearch.contains("rcmd-list")) { 59 | data.remove("business_en_cn") 60 | data.remove("map_business_hotkey") 61 | } 62 | param.args[2] = jo.toString().fromJson(qmPackage.jsonObjectClass) ?: return@out 63 | } else if (path == "music.defaultKey.DefaultKeyService.GetDefaultKeyForQQMusicMobile" 64 | && purifySearch.contains("scroll") 65 | ) { 66 | val json = param.args[2]?.toString() ?: return@out 67 | val jo = json.runCatchingOrNull { toJSONObject() } ?: return@out 68 | val data = jo.optJSONObject(path)?.optJSONObject("data") ?: return@out 69 | data.remove("keys") 70 | data.remove("map_business_keys") 71 | param.args[2] = jo.toString().fromJson(qmPackage.jsonObjectClass) ?: return@out 72 | } else if (path == "notice" && blockCommentBanners) { 73 | val json = param.args[2]?.toString() ?: return@out 74 | val jo = json.runCatchingOrNull { toJSONObject() } ?: return@out 75 | val data = jo.optJSONObject(path)?.optJSONObject("data") ?: return@out 76 | if (data.optJSONArray("Banners").isNotEmpty()) { 77 | data.remove("Banners") 78 | param.args[2] = jo.toString().fromJson(qmPackage.jsonObjectClass) ?: return@out 79 | } 80 | } else if (path == "recommend" && removeCommentRecommend) { 81 | val json = param.args[2]?.toString() ?: return@out 82 | val jo = json.runCatchingOrNull { toJSONObject() } ?: return@out 83 | val data = jo.optJSONObject(path)?.optJSONObject("data") ?: return@out 84 | if (data.optJSONArray("RecItems").isNotEmpty()) { 85 | data.remove("RecItems") 86 | param.args[2] = jo.toString().fromJson(qmPackage.jsonObjectClass) ?: return@out 87 | } 88 | } else if ((path == "music.sociality.KolEntrance.GetKolEntrance" 89 | || path == "music.sociality.KolEntrance.GetNewKolEntrance" 90 | || path == "music.sociality.KolEntrance.GetFlashMsg") && removeMineKol 91 | ) { 92 | val json = param.args[2]?.toString() ?: return@out 93 | val jo = json.runCatchingOrNull { toJSONObject() } ?: return@out 94 | val data = jo.optJSONObject(path)?.optJSONObject("data") ?: return@out 95 | if (data.optBoolean("ShowEntrance") || data.optBoolean("ShowFlashMsg")) { 96 | data.put("ShowEntrance", false) // for GetKolEntrance 97 | data.put("ShowFlashMsg", false) // for GetFlashMsg 98 | param.args[2] = jo.toString().fromJson(qmPackage.jsonObjectClass) ?: return@out 99 | } 100 | } else if (path == "music.individuation.Recommend.GetRecommend" && moveDownRecently) { 101 | val json = param.args[2]?.toString() ?: return@out 102 | val jo = json.runCatchingOrNull { toJSONObject() } ?: return@out 103 | val data = jo.optJSONObject(path)?.optJSONObject("data") ?: return@out 104 | val shelfList = data.optJSONArray("v_shelf") ?: return@out 105 | var recentJson: JSONObject? = null 106 | val newShelf = JSONArray() 107 | val songListTitle = shelfList.asSequence() 108 | .map { it.optString("title_content") }.toList() 109 | .let { tl -> tl.find { it == "收藏歌单" } ?: tl.find { it == "自建歌单" } } 110 | ?: return@out 111 | for (item in shelfList) { 112 | val title = item.optString("title_content") 113 | if (title != "最近播放" && title != songListTitle) { 114 | newShelf.put(item) 115 | } else if (title == "最近播放") { 116 | recentJson = item 117 | } else { 118 | newShelf.put(item) 119 | recentJson?.also { 120 | newShelf.put(it) 121 | } ?: return@out 122 | } 123 | } 124 | data.put("v_shelf", newShelf) 125 | param.args[2] = jo.toString().fromJson(qmPackage.jsonObjectClass) ?: return@out 126 | } else if (path == "Personal.PersonalCenterV2.get_subject_info" && hidden && unlockTheme) { 127 | val json = param.args[2]?.toString() ?: return@out 128 | val jo = json.runCatchingOrNull { toJSONObject() } ?: return@out 129 | val data = jo.optJSONObject(path)?.optJSONObject("data") ?: return@out 130 | val themeList = data.optJSONArray("vlist") ?: return@out 131 | data.optJSONObject("alert")?.put("revertTheme", 0) 132 | for (item in themeList) 133 | item.put("enable", 1) 134 | param.args[2] = jo.toString().fromJson(qmPackage.jsonObjectClass) ?: return@out 135 | } else if (path == "music.lyricsPoster.PicturePoster.getFont" && hidden && unlockFont) { 136 | val json = param.args[2]?.toString() ?: return@out 137 | val jo = json.runCatchingOrNull { toJSONObject() } ?: return@out 138 | val data = jo.optJSONObject(path)?.optJSONObject("data") ?: return@out 139 | for (font in data.optJSONArray("fontList").orEmpty()) 140 | font.put("enable", 1) 141 | param.args[2] = jo.toString().fromJson(qmPackage.jsonObjectClass) ?: return@out 142 | } else if (path == "musictv.openapi.LyricSvr.GetKineticLyricCategory" && hidden && unlockLyricKinetic) { 143 | val json = param.args[2]?.toString() ?: return@out 144 | val jo = json.runCatchingOrNull { toJSONObject() } ?: return@out 145 | val data = jo.optJSONObject(path)?.optJSONObject("data") ?: return@out 146 | for (tab in data.optJSONArray("tabs").orEmpty()) 147 | for (template in tab.optJSONArray("templates").orEmpty()) 148 | template.put("vip_needed", "0") 149 | param.args[2] = jo.toString().fromJson(qmPackage.jsonObjectClass) ?: return@out 150 | } else if (path == "Advert.SdkAdvertServer.ProcessRequest" && blockCommonAds) { 151 | val json = param.args[2]?.toString() ?: return@out 152 | val jo = json.runCatchingOrNull { toJSONObject() } ?: return@out 153 | val data = jo.optJSONObject(path)?.optJSONObject("data") ?: return@out 154 | // see com.tencent.qqmusic.business.ad.naming.SdkAdId 155 | // set to 0 for LockScreenLiveController, 10602: AD_ID_PLAYER_LIVE_INFO 156 | if (data.optInt("musicadtype") == 10602) 157 | data.put("musicadtype", 0) 158 | data.optJSONObject("data")?.run { 159 | put("ad_list", JSONArray()) 160 | put("maxreqtimes", 0) 161 | put("maxshowtimes", 0) 162 | } 163 | param.args[2] = jo.toString().fromJson(qmPackage.jsonObjectClass) ?: return@out 164 | } else if ((path == "VipCenter.MyVipRed.get_vip_reddot" 165 | || path == "integral.task_mall_read.get_task_entry_configure" 166 | || path == "music.vip.DressUpConfigSvr.GetDressUpShow4Mine") 167 | && purifyRedDots 168 | ) { 169 | val json = param.args[2]?.toString() ?: return@out 170 | val jo = json.runCatchingOrNull { toJSONObject() } ?: return@out 171 | val data = jo.optJSONObject(path)?.optJSONObject("data") ?: return@out 172 | data.put("reddot", 0) // for get_vip_reddot 173 | data.put("iconurl_1", "") 174 | data.put("iconurl_2", "") 175 | data.put("red_dot_status", 0) // for get_task_entry_configure 176 | data.put("logo", "") 177 | data.put("light_logo", "") 178 | data.optJSONObject("config")?.run { // for GetDressUpShow4Mine 179 | put("redDot", 0) 180 | put("icon", "") 181 | } 182 | param.args[2] = jo.toString().fromJson(qmPackage.jsonObjectClass) ?: return@out 183 | } else if (path == "abtest.ClientStrategyServer.get_strategy" && hideSongListGuide) { 184 | val json = param.args[2]?.toString() ?: return@out 185 | val jo = json.runCatchingOrNull { toJSONObject() } ?: return@out 186 | val data = jo.optJSONObject(path)?.optJSONObject("data") ?: return@out 187 | data.optJSONObject("m_strategy")?.optJSONObject("MyTAB") 188 | ?.optJSONObject("miscellany")?.put("page", "Newpage2") 189 | param.args[2] = jo.toString().fromJson(qmPackage.jsonObjectClass) ?: return@out 190 | } 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /app/src/main/java/me/kofua/qmhelper/hook/CommonAdsHook.kt: -------------------------------------------------------------------------------- 1 | package me.kofua.qmhelper.hook 2 | 3 | import me.kofua.qmhelper.from 4 | import me.kofua.qmhelper.hookInfo 5 | import me.kofua.qmhelper.utils.* 6 | 7 | object CommonAdsHook : BaseHook { 8 | override fun hook() { 9 | if (!sPrefs.getBoolean("block_common_ads", false)) return 10 | 11 | hookInfo.adResponseData.item.forEach { item -> 12 | item.getAds.forEach { m -> 13 | item.clazz.from(classLoader)?.hookAfterMethod(m.name) { param -> 14 | val ads = param.result as? MutableList<*> 15 | ?: return@hookAfterMethod 16 | for (i in ads.size - 1 downTo 0) { 17 | val showAdMark = ads[i]?.getObjectField("creative") 18 | ?.getObjectField("option") 19 | ?.getObjectFieldAs("isShowAdMark") ?: false 20 | val adTag = ads[i]?.getObjectField("madAdInfo") 21 | ?.getObjectFieldAs("adTag") == "广告" 22 | if (showAdMark || adTag) 23 | ads.removeAt(i) 24 | } 25 | } 26 | } 27 | } 28 | hookInfo.bannerManager.replaceMethod({ requestAd }) { null } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/me/kofua/qmhelper/hook/CopyHook.kt: -------------------------------------------------------------------------------- 1 | package me.kofua.qmhelper.hook 2 | 3 | import android.app.Activity 4 | import android.app.AlertDialog 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.util.AttributeSet 8 | import android.view.View 9 | import android.widget.TextView 10 | import me.kofua.qmhelper.R 11 | import me.kofua.qmhelper.hookInfo 12 | import me.kofua.qmhelper.utils.* 13 | 14 | typealias OnCopyAllListener = (text: CharSequence) -> Unit 15 | 16 | object CopyHook : BaseHook { 17 | private var forceInterceptTouchField = "" 18 | 19 | private val expandableTextViewClass by Weak { "com.tencent.expandabletextview.ExpandableTextView" from classLoader } 20 | 21 | override fun hook() { 22 | if (!sPrefs.getBoolean("copy_enhance", false)) return 23 | 24 | @Suppress("UNCHECKED_CAST") 25 | hookInfo.dataPlugin.hookBeforeMethod({ handleJsRequest }) { param -> 26 | val dataPlugin = param.thisObject 27 | val method = param.args[2] as String 28 | val params = param.args[3] as Array 29 | if (method == "setClipboard") { 30 | val text = params.firstOrNull()?.runCatchingOrNull { toJSONObject() } 31 | ?.optString("text")?.replace("\\n", "\n") 32 | ?: return@hookBeforeMethod 33 | val activity = dataPlugin.getObjectField( 34 | hookInfo.dataPlugin.runtime 35 | )?.callMethodAs( 36 | hookInfo.dataPlugin.activity 37 | ) ?: return@hookBeforeMethod 38 | showCopyDialog(activity, text, param) 39 | param.result = null 40 | } 41 | } 42 | expandableTextViewClass?.hookAfterConstructor( 43 | Context::class.java, AttributeSet::class.java, Int::class.javaPrimitiveType 44 | ) { param -> 45 | forceInterceptTouchField.ifEmpty { 46 | expandableTextViewClass?.declaredFields?.find { 47 | it.type == Boolean::class.javaPrimitiveType && it.modifiers == 0 48 | && param.thisObject.getBooleanField(it.name) 49 | }?.name?.also { forceInterceptTouchField = it } 50 | }.ifNotEmpty { 51 | param.thisObject.setBooleanField(it, false) 52 | } 53 | } 54 | hookInfo.albumIntroViewHolder.hookAfterMethod({ onHolderCreated }) { param -> 55 | val holder = param.thisObject 56 | holder.getObjectFieldAs( 57 | hookInfo.albumIntroViewHolder.tvAlbumDetail 58 | )?.longClick { v -> 59 | val text = holder.getObjectFieldAs( 60 | hookInfo.albumIntroViewHolder.lastTextContent 61 | ) ?: "" 62 | showCopyDialog(v.context, text, null) { 63 | it.copyToClipboard() 64 | BannerTips.success(R.string.copy_success) 65 | } 66 | true 67 | } 68 | } 69 | hookInfo.albumTagViewHolder.hookAfterMethod({ onHolderCreated }) { param -> 70 | val holder = param.thisObject 71 | holder.javaClass.declaredFields.filter { 72 | TextView::class.java.isAssignableFrom(it.type) 73 | }.forEach { f -> 74 | holder.getObjectFieldAs(f.name)?.longClick { v -> 75 | showCopyDialog(v.context, v.text, null) { 76 | it.copyToClipboard() 77 | BannerTips.success(R.string.copy_success) 78 | } 79 | true 80 | } 81 | } 82 | } 83 | } 84 | 85 | private fun showCopyDialog( 86 | context: Context, 87 | text: CharSequence, 88 | param: MethodHookParam?, 89 | onCopyAll: OnCopyAllListener? = null 90 | ) { 91 | AlertDialog.Builder(context, themeIdForDialog).run { 92 | setTitle(string(R.string.copy_enhance)) 93 | setMessage(text) 94 | setPositiveButton(string(R.string.share)) { _, _ -> 95 | context.startActivity( 96 | Intent.createChooser( 97 | Intent().apply { 98 | action = Intent.ACTION_SEND 99 | putExtra(Intent.EXTRA_TEXT, text) 100 | type = "text/plain" 101 | }, string(R.string.share_copy_content) 102 | ) 103 | ) 104 | } 105 | setNeutralButton(string(R.string.copy_all)) { _, _ -> 106 | param?.invokeOriginalMethod() 107 | onCopyAll?.invoke(text) 108 | } 109 | setNegativeButton(android.R.string.cancel, null) 110 | show() 111 | }.apply { 112 | findViewById(android.R.id.message).setTextIsSelectable(true) 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /app/src/main/java/me/kofua/qmhelper/hook/DebugHook.kt: -------------------------------------------------------------------------------- 1 | package me.kofua.qmhelper.hook 2 | 3 | import android.app.Activity 4 | import android.os.Bundle 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import me.kofua.qmhelper.qmPackage 9 | import me.kofua.qmhelper.utils.* 10 | import org.json.JSONObject 11 | 12 | object DebugHook : BaseHook { 13 | 14 | override fun hook() { 15 | /*"com.tencent.qqmusiccommon.appconfig.ChannelConfig".hookBeforeMethod( 16 | classLoader, "a" 17 | ) { param -> 18 | Thread.currentThread().stackTrace 19 | .find { it.methodName == "createSettingProvider" } 20 | ?.run { 21 | param.result = "80000" 22 | } 23 | } 24 | "com.tencent.qqmusiccommon.appconfig.w".hookBeforeMethod( 25 | classLoader, "j", String::class.java 26 | ) { it.result = true }*/ 27 | 28 | View::class.java.hookBeforeMethod( 29 | "setOnLongClickListener", 30 | View.OnLongClickListener::class.java 31 | ) { param -> 32 | val listener = param.args[0] as? View.OnLongClickListener ?: return@hookBeforeMethod 33 | param.args[0] = View.OnLongClickListener { 34 | Log.d("kofua, onLongClicked, view: $it, listener: ${listener.javaClass.name}") 35 | listener.onLongClick(it) 36 | } 37 | } 38 | View::class.java.hookBeforeMethod( 39 | "setOnClickListener", 40 | View.OnClickListener::class.java 41 | ) { param -> 42 | val listener = param.args[0] as? View.OnClickListener ?: return@hookBeforeMethod 43 | param.args[0] = View.OnClickListener { 44 | Log.d("kofua, onClicked, view: $it, listener: ${listener.javaClass.name}") 45 | listener.onClick(it) 46 | } 47 | } 48 | Activity::class.java.hookBeforeMethod("onCreate", Bundle::class.java) { param -> 49 | Log.d("kofua, creating activity: ${param.thisObject}") 50 | } 51 | qmPackage.baseFragmentClass?.hookAfterMethod( 52 | "onCreateView", 53 | LayoutInflater::class.java, 54 | ViewGroup::class.java, 55 | Bundle::class.java 56 | ) { param -> 57 | Log.d("kofua, creating fragment: ${param.thisObject}, view: ${param.result}") 58 | } 59 | "com.tencent.qqmusiccommon.util.MLog".from(classLoader)?.declaredMethods?.filter { m -> 60 | m.isPublic && m.isStatic && m.returnType == Void.TYPE && m.parameterTypes.let { 61 | it.size >= 2 && it[0] == String::class.java && it[1] == String::class.java 62 | } 63 | }?.forEach { m -> 64 | m.hookBefore { param -> 65 | val methodName = param.method.name 66 | val tag = param.args[0] as? String ?: "" 67 | val message = param.args[1] as? String ?: "" 68 | val other = param.args.getOrNull(2) 69 | when (methodName) { 70 | "d" -> when (other) { 71 | null -> android.util.Log.d(tag, message) 72 | is Throwable -> android.util.Log.d(tag, message, other) 73 | is Array<*> -> android.util.Log.d(tag, message.format(*other)) 74 | } 75 | 76 | "e" -> when (other) { 77 | null -> android.util.Log.e(tag, message) 78 | is Throwable -> android.util.Log.e(tag, message, other) 79 | is Array<*> -> android.util.Log.e(tag, message.format(*other)) 80 | } 81 | 82 | "i" -> when (other) { 83 | null -> android.util.Log.i(tag, message) 84 | is Throwable -> android.util.Log.i(tag, message, other) 85 | is Array<*> -> android.util.Log.i(tag, message.format(*other)) 86 | } 87 | 88 | "v" -> when (other) { 89 | null -> android.util.Log.v(tag, message) 90 | is Throwable -> android.util.Log.v(tag, message, other) 91 | is Array<*> -> android.util.Log.v(tag, message.format(*other)) 92 | } 93 | 94 | "w" -> when (other) { 95 | null -> android.util.Log.w(tag, message) 96 | is Throwable -> android.util.Log.w(tag, message, other) 97 | is Array<*> -> android.util.Log.w(tag, message.format(*other)) 98 | } 99 | } 100 | param.result = null 101 | } 102 | } 103 | "com.tencent.qqmusiccommon.hippy.bridge.WebApiHippyBridge".from(classLoader) 104 | ?.declaredMethods?.find { it.name == "invoke" }?.hookAfter { param -> 105 | Log.d("kofua, invoke, hippyMapJson: ${JSONObject(param.args[0].getObjectField("mDatas") as Map<*, *>)}") 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /app/src/main/java/me/kofua/qmhelper/hook/HomePageHook.kt: -------------------------------------------------------------------------------- 1 | package me.kofua.qmhelper.hook 2 | 3 | import android.view.MotionEvent 4 | import android.view.View 5 | import me.kofua.qmhelper.from 6 | import me.kofua.qmhelper.hookInfo 7 | import me.kofua.qmhelper.utils.* 8 | 9 | object HomePageHook : BaseHook { 10 | 11 | override fun hook() { 12 | if (sPrefs.getBoolean("hide_music_world", false)) { 13 | hookInfo.mainDesktopHeader.showMusicWorld.name.ifNotEmpty { 14 | hookInfo.mainDesktopHeader.replaceMethod({ showMusicWorld }) { null } 15 | } 16 | hookInfo.musicWorldPullEntrance.replaceMethod({ showButton }) { null } 17 | } 18 | if (sPrefs.getBoolean("hide_vip_bubble", false)) { 19 | hookInfo.userInfoHolder.replaceMethod({ showBubble }) { null } 20 | hookInfo.vipAdBarData.from(classLoader)?.declaredConstructors 21 | ?.find { m -> m.parameterTypes.let { it.size == 10 && it[8] == Boolean::class.javaPrimitiveType } } 22 | ?.hookBefore { it.args[8] = true } 23 | } 24 | if (sPrefs.getBoolean("purify_live_guide", false)) { 25 | hookInfo.topAreaDelegate.replaceMethod({ initLiveGuide }) { null } 26 | hookInfo.topAreaDelegate.showCurListen.name.ifNotEmpty { 27 | hookInfo.topAreaDelegate.replaceMethod({ showCurListen }) { null } 28 | } 29 | } 30 | if (sPrefs.getBoolean("purify_share_guide", false)) { 31 | hookInfo.topAreaDelegate.replaceMethod({ showShareGuide }) { null } 32 | } 33 | if (sPrefs.getBoolean("forbid_slide", false)) { 34 | hookInfo.playViewModel.hookBeforeMethod({ setCanSlide }) { 35 | it.args[0].callMethod(hookInfo.playViewModel.postCanSlide, false) 36 | it.result = null 37 | } 38 | } 39 | if (sPrefs.getBoolean("hide_ad_bar", false)) { 40 | hookInfo.adBar.clazz.from(classLoader)?.run { 41 | hookAfterAllConstructors { param -> 42 | val view = param.thisObject as View 43 | view.visibility = View.GONE 44 | } 45 | val methods = hookInfo.adBar 46 | .methods.takeIf { it.size == 2 } ?: return@run 47 | val methodA = methods[0].name 48 | val methodB = methods[1].name 49 | hookAfterMethod(methodA) { param -> 50 | val view = param.thisObject as View 51 | if (view.visibility == View.VISIBLE) 52 | view.callMethod(methodB) 53 | } 54 | hookAfterMethod(methodB) { param -> 55 | val view = param.thisObject as View 56 | if (view.visibility == View.VISIBLE) 57 | view.callMethod(methodA) 58 | } 59 | } 60 | } 61 | if (sPrefs.getBoolean("forbid_music_world", false)) { 62 | hookInfo.musicWorldTouchListener.from(classLoader) 63 | ?.replaceMethod("onTouch", View::class.java, MotionEvent::class.java) { false } 64 | } 65 | if (sPrefs.getBoolean("block_bottom_tips", false)) { 66 | hookInfo.bottomTipController.replaceMethod({ updateBottomTips }) { null } 67 | } 68 | val blockCoverAds = sPrefs.getStringSet("block_cover_ads", null) ?: setOf() 69 | if (blockCoverAds.contains("video")) { 70 | hookInfo.videoViewDelegate.replaceMethod({ onResult }) { null } 71 | } 72 | if (blockCoverAds.contains("genre")) { 73 | hookInfo.genreViewDelegate.replaceMethod({ onBind }) { null } 74 | hookInfo.topSongViewDelegate.replaceMethod({ onBind }) { null } 75 | } 76 | if (sPrefs.getBoolean("block_user_guide", false)) { 77 | hookInfo.userGuideViewDelegate.replaceMethod({ showUserGuide }) { null } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/src/main/java/me/kofua/qmhelper/hook/HomeTopTabHook.kt: -------------------------------------------------------------------------------- 1 | package me.kofua.qmhelper.hook 2 | 3 | import me.kofua.qmhelper.R 4 | import me.kofua.qmhelper.hookInfo 5 | import me.kofua.qmhelper.utils.* 6 | 7 | object HomeTopTabHook : BaseHook { 8 | private val purifyHomeTopTabIds by lazy { 9 | sPrefs.getStringSet("purify_home_top_tab", null) 10 | ?.sorted()?.map { it.toInt() } ?: listOf() 11 | } 12 | private val purifyHomeTobTabNames by lazy { 13 | stringArray(R.array.home_top_tab_values).map { it.toInt() } 14 | .zip(stringArray(R.array.home_top_tab_entries)) 15 | .filter { purifyHomeTopTabIds.contains(it.first) } 16 | .map { it.second } 17 | } 18 | 19 | override fun hook() { 20 | if (purifyHomeTopTabIds.isEmpty()) return 21 | 22 | hookInfo.homePageFragment.hookBeforeMethod({ initTabFragment }) { param -> 23 | if (purifyHomeTopTabIds.contains(param.args[0] as Int)) 24 | param.result = null 25 | } 26 | hookInfo.mainDesktopHeader.hookBeforeMethod({ addTabByName }) { param -> 27 | if (purifyHomeTobTabNames.contains(param.args[0] as String)) 28 | param.result = null 29 | } 30 | hookInfo.mainDesktopHeader.hookBeforeMethod({ addTabById }) { param -> 31 | val name = runCatchingOrNull { string(param.args[0] as Int) } 32 | ?: return@hookBeforeMethod 33 | if (purifyHomeTobTabNames.contains(name)) 34 | param.result = null 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/me/kofua/qmhelper/hook/JceHook.kt: -------------------------------------------------------------------------------- 1 | package me.kofua.qmhelper.hook 2 | 3 | import me.kofua.qmhelper.BuildConfig 4 | import me.kofua.qmhelper.hookInfo 5 | import me.kofua.qmhelper.utils.* 6 | 7 | object JceHook : BaseHook { 8 | override fun hook() { 9 | val hidden = sPrefs.getBoolean("hidden", false) 10 | val unlockTheme = sPrefs.getBoolean("unlock_theme", false) 11 | val unlockFont = sPrefs.getBoolean("unlock_font", false) 12 | 13 | hookInfo.jceRespConverter.hookAfterMethod({ parse }) { param -> 14 | val type = (param.args[1] as Class<*>).name 15 | if (BuildConfig.DEBUG) 16 | Log.d("net.jce, type: $type, json: ${param.result?.toJson()}") 17 | if (type == "com.tencent.jce.playerStyle.PlayerStyleRsp" && hidden && unlockTheme) { 18 | param.result?.run { 19 | getObjectField("alert")?.setIntField("revertTheme", 0) 20 | getObjectField("styleConf")?.setIntField("status", 0) 21 | } 22 | } else if ((type == "com.tencent.jce.personal.ApplyIconRsp" 23 | || type == "com.tencent.jce.personal.GetIconRsp") && hidden && unlockTheme 24 | ) { 25 | param.result?.getObjectField("auth")?.run { 26 | setIntField("enable", 1) 27 | setIntField("authType", 0) 28 | } 29 | } else if (type == "com.tencent.qqmusic.business.playernew.view.playerlyric.model.QueryLyricFontRsp" && hidden && unlockFont) { 30 | param.result?.getObjectFieldAs?>("fontList")?.forEach { 31 | it?.setIntField("auth", 0) 32 | } 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/me/kofua/qmhelper/hook/MiscHook.kt: -------------------------------------------------------------------------------- 1 | package me.kofua.qmhelper.hook 2 | 3 | import me.kofua.qmhelper.data.StorageVolume.Companion.toMockVolume 4 | import me.kofua.qmhelper.data.StorageVolume.Companion.toRealVolume 5 | import me.kofua.qmhelper.hookInfo 6 | import me.kofua.qmhelper.utils.* 7 | 8 | object MiscHook : BaseHook { 9 | override fun hook() { 10 | if (sPrefs.getBoolean("fix_song_filename", false)) { 11 | hookInfo.fileUtils.hookBeforeMethod({ toValidFilename }) { param -> 12 | val name = param.args[0] as? String 13 | var extra = param.args[1] as? String 14 | val ext = param.args[2] as? String 15 | extra = if (extra.orEmpty().startsWith(" [mqms")) "" else extra 16 | param.result = (name + extra + ext).toValidFatFilename(250) 17 | } 18 | } 19 | if (sPrefs.getBoolean("allow_save_to_sdcard_extern", false)) { 20 | hookInfo.storageUtils.hookAfterMethod({ getVolumes }) { param -> 21 | val volumes = param.result as Set<*> 22 | val newVolumes = hashSetOf() 23 | var changed = false 24 | runCatchingOrNull { 25 | for (v in volumes) { 26 | val mockVolume = v?.toMockVolume() ?: break 27 | val removable = mockVolume.removable 28 | if (!removable) { 29 | newVolumes.add(v) 30 | } else { 31 | mockVolume.copy(removable = false) 32 | .toRealVolume()?.let { 33 | changed = true 34 | newVolumes.add(it) 35 | } 36 | } 37 | } 38 | } 39 | if (changed) param.result = newVolumes 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/me/kofua/qmhelper/hook/SSLHook.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("DEPRECATION") 2 | 3 | package me.kofua.qmhelper.hook 4 | 5 | import android.annotation.SuppressLint 6 | import android.net.http.SslError 7 | import android.webkit.SslErrorHandler 8 | import android.webkit.WebView 9 | import me.kofua.qmhelper.utils.* 10 | import org.apache.http.conn.scheme.HostNameResolver 11 | import org.apache.http.conn.ssl.SSLSocketFactory 12 | import java.net.Socket 13 | import java.security.KeyStore 14 | import java.security.SecureRandom 15 | import java.security.cert.X509Certificate 16 | import javax.net.ssl.* 17 | 18 | object SSLHook : BaseHook { 19 | override fun hook() { 20 | @SuppressLint("CustomX509TrustManager") 21 | val emptyTrustManagers = arrayOf(object : X509TrustManager { 22 | @SuppressLint("TrustAllX509TrustManager") 23 | override fun checkClientTrusted(chain: Array, authType: String) { 24 | } 25 | 26 | @SuppressLint("TrustAllX509TrustManager") 27 | override fun checkServerTrusted(chain: Array, authType: String) { 28 | } 29 | 30 | override fun getAcceptedIssuers(): Array = emptyArray() 31 | 32 | @Suppress("unused", "UNUSED_PARAMETER") 33 | fun checkServerTrusted( 34 | chain: Array, 35 | authType: String, 36 | host: String 37 | ): List = emptyList() 38 | }) 39 | 40 | "javax.net.ssl.TrustManagerFactory".hookBeforeMethod( 41 | classLoader, 42 | "getTrustManagers" 43 | ) { it.result = emptyTrustManagers } 44 | 45 | "javax.net.ssl.SSLContext".hookBeforeMethod( 46 | classLoader, 47 | "init", 48 | "javax.net.ssl.KeyManager[]", 49 | "javax.net.ssl.TrustManager[]", 50 | SecureRandom::class.java 51 | ) { param -> 52 | param.args[0] = null 53 | param.args[1] = emptyTrustManagers 54 | param.args[2] = null 55 | } 56 | 57 | "javax.net.ssl.HttpsURLConnection".hookBeforeMethod( 58 | classLoader, 59 | "setSSLSocketFactory", 60 | javax.net.ssl.SSLSocketFactory::class.java 61 | ) { it.args[0] = "javax.net.ssl.SSLSocketFactory".on(classLoader).new() } 62 | 63 | "org.apache.http.conn.scheme.SchemeRegistry".from(classLoader) 64 | ?.hookBeforeMethod("register", "org.apache.http.conn.scheme.Scheme") { param -> 65 | if (param.args[0].callMethodAs("getName") == "https") { 66 | param.args[0] = param.args[0].javaClass.new( 67 | "https", 68 | SSLSocketFactory.getSocketFactory(), 69 | 443 70 | ) 71 | } 72 | } 73 | 74 | "org.apache.http.conn.ssl.HttpsURLConnection".from(classLoader)?.run { 75 | hookBeforeMethod("setDefaultHostnameVerifier", HostnameVerifier::class.java) { param -> 76 | param.args[0] = SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER 77 | } 78 | 79 | hookBeforeMethod("setHostnameVerifier", HostnameVerifier::class.java) { param -> 80 | param.args[0] = SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER 81 | } 82 | 83 | } 84 | "org.apache.http.conn.ssl.SSLSocketFactory".hookBeforeMethod( 85 | classLoader, 86 | "getSocketFactory" 87 | ) { it.result = SSLSocketFactory::class.java.new() } 88 | 89 | "org.apache.http.conn.ssl.SSLSocketFactory".from(classLoader) 90 | ?.hookAfterConstructor( 91 | String::class.java, 92 | KeyStore::class.java, 93 | String::class.java, 94 | KeyStore::class.java, 95 | SecureRandom::class.java, 96 | HostNameResolver::class.java 97 | ) { param -> 98 | val algorithm = param.args[0] as? String 99 | val keystore = param.args[1] as? KeyStore 100 | val keystorePassword = param.args[2] as? String 101 | val random = param.args[4] as? SecureRandom 102 | 103 | @Suppress("UNCHECKED_CAST") 104 | val trustManagers = emptyTrustManagers as Array 105 | 106 | val keyManagers = keystore?.let { 107 | SSLSocketFactory::class.java.callStaticMethodAs>( 108 | "createKeyManagers", 109 | keystore, 110 | keystorePassword 111 | ) 112 | } 113 | 114 | param.thisObject.run { 115 | setObjectField("sslcontext", SSLContext.getInstance(algorithm)) 116 | getObjectField("sslcontext") 117 | ?.callMethod("init", keyManagers, trustManagers, random) 118 | setObjectField( 119 | "socketfactory", 120 | getObjectField("sslcontext")?.callMethod("getSocketFactory") 121 | ) 122 | } 123 | } 124 | 125 | "org.apache.http.conn.ssl.SSLSocketFactory".hookAfterMethod( 126 | classLoader, 127 | "isSecure", 128 | Socket::class.java 129 | ) { it.result = true } 130 | 131 | "okhttp3.CertificatePinner".from(classLoader)?.run { 132 | (runCatchingOrNull { getDeclaredMethod("findMatchingPins", String::class.java) } 133 | ?: declaredMethods.firstOrNull { it.parameterTypes.size == 1 && it.parameterTypes[0] == String::class.java && it.returnType == List::class.java })?.hookBefore { param -> 134 | param.args[0] = "" 135 | } 136 | } 137 | 138 | "android.webkit.WebViewClient".from(classLoader)?.run { 139 | replaceMethod( 140 | "onReceivedSslError", 141 | WebView::class.java, 142 | SslErrorHandler::class.java, 143 | SslError::class.java 144 | ) { param -> 145 | (param.args[1] as SslErrorHandler).proceed() 146 | null 147 | } 148 | replaceMethod( 149 | "onReceivedError", 150 | WebView::class.java, 151 | Int::class.javaPrimitiveType, 152 | String::class.java, 153 | String::class.java 154 | ) { 155 | null 156 | } 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /app/src/main/java/me/kofua/qmhelper/hook/SettingsHook.kt: -------------------------------------------------------------------------------- 1 | package me.kofua.qmhelper.hook 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.os.Bundle 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import android.widget.RelativeLayout 10 | import android.widget.TextView 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.launch 13 | import me.kofua.qmhelper.R 14 | import me.kofua.qmhelper.from 15 | import me.kofua.qmhelper.hookInfo 16 | import me.kofua.qmhelper.qmPackage 17 | import me.kofua.qmhelper.setting.Setting 18 | import me.kofua.qmhelper.setting.SettingPack 19 | import me.kofua.qmhelper.utils.* 20 | import java.lang.reflect.InvocationHandler 21 | import java.text.SimpleDateFormat 22 | import java.util.Date 23 | import java.util.Locale 24 | import java.util.concurrent.CopyOnWriteArrayList 25 | 26 | object SettingsHook : BaseHook { 27 | private val purifyRedDots by lazy { sPrefs.getBoolean("purify_red_dots", false) } 28 | private val purifyMoreItems by lazy { 29 | sPrefs.getStringSet("purify_more_items", null) ?: setOf() 30 | } 31 | private val settingPack = SettingPack() 32 | private val settingViewTextFields by lazy { 33 | hookInfo.settingView.clazz.from(classLoader) 34 | ?.declaredFields?.filter { it.type == TextView::class.java } 35 | ?.map { it.name } ?: listOf() 36 | } 37 | private val todayFormat: String 38 | get() = SimpleDateFormat("yyyy-MM-dd", Locale.CHINA) 39 | .format(Date()) 40 | 41 | override fun hook() { 42 | hookInfo.appStarterActivity.hookAfterMethod({ doOnCreate }) { param -> 43 | val activity = param.thisObject as Activity 44 | activity.addModuleAssets() 45 | settingPack.activity = activity 46 | settingPack.checkUpdate(dialog = false) 47 | if (sPrefs.getBoolean("daily_sign_in", false)) 48 | handler.post(8000L) { 49 | mainScope.launch(Dispatchers.IO) { dailySignIn() } 50 | } 51 | if (uiMode == UiMode.NORMAL && !sPrefs.getBoolean("ui_mode_hint", false)) { 52 | handler.post(2000L) { 53 | activity.showMessageDialog( 54 | string(R.string.tips_title), 55 | string(R.string.tips_open_clean_mode), 56 | string(R.string.go_to_mode_setting), 57 | string(R.string.i_know), 58 | { sPrefs.edit { putBoolean("ui_mode_hint", true) } }, 59 | ) { 60 | sPrefs.edit { putBoolean("ui_mode_hint", true) } 61 | hookInfo.modeFragment.from(classLoader)?.let { 62 | activity.callMethod( 63 | hookInfo.appStarterActivity.addSecondFragment, 64 | it, null 65 | ) 66 | } ?: BannerTips.error(R.string.jump_failed) 67 | } 68 | } 69 | } 70 | } 71 | qmPackage.appStarterActivityClass?.hookAfterMethod( 72 | "onActivityResult", 73 | Int::class.javaPrimitiveType, 74 | Int::class.javaPrimitiveType, 75 | Intent::class.java 76 | ) { param -> 77 | val requestCode = param.args[0] as Int 78 | val resultCode = param.args[1] as Int 79 | val data = param.args[2] as? Intent 80 | settingPack.onActivityResult(requestCode, resultCode, data) 81 | } 82 | @Suppress("UNCHECKED_CAST") 83 | qmPackage.appStarterActivityClass?.hookAfterMethod( 84 | "onRequestPermissionsResult", 85 | Int::class.javaPrimitiveType, 86 | Array::class.java, 87 | IntArray::class.java, 88 | ) { param -> 89 | val requestCode = param.args[0] as Int 90 | val permissions = param.args[1] as Array 91 | val grantResults = param.args[2] as IntArray 92 | settingPack.onRequestPermissionsResult(requestCode, permissions, grantResults) 93 | } 94 | hookInfo.uiModeManager.replaceMethod({ isThemeForbid }) { false } 95 | hookInfo.settingView.hookAfterMethod({ setSetting }) { param -> 96 | val viewGroup = param.thisObject as ViewGroup 97 | val setting = param.args[0] ?: return@hookAfterMethod 98 | settingViewTextFields.forEach { 99 | viewGroup.getObjectFieldAs(it)?.isSingleLine = false 100 | } 101 | val typeField = hookInfo.setting.type 102 | if (setting.getIntField(typeField) != Setting.Type.DIVIDER.key) { 103 | (viewGroup.layoutParams as? RelativeLayout.LayoutParams)?.apply { 104 | height = RelativeLayout.LayoutParams.WRAP_CONTENT 105 | }?.let { viewGroup.layoutParams = it } 106 | if (viewGroup.minimumHeight == 50.dp) 107 | viewGroup.setPadding(0, 6.dp, 0, 6.dp) 108 | } 109 | } 110 | hookInfo.settingView.hookBeforeMethod({ setLastClickTime }) { it.args[1] = 0L } 111 | @Suppress("UNCHECKED_CAST") 112 | hookInfo.setting.drawerSettingPack.hookAfterMethod({ createSettingProvider }) { param -> 113 | val settingProviders = param.result as CopyOnWriteArrayList 114 | val hostField = hookInfo.setting.baseSettingPack.host 115 | val fragment = param.thisObject.getObjectField(hostField) 116 | ?: return@hookAfterMethod 117 | val getSetting = hookInfo.setting.baseSettingProvider.getSetting 118 | val rightDescField = hookInfo.setting.rightDesc 119 | val redDotListenerField = hookInfo.setting.redDotListener 120 | settingProviders.map { it?.callMethod(getSetting) }.forEach { s -> 121 | if (purifyRedDots) { 122 | s?.getObjectField(rightDescField)?.takeIf { 123 | it != "未开启" 124 | }?.run { s.setObjectField(rightDescField, null) } 125 | s.setObjectField(redDotListenerField, null) 126 | } 127 | } 128 | val titleField = hookInfo.setting.title 129 | for (i in settingProviders.size - 1 downTo 0) { 130 | settingProviders[i]?.callMethod(getSetting) 131 | ?.getObjectFieldAs(titleField)?.takeIf { 132 | purifyMoreItems.contains(it) 133 | }?.run { settingProviders.removeAt(i) } 134 | } 135 | if (settingProviders.last()?.callMethod(getSetting) 136 | ?.getObjectField(titleField) == Setting.TITLE_DIVIDER 137 | ) settingProviders.removeAt(settingProviders.lastIndex) 138 | 139 | val moduleSetting = Setting.button(R.string.app_name) { 140 | onQMHelperSettingClicked(it.context) 141 | } 142 | settingProviders.add(1, settingProvider(fragment, moduleSetting)) 143 | } 144 | if (purifyMoreItems.contains("创作者中心")) 145 | hookInfo.setting.drawerSettingPack.replaceMethod({ initKolEnter }) { null } 146 | 147 | if (!purifyRedDots) return 148 | hookInfo.personalEntryView.hookAfterMethod({ update }) { param -> 149 | param.thisObject.getObjectFieldAs(hookInfo.personalEntryView.rightDescView) 150 | .run { text = "" } 151 | param.thisObject.getObjectFieldAs(hookInfo.personalEntryView.redDotView) 152 | .run { visibility = View.GONE } 153 | } 154 | hookInfo.settingFragment.hookBeforeMethod({ resume }) { param -> 155 | param.thisObject.getObjectFieldAs>(hookInfo.settingFragment.settingList) 156 | .forEach { 157 | it?.setObjectField(hookInfo.setting.redDotListener, null) 158 | } 159 | } 160 | } 161 | 162 | private fun settingProvider(fragment: Any, setting: Any?): Any? { 163 | val baseSettingProviderClass = qmPackage.baseSettingProviderClass ?: return null 164 | val create = hookInfo.setting.baseSettingProvider.create.name 165 | val handler = InvocationHandler { _, _, _ -> setting } 166 | val settingProviderClass = baseSettingProviderClass.proxy(create) 167 | val unhook = baseSettingProviderClass.constructors.first().hookBefore { 168 | if (it.thisObject.javaClass.name == settingProviderClass.name) 169 | it.thisObject.invocationHandler(handler) 170 | } 171 | return settingProviderClass.new(currentContext, fragment).also { unhook?.unhook() } 172 | } 173 | 174 | private fun onQMHelperSettingClicked(context: Context) { 175 | val baseSettingFragmentClass = qmPackage.baseSettingFragmentClass ?: return 176 | val baseSettingPackClass = qmPackage.baseSettingPackClass ?: return 177 | val settingPackage = hookInfo.setting.baseSettingFragment.settingPackage.name 178 | val title = hookInfo.setting.baseSettingFragment.title.name 179 | val createSettingProvider = hookInfo.setting.baseSettingPack.createSettingProvider.name 180 | val handler = InvocationHandler { fp, fm, fArgs -> 181 | if (fm.name == settingPackage) { 182 | val packSettingHandler = InvocationHandler { _, _, _ -> 183 | val settingProviders = settingPack.getSettings() 184 | .map { settingProvider(fp, it) } 185 | CopyOnWriteArrayList(settingProviders) 186 | } 187 | val moduleSettingPackClass = baseSettingPackClass.proxy(createSettingProvider) 188 | val unhook = baseSettingPackClass.constructors.first().hookBefore { 189 | if (it.thisObject.javaClass.name == moduleSettingPackClass.name) 190 | it.thisObject.invocationHandler(packSettingHandler) 191 | } 192 | moduleSettingPackClass.new(currentContext, fp, Bundle()).also { unhook?.unhook() } 193 | } else if (fm.name == title) { 194 | string(R.string.app_name) 195 | } else if (fArgs.isNullOrEmpty()) { 196 | fp.callSuper(fm) 197 | } else { 198 | fp.callSuper(fm, fArgs) 199 | } 200 | } 201 | val moduleSettingFragmentClass = baseSettingFragmentClass.proxy(settingPackage, title) 202 | val unhook = qmPackage.baseFragmentClass?.hookBeforeConstructor { 203 | if (it.thisObject.javaClass.name == moduleSettingFragmentClass.name) 204 | it.thisObject.invocationHandler(handler) 205 | } 206 | context.callMethod( 207 | hookInfo.appStarterActivity.addSecondFragment, 208 | moduleSettingFragmentClass, null 209 | ) 210 | unhook?.unhook() 211 | } 212 | 213 | private fun dailySignIn(check: Boolean = true) { 214 | val recordKey = "${uin()}_daily_sign_in_record" 215 | if (!isLogin() || sCaches.getString(recordKey, null) == todayFormat) 216 | return 217 | runCatching { 218 | webJsonPost( 219 | "https://u.y.qq.com/cgi-bin/musics.fcg?_webcgikey=doSignIn", 220 | "music.actCenter.DaysignactSvr", 221 | "doSignIn", 222 | mapOf("date" to "") 223 | ) 224 | }.onFailure { 225 | Log.e(it) 226 | }.onSuccess { json -> 227 | val response = json?.runCatchingOrNull { toJSONObject() } ?: return 228 | val code = response.optJSONObject("req_0") 229 | ?.optJSONObject("data")?.optInt("code") ?: return 230 | if (code == 0) { 231 | if (check) { 232 | runCatching { 233 | webHtmlGet("https://i.y.qq.com/n2/m/client/day_sign/index.html") 234 | }.onFailure { 235 | Log.e(it) 236 | }.onSuccess { 237 | dailySignIn(false) 238 | } 239 | } else { 240 | sCaches.edit { putString(recordKey, todayFormat) } 241 | BannerTips.success(R.string.daily_sign_in_success) 242 | } 243 | } else if (code == 2) { 244 | sCaches.edit { putString(recordKey, todayFormat) } 245 | } 246 | } 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /app/src/main/java/me/kofua/qmhelper/hook/SplashHook.kt: -------------------------------------------------------------------------------- 1 | package me.kofua.qmhelper.hook 2 | 3 | import me.kofua.qmhelper.from 4 | import me.kofua.qmhelper.hookInfo 5 | import me.kofua.qmhelper.utils.* 6 | 7 | object SplashHook : BaseHook { 8 | 9 | private val splashShowTypeClass by Weak { "com.tencentmusic.ad.core.constant.SplashShowType" from classLoader } 10 | private val noAdSplashType by lazy { splashShowTypeClass?.getStaticObjectField("NO_AD") } 11 | 12 | override fun hook() { 13 | if (!sPrefs.getBoolean("purify_splash", false)) return 14 | 15 | hookInfo.splashAdapter.from(classLoader)?.declaredMethods 16 | ?.filter { it.returnType == splashShowTypeClass } 17 | ?.forEach { m -> 18 | m.hookBefore { param -> 19 | noAdSplashType?.let { param.result = it } 20 | } 21 | } 22 | hookInfo.adManager.replaceMethod({ get }) { null } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/me/kofua/qmhelper/hook/WebLoginHook.kt: -------------------------------------------------------------------------------- 1 | package me.kofua.qmhelper.hook 2 | 3 | import me.kofua.qmhelper.hookInfo 4 | import me.kofua.qmhelper.utils.hookBeforeMethod 5 | import me.kofua.qmhelper.utils.isFakeSigEnabledFor 6 | import me.kofua.qmhelper.utils.isPackageInstalled 7 | 8 | object WebLoginHook : BaseHook { 9 | private val loginClients = arrayOf("com.tencent.mobileqq", "com.tencent.tim") 10 | 11 | override fun hook() { 12 | hookInfo.authAgent.hookBeforeMethod({ startActionActivity }) { param -> 13 | loginClients.find { isPackageInstalled(it) }?.let { 14 | if (!isFakeSigEnabledFor(it)) 15 | param.result = false 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/me/kofua/qmhelper/hook/WebViewHook.kt: -------------------------------------------------------------------------------- 1 | package me.kofua.qmhelper.hook 2 | 3 | import android.graphics.Bitmap 4 | import android.webkit.JavascriptInterface 5 | import android.webkit.WebView 6 | import android.webkit.WebViewClient 7 | import me.kofua.qmhelper.BuildConfig 8 | import me.kofua.qmhelper.XposedInit.Companion.moduleRes 9 | import me.kofua.qmhelper.qmPackage 10 | import me.kofua.qmhelper.utils.* 11 | 12 | object WebViewHook : BaseHook { 13 | private val hidden by lazy { sPrefs.getBoolean("hidden", false) } 14 | private val unlockTheme by lazy { sPrefs.getBoolean("unlock_theme", false) } 15 | 16 | private val hookedClient = HashSet>() 17 | private val hooker: Hooker = { param -> 18 | try { 19 | val url = param.args[1] as String 20 | if (url.startsWith("https://i.y.qq.com/n2/m/theme/index.html") 21 | || url.startsWith("https://i.y.qq.com/n2/m/basic/client/themev2/home/index.html") 22 | || url.startsWith("https://y.qq.com/m/basic/client/themev2/detail/index.html") 23 | || url.startsWith("https://y.qq.com/m/client/player_detail/index.html") 24 | ) { 25 | param.args[0].callMethod( 26 | "evaluateJavascript", 27 | """(function(){$js})()""".trimMargin(), 28 | null 29 | ) 30 | } 31 | } catch (e: Throwable) { 32 | Log.e(e) 33 | } 34 | } 35 | 36 | private val jsHooker = object : Any() { 37 | @Suppress("UNUSED") 38 | @JavascriptInterface 39 | fun hook(url: String, requestBody: String?, text: String): String { 40 | return this@WebViewHook.hook(url, requestBody, text) 41 | } 42 | } 43 | 44 | private val js by lazy { 45 | runCatchingOrNull { 46 | moduleRes.assets.open("xhook.js") 47 | .use { it.bufferedReader().readText() } 48 | } ?: "" 49 | } 50 | 51 | override fun hook() { 52 | if (BuildConfig.DEBUG) 53 | WebView.setWebContentsDebuggingEnabled(true) 54 | WebView::class.java.hookBeforeMethod( 55 | "setWebViewClient", 56 | WebViewClient::class.java 57 | ) { param -> 58 | val clazz = param.args[0]?.javaClass ?: return@hookBeforeMethod 59 | (param.thisObject as WebView).run { 60 | addJavascriptInterface(jsHooker, "hooker") 61 | } 62 | if (hookedClient.contains(clazz)) return@hookBeforeMethod 63 | try { 64 | clazz.getDeclaredMethod( 65 | "onPageStarted", 66 | WebView::class.java, 67 | String::class.java, 68 | Bitmap::class.java 69 | ).hookBefore(hooker) 70 | hookedClient.add(clazz) 71 | Log.d("hook webview $clazz") 72 | } catch (_: NoSuchMethodException) { 73 | } 74 | } 75 | if (BuildConfig.DEBUG) 76 | qmPackage.x5WebViewClass?.callStaticMethod("setWebContentsDebuggingEnabled", true) 77 | qmPackage.x5WebViewClass?.hookBeforeMethod( 78 | "setWebViewClient", 79 | "com.tencent.smtt.sdk.WebViewClient" 80 | ) { param -> 81 | val clazz = param.args[0]?.javaClass ?: return@hookBeforeMethod 82 | param.thisObject.callMethod("addJavascriptInterface", jsHooker, "hooker") 83 | if (hookedClient.contains(clazz)) return@hookBeforeMethod 84 | try { 85 | clazz.getDeclaredMethod( 86 | "onPageStarted", 87 | qmPackage.x5WebViewClass, 88 | String::class.java, 89 | Bitmap::class.java 90 | ).hookBefore(hooker) 91 | hookedClient.add(clazz) 92 | Log.d("hook webview $clazz") 93 | } catch (_: NoSuchMethodException) { 94 | } 95 | } 96 | } 97 | 98 | fun hook(url: String, requestBody: String?, text: String): String { 99 | if (BuildConfig.DEBUG) 100 | Log.d("net.webview, url: $url, requestBody: $requestBody, text: $text") 101 | if (hidden && unlockTheme) { 102 | if (url.contains("/cgi-bin/musics.fcg?_webcgikey=GetSubject") 103 | || url.contains("/cgi-bin/musics.fcg?_webcgikey=GetIcon") 104 | ) { 105 | val json = text.runCatchingOrNull { toJSONObject() } ?: return text 106 | val data = json.optJSONObject("req_0")?.optJSONObject("data") ?: return text 107 | data.optJSONObject("auth")?.run { 108 | put("enable", 1) 109 | put("authType", 0) 110 | } 111 | return json.toString() 112 | } else if (url.contains("/cgi-bin/musics.fcg?_webcgikey=get_subject_info")) { 113 | val json = text.runCatchingOrNull { toJSONObject() } ?: return text 114 | val data = json.optJSONObject("req_0")?.optJSONObject("data") ?: return text 115 | val themeList = data.optJSONArray("vlist") ?: return text 116 | data.optJSONObject("alert")?.put("revertTheme", 0) 117 | for (item in themeList) { 118 | item.put("enable", 1) 119 | } 120 | return json.toString() 121 | } else if (url.contains("/cgi-bin/musics.fcg?_webcgikey=GetPlayerStyleDetail")) { 122 | val json = text.runCatchingOrNull { toJSONObject() } ?: return text 123 | val data = json.optJSONObject("req_0")?.optJSONObject("data") ?: return text 124 | val styleConf = data.optJSONObject("styleConf") ?: return text 125 | data.optJSONObject("alert")?.put("revertTheme", 0) 126 | styleConf.put("status", 0) 127 | return json.toString() 128 | } 129 | } 130 | return text 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /app/src/main/java/me/kofua/qmhelper/setting/Setting.kt: -------------------------------------------------------------------------------- 1 | package me.kofua.qmhelper.setting 2 | 3 | import android.content.SharedPreferences 4 | import android.view.View 5 | import androidx.annotation.StringRes 6 | import me.kofua.qmhelper.hookInfo 7 | import me.kofua.qmhelper.qmPackage 8 | import me.kofua.qmhelper.utils.* 9 | import java.lang.reflect.Proxy 10 | 11 | typealias IsSwitchOn = () -> Boolean 12 | typealias OnSwitchChanged = (enabled: Boolean) -> Unit 13 | 14 | class Setting { 15 | enum class Type(val key: Int) { 16 | SWITCH(0), BUTTON_WITHOUT_ARROW(1), BUTTON_WITH_ARROW(2), CATEGORY(3), DIVIDER(6) 17 | } 18 | 19 | companion object { 20 | const val TITLE_DIVIDER = "blank space" 21 | private val emptyClickListener = View.OnClickListener { } 22 | 23 | fun get(build: Setting.() -> Unit) = Setting().apply(build).build() 24 | 25 | fun switch( 26 | title: String, 27 | summary: String? = null, 28 | isSwitchOn: IsSwitchOn? = null, 29 | onSwitchChanged: OnSwitchChanged? = null 30 | ) = get { 31 | this.type = Type.SWITCH 32 | this.title = title 33 | this.summary = summary 34 | this.isSwitchOn = isSwitchOn 35 | this.onSwitchChanged = onSwitchChanged 36 | } 37 | 38 | fun switch( 39 | @StringRes title: Int, 40 | @StringRes summary: Int? = null, 41 | isSwitchOn: IsSwitchOn? = null, 42 | onSwitchChanged: OnSwitchChanged? = null 43 | ) = switch(string(title), summary?.let { string(it) }, isSwitchOn, onSwitchChanged) 44 | 45 | fun switch( 46 | key: String, 47 | title: String, 48 | summary: String? = null, 49 | defValue: Boolean = false, 50 | prefs: SharedPreferences = sPrefs, 51 | ) = switch( 52 | title, 53 | summary, 54 | { prefs.getBoolean(key, defValue) }, 55 | ) { prefs.edit { putBoolean(key, it) } } 56 | 57 | fun switch( 58 | key: String, 59 | @StringRes title: Int, 60 | @StringRes summary: Int? = null, 61 | defValue: Boolean = false, 62 | prefs: SharedPreferences = sPrefs, 63 | ) = switch(key, string(title), summary?.let { string(it) }, defValue, prefs) 64 | 65 | fun button( 66 | title: String, 67 | summary: String? = null, 68 | rightDesc: String? = null, 69 | arrow: Boolean = true, 70 | clickListener: View.OnClickListener? = null 71 | ) = get { 72 | this.type = if (arrow) Type.BUTTON_WITH_ARROW else Type.BUTTON_WITHOUT_ARROW 73 | this.title = title 74 | this.summary = summary 75 | this.rightDesc = rightDesc 76 | this.clickListener = clickListener 77 | } 78 | 79 | fun button( 80 | @StringRes title: Int, 81 | @StringRes summary: Int? = null, 82 | @StringRes rightDesc: Int? = null, 83 | arrow: Boolean = true, 84 | clickListener: View.OnClickListener? = null 85 | ) = button( 86 | string(title), 87 | summary?.let { string(it) }, 88 | rightDesc?.let { string(it) }, 89 | arrow, 90 | clickListener 91 | ) 92 | 93 | fun category(title: String) = get { 94 | this.type = Type.CATEGORY 95 | this.title = title 96 | } 97 | 98 | fun category(@StringRes title: Int) = category(string(title)) 99 | 100 | fun divider() = get { 101 | this.type = Type.DIVIDER 102 | this.title = TITLE_DIVIDER 103 | } 104 | } 105 | 106 | var type: Type = Type.SWITCH 107 | var title: String? = null 108 | var rightDesc: String? = null 109 | var summary: String? = null 110 | var isSwitchOn: IsSwitchOn? = null 111 | var onSwitchChanged: OnSwitchChanged? = null 112 | var clickListener: View.OnClickListener? = null 113 | 114 | private fun build(): Any? { 115 | val builder = hookInfo.setting.builder 116 | return qmPackage.settingClass 117 | ?.callStaticMethod(hookInfo.setting.with, currentContext) 118 | ?.apply { 119 | setIntField(builder.type, type.key) 120 | setObjectField(builder.title, title) 121 | setObjectField(builder.rightDesc, rightDesc) 122 | setObjectField(builder.summary, summary) 123 | setObjectField(builder.switchListener, Proxy.newProxyInstance( 124 | currentContext.classLoader, 125 | arrayOf(qmPackage.switchListenerClass) 126 | ) { _, m, args -> 127 | val switchListener = hookInfo.setting.switchListener 128 | when (m.name) { 129 | switchListener.isSwitchOn.name -> isSwitchOn?.invoke() ?: false 130 | switchListener.onSwitchStatusChange.name -> onSwitchChanged?.invoke(args[0] as Boolean) 131 | else -> null 132 | } 133 | }) 134 | setObjectField(builder.clickListener, clickListener ?: emptyClickListener) 135 | }?.callMethod(builder.build) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /app/src/main/java/me/kofua/qmhelper/setting/SettingPack.kt: -------------------------------------------------------------------------------- 1 | package me.kofua.qmhelper.setting 2 | 3 | import android.Manifest 4 | import android.annotation.SuppressLint 5 | import android.app.Activity 6 | import android.app.AlertDialog 7 | import android.content.ActivityNotFoundException 8 | import android.content.Intent 9 | import android.content.pm.PackageManager.PERMISSION_GRANTED 10 | import android.provider.DocumentsContract 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.delay 13 | import kotlinx.coroutines.launch 14 | import kotlinx.coroutines.withContext 15 | import me.kofua.qmhelper.BuildConfig 16 | import me.kofua.qmhelper.R 17 | import me.kofua.qmhelper.utils.* 18 | import org.json.JSONArray 19 | import java.io.File 20 | import java.net.URL 21 | import kotlin.system.exitProcess 22 | 23 | class SettingPack { 24 | var activity by Weak { null } 25 | 26 | private var clickCounter = 0 27 | 28 | companion object { 29 | private const val CODE_EXPORT = 2333 30 | private const val CODE_IMPORT = 2334 31 | private const val CODE_STORAGE = 2335 32 | private const val CODE_CHOOSE_DECRYPT_DIR = 2336 33 | 34 | @JvmStatic 35 | fun restartApplication(activity: Activity) { 36 | val pm = activity.packageManager 37 | val intent = pm.getLaunchIntentForPackage(activity.packageName) 38 | activity.finishAffinity() 39 | activity.startActivity(intent) 40 | exitProcess(0) 41 | } 42 | } 43 | 44 | fun getSettings() = buildList { 45 | Setting.category(R.string.prefs_category_main) 46 | ?.let { add(it) } 47 | Setting.switch( 48 | "copy_enhance", 49 | R.string.prefs_copy_enhance_title, 50 | R.string.prefs_copy_enhance_summary, 51 | )?.let { add(it) } 52 | Setting.switch( 53 | "daily_sign_in", 54 | R.string.prefs_daily_sign_in_title, 55 | R.string.prefs_daily_sign_in_summary 56 | )?.let { add(it) } 57 | 58 | Setting.category(R.string.prefs_category_purify) 59 | ?.let { add(it) } 60 | Setting.switch( 61 | "purify_splash", 62 | R.string.prefs_purify_splash_title, 63 | R.string.prefs_purify_splash_summary, 64 | )?.let { add(it) } 65 | Setting.button( 66 | R.string.prefs_purify_home_top_tab_title, 67 | R.string.prefs_purify_home_top_tab_summary 68 | ) { 69 | onPurifyHomeTopTabClicked() 70 | }?.let { add(it) } 71 | Setting.switch( 72 | "purify_red_dots", 73 | R.string.prefs_purify_red_dots_title, 74 | R.string.prefs_purify_red_dots_summary, 75 | )?.let { add(it) } 76 | Setting.button( 77 | R.string.prefs_purify_more_items_title, 78 | R.string.prefs_purify_more_items_summary, 79 | ) { 80 | onPurifyMorePageClicked() 81 | }?.let { add(it) } 82 | Setting.switch( 83 | "hide_music_world", 84 | R.string.prefs_hide_music_world_title, 85 | R.string.prefs_hide_music_world_summary, 86 | )?.let { add(it) } 87 | Setting.switch( 88 | "hide_vip_bubble", 89 | R.string.prefs_hide_vip_bubble_title, 90 | R.string.prefs_hide_vip_bubble_summary, 91 | )?.let { add(it) } 92 | Setting.switch( 93 | "purify_live_guide", 94 | R.string.prefs_purify_live_guide_title, 95 | R.string.prefs_purify_live_guide_summary, 96 | )?.let { add(it) } 97 | Setting.switch( 98 | "forbid_slide", 99 | R.string.prefs_forbid_slide_title, 100 | R.string.prefs_forbid_slide_summary, 101 | )?.let { add(it) } 102 | Setting.switch( 103 | "block_live", 104 | R.string.prefs_block_live_title, 105 | )?.let { add(it) } 106 | Setting.button(R.string.prefs_purify_search_title) { 107 | onPurifySearchClicked() 108 | }?.let { add(it) } 109 | Setting.switch( 110 | "hide_ad_bar", 111 | R.string.prefs_hide_ad_bar_title, 112 | R.string.prefs_hide_ad_bar_summary, 113 | )?.let { add(it) } 114 | Setting.switch( 115 | "forbid_music_world", 116 | R.string.prefs_forbid_music_world_title, 117 | R.string.prefs_forbid_music_world_summary, 118 | )?.let { add(it) } 119 | Setting.switch( 120 | "block_bottom_tips", 121 | R.string.prefs_block_bottom_tips_title, 122 | R.string.prefs_block_bottom_tips_summary, 123 | )?.let { add(it) } 124 | Setting.button( 125 | R.string.prefs_block_cover_ads_title, 126 | R.string.prefs_block_cover_ads_summary 127 | ) { 128 | onBlockCoverAdsClicked() 129 | }?.let { add(it) } 130 | Setting.switch( 131 | "block_user_guide", 132 | R.string.prefs_block_user_guide_title, 133 | R.string.prefs_block_user_guide_summary, 134 | )?.let { add(it) } 135 | Setting.switch( 136 | "block_comment_banners", 137 | R.string.prefs_block_comment_banners_title, 138 | R.string.prefs_block_comment_banners_summary, 139 | )?.let { add(it) } 140 | Setting.switch( 141 | "remove_comment_recommend", 142 | R.string.prefs_remove_comment_recommend_title, 143 | R.string.prefs_remove_comment_recommend_summary, 144 | )?.let { add(it) } 145 | Setting.switch( 146 | "remove_mine_kol", 147 | R.string.prefs_remove_mine_kol_title, 148 | R.string.prefs_remove_mine_kol_summary, 149 | )?.let { add(it) } 150 | Setting.switch( 151 | "purify_share_guide", 152 | R.string.prefs_purify_share_guide_title, 153 | R.string.prefs_purify_share_guide_summary 154 | )?.let { add(it) } 155 | Setting.switch( 156 | "block_common_ads", 157 | R.string.prefs_block_common_ads_title, 158 | R.string.prefs_block_common_ads_summary 159 | )?.let { add(it) } 160 | Setting.switch( 161 | R.string.prefs_global_light_effect_title, 162 | R.string.prefs_global_light_effect_summary, 163 | isSwitchOn = { !qmSp.getBoolean("KEY_GLOBAL_LIGHT_EFFECT_SWITCH", true) }, 164 | onSwitchChanged = { enabled -> 165 | qmSp.edit { putBoolean("KEY_GLOBAL_LIGHT_EFFECT_SWITCH", !enabled) } 166 | } 167 | )?.also { add(it) } 168 | Setting.switch( 169 | "move_down_recently", 170 | R.string.prefs_move_down_recently_title, 171 | R.string.prefs_move_down_recently_summary 172 | )?.also { add(it) } 173 | Setting.switch( 174 | "hide_song_list_guide", 175 | R.string.prefs_hide_song_list_guide_title, 176 | R.string.prefs_hide_song_list_guide_summary 177 | )?.also { add(it) } 178 | 179 | Setting.category(R.string.prefs_category_misc) 180 | ?.let { add(it) } 181 | Setting.switch( 182 | "fix_song_filename", 183 | R.string.prefs_fix_song_filename_title, 184 | R.string.prefs_fix_song_filename_summary, 185 | )?.let { add(it) } 186 | Setting.switch( 187 | "allow_save_to_sdcard_extern", 188 | R.string.prefs_allow_save_to_sdcard_extern_title, 189 | R.string.prefs_allow_save_to_sdcard_extern_summary, 190 | )?.let { add(it) } 191 | 192 | if (sPrefs.getBoolean("hidden", false)) { 193 | Setting.category(R.string.prefs_category_hidden) 194 | ?.let { add(it) } 195 | Setting.switch( 196 | R.string.prefs_hidden_title, 197 | R.string.prefs_hidden_summary, 198 | isSwitchOn = { sPrefs.getBoolean("hidden", false) }, 199 | onSwitchChanged = { enabled -> 200 | sPrefs.edit { putBoolean("hidden", enabled) } 201 | if (enabled) { 202 | BannerTips.success(R.string.hidden_enabled) 203 | } else { 204 | BannerTips.success(R.string.hidden_disabled) 205 | } 206 | } 207 | )?.let { add(it) } 208 | Setting.button(R.string.prefs_decrypt_downloads_title) { 209 | onDecryptButtonClicked() 210 | }?.let { add(it) } 211 | Setting.switch( 212 | "unlock_theme", 213 | R.string.prefs_unlock_theme_title 214 | )?.let { add(it) } 215 | Setting.switch( 216 | "unlock_font", 217 | R.string.prefs_unlock_font_title, 218 | R.string.prefs_unlock_font_summary 219 | )?.let { add(it) } 220 | Setting.switch( 221 | "unlock_lyric_kinetic", 222 | R.string.prefs_unlock_lyric_kinetic_title 223 | )?.let { add(it) } 224 | } 225 | 226 | Setting.category(R.string.prefs_category_settings) 227 | ?.let { add(it) } 228 | Setting.switch( 229 | "save_log", 230 | string(R.string.prefs_save_log_title), 231 | string(R.string.prefs_save_log_summary, logFile.absolutePath), 232 | )?.let { add(it) } 233 | Setting.button(R.string.prefs_share_log_title) { 234 | onShareLogClicked() 235 | }?.let { add(it) } 236 | Setting.button( 237 | R.string.reboot_host, 238 | R.string.reboot_host_summary, 239 | ) { 240 | activity?.let { restartApplication(it) } 241 | }?.let { add(it) } 242 | 243 | Setting.category(R.string.prefs_category_backup) 244 | ?.let { add(it) } 245 | Setting.button(R.string.prefs_export_title) { 246 | onExportClicked() 247 | }?.let { add(it) } 248 | Setting.button(R.string.prefs_import_title) { 249 | onImportClicked() 250 | }?.let { add(it) } 251 | 252 | Setting.category(R.string.prefs_category_about) 253 | ?.let { add(it) } 254 | Setting.button( 255 | string(R.string.prefs_version_title), 256 | "%s (%s)".format(BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE), 257 | arrow = false 258 | ) { 259 | onVersionClicked() 260 | }?.let { add(it) } 261 | val latestVer = sCaches.getString("latest_version", "") 262 | Setting.button( 263 | R.string.prefs_check_update_title, 264 | rightDesc = if (!latestVer.isNullOrEmpty()) R.string.found_update else null 265 | ) { 266 | checkUpdate(dialog = true, force = true) 267 | }?.let { add(it) } 268 | Setting.button( 269 | R.string.prefs_author_title, 270 | R.string.prefs_author_summary 271 | ) { 272 | onAuthorClicked() 273 | }?.let { add(it) } 274 | Setting.button( 275 | R.string.prefs_tg_channel_title, 276 | R.string.prefs_tg_channel_summary 277 | ) { 278 | onTGChannelClicked() 279 | }?.let { add(it) } 280 | } 281 | 282 | fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 283 | when (requestCode) { 284 | CODE_EXPORT, CODE_IMPORT -> { 285 | val uri = data?.data 286 | if (uri == null || resultCode != Activity.RESULT_OK) return 287 | val prefsFile = File(currentContext.filesDir, "../shared_prefs/qmhelper.xml") 288 | when (requestCode) { 289 | CODE_IMPORT -> runCatchingOrNull { 290 | prefsFile.outputStream().use { o -> 291 | currentContext.contentResolver.openInputStream(uri) 292 | ?.use { it.copyTo(o) } 293 | } 294 | activity?.showMessageDialog( 295 | R.string.tips_title, 296 | R.string.pls_reboot_host, 297 | R.string.yes, 298 | R.string.no 299 | ) { 300 | activity?.let { restartApplication(it) } 301 | } 302 | } 303 | 304 | CODE_EXPORT -> runCatchingOrNull { 305 | prefsFile.inputStream().use { 306 | currentContext.contentResolver.openOutputStream(uri) 307 | ?.use { o -> it.copyTo(o) } 308 | } 309 | } 310 | } 311 | } 312 | 313 | CODE_CHOOSE_DECRYPT_DIR -> { 314 | val uri = data?.data 315 | if (uri == null || resultCode != Activity.RESULT_OK) return 316 | val saveDir = uri.realDirPath()?.let { File(it) } 317 | ?: run { BannerTips.failed(R.string.invalid_storage_path); return } 318 | decryptSongs(saveDir) 319 | } 320 | } 321 | } 322 | 323 | @Suppress("UNUSED_PARAMETER") 324 | fun onRequestPermissionsResult( 325 | requestCode: Int, 326 | permissions: Array, 327 | grantResults: IntArray 328 | ) { 329 | if (requestCode == CODE_STORAGE) { 330 | val activity = activity ?: return 331 | if (grantResults.isNotEmpty() && grantResults.first() == PERMISSION_GRANTED) { 332 | chooseDecryptSaveDir() 333 | } else { 334 | val storagePerm = Manifest.permission.WRITE_EXTERNAL_STORAGE 335 | if (activity.shouldShowRequestPermissionRationale(storagePerm)) { 336 | activity.showMessageDialog( 337 | R.string.tips_title, 338 | R.string.need_to_request_storage_perm, 339 | android.R.string.ok, 340 | android.R.string.cancel 341 | ) { 342 | activity.requestPermissions(arrayOf(storagePerm), CODE_STORAGE) 343 | } 344 | } else { 345 | BannerTips.failed(R.string.storage_perm_grant_failed) 346 | } 347 | } 348 | } 349 | } 350 | 351 | private fun onPurifyHomeTopTabClicked() { 352 | val checkedTabIds = sPrefs.getStringSet("purify_home_top_tab", null) ?: setOf() 353 | val tabs = stringArray(R.array.home_top_tab_entries) 354 | val tabIds = stringArray(R.array.home_top_tab_values) 355 | showMultiChoiceDialog(tabs, tabIds, checkedTabIds) { 356 | sPrefs.edit { putStringSet("purify_home_top_tab", it) } 357 | } 358 | } 359 | 360 | private fun onPurifyMorePageClicked() { 361 | val checkedItemIds = sPrefs.getStringSet("purify_more_items", null) ?: setOf() 362 | val items = stringArray(R.array.more_items_entries) 363 | val itemIds = stringArray(R.array.more_items_values) 364 | showMultiChoiceDialog(items, itemIds, checkedItemIds) { 365 | sPrefs.edit { putStringSet("purify_more_items", it) } 366 | } 367 | } 368 | 369 | private fun showMultiChoiceDialog( 370 | entries: Array, 371 | values: Array, 372 | checkedValues: Set, 373 | onConfirm: (newCheckedValues: Set) -> Unit 374 | ) { 375 | val checkedStates = values.map { checkedValues.contains(it) } 376 | .toBooleanArray() 377 | val newCheckedValues = mutableSetOf() 378 | .apply { addAll(checkedValues) } 379 | AlertDialog.Builder(activity, themeIdForDialog) 380 | .setMultiChoiceItems(entries, checkedStates) { _, which, isChecked -> 381 | if (isChecked) newCheckedValues.add(values[which]) 382 | else newCheckedValues.remove(values[which]) 383 | } 384 | .setNegativeButton(android.R.string.cancel, null) 385 | .setPositiveButton(android.R.string.ok) { _, _ -> 386 | onConfirm(newCheckedValues) 387 | }.show() 388 | } 389 | 390 | private fun onExportClicked() { 391 | val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { 392 | type = "text/xml" 393 | putExtra(Intent.EXTRA_TITLE, "qmhelper.xml") 394 | addCategory(Intent.CATEGORY_OPENABLE) 395 | } 396 | try { 397 | activity?.startActivityForResult( 398 | Intent.createChooser( 399 | intent, 400 | string(R.string.save_prefs_file) 401 | ), CODE_EXPORT 402 | ) 403 | } catch (_: ActivityNotFoundException) { 404 | BannerTips.error(R.string.open_file_manager_failed) 405 | } 406 | } 407 | 408 | private fun onImportClicked() { 409 | val intent = Intent(Intent.ACTION_GET_CONTENT).apply { 410 | type = "text/xml" 411 | addCategory(Intent.CATEGORY_OPENABLE) 412 | } 413 | try { 414 | activity?.startActivityForResult( 415 | Intent.createChooser( 416 | intent, 417 | string(R.string.choose_prefs_file) 418 | ), CODE_IMPORT 419 | ) 420 | } catch (_: ActivityNotFoundException) { 421 | BannerTips.error(R.string.open_file_manager_failed) 422 | } 423 | } 424 | 425 | private fun onShareLogClicked() { 426 | if ((!logFile.exists() && !oldLogFile.exists()) || !shouldSaveLog) { 427 | BannerTips.failed(R.string.not_found_log_file) 428 | return 429 | } 430 | AlertDialog.Builder(activity, themeIdForDialog) 431 | .setTitle(string(R.string.prefs_share_log_title)) 432 | .setItems( 433 | arrayOf( 434 | "log.txt", 435 | string(R.string.old_log_item, "old_log.txt") 436 | ) 437 | ) { _, which -> 438 | val toShareLog = if (which == 0) logFile else oldLogFile 439 | if (toShareLog.exists()) { 440 | toShareLog.copyTo( 441 | File(activity?.cacheDir, "com_qq_e_download/log.txt"), 442 | overwrite = true 443 | ) 444 | val uri = 445 | "content://${activity?.packageName}.fileprovider/gdt_sdk_download_path2/log.txt".toUri() 446 | activity?.startActivity(Intent.createChooser(Intent().apply { 447 | action = Intent.ACTION_SEND 448 | putExtra(Intent.EXTRA_STREAM, uri) 449 | addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) 450 | setDataAndType(uri, "text/log") 451 | }, string(R.string.prefs_share_log_title))) 452 | } else { 453 | BannerTips.failed(R.string.log_file_not_exist) 454 | } 455 | } 456 | .show() 457 | } 458 | 459 | fun checkUpdate(dialog: Boolean, force: Boolean = false) = mainScope.launch(Dispatchers.IO) { 460 | val lastCheckTime = sCaches.getLong("last_check_update_time", 0L) 461 | if (!force && System.currentTimeMillis() - lastCheckTime < 10 * 60 * 1000) 462 | return@launch 463 | val json = URL(string(R.string.releases_url)) 464 | .runCatchingOrNull { readText() } ?: run { 465 | if (dialog) BannerTips.error(R.string.check_update_failed) 466 | return@launch 467 | } 468 | val jsonArray = json.runCatchingOrNull { JSONArray(this) } ?: run { 469 | if (dialog) BannerTips.error(R.string.check_update_failed) 470 | return@launch 471 | } 472 | sCaches.edit { putLong("last_check_update_time", System.currentTimeMillis()) } 473 | var latestVer = "" 474 | var latestVerTag = "" 475 | var changelog = "" 476 | for (result in jsonArray) { 477 | val tagName = result.optString("tag_name").takeIf { 478 | it.startsWith("v") 479 | } ?: continue 480 | val name = result.optString("name") 481 | if (name.isNotEmpty() && BuildConfig.VERSION_NAME != name) { 482 | latestVer = name 483 | latestVerTag = tagName 484 | changelog = result.optString("body") 485 | .substringAfterLast("更新日志").trim() 486 | } 487 | break 488 | } 489 | if (latestVer.isNotEmpty()) { 490 | sCaches.edit { putString("latest_version", latestVer) } 491 | if (!dialog) return@launch 492 | withContext(Dispatchers.Main) { 493 | activity?.showMessageDialog( 494 | string(R.string.found_update_with_version, latestVer), 495 | changelog, 496 | string(R.string.update_now), 497 | string(R.string.i_know) 498 | ) { 499 | val uri = string(R.string.update_url, latestVerTag).toUri() 500 | activity?.startActivity(Intent(Intent.ACTION_VIEW, uri)) 501 | } 502 | } 503 | } else { 504 | sCaches.edit { remove("latest_version") } 505 | if (!dialog) return@launch 506 | BannerTips.success(R.string.no_update) 507 | } 508 | } 509 | 510 | private fun onVersionClicked() { 511 | if (sPrefs.getBoolean("hidden", false)) return 512 | if (++clickCounter == 7) { 513 | clickCounter = 0 514 | sPrefs.edit { putBoolean("hidden", true) } 515 | BannerTips.success(R.string.hidden_enabled) 516 | } else if (clickCounter >= 4) { 517 | BannerTips.success(string(R.string.hidden_remain_click_count, 7 - clickCounter)) 518 | } 519 | } 520 | 521 | private fun onAuthorClicked() { 522 | val uri = string(R.string.repo_url).toUri() 523 | activity?.startActivity(Intent(Intent.ACTION_VIEW, uri)) 524 | } 525 | 526 | private fun onTGChannelClicked() { 527 | val uri = string(R.string.tg_url).toUri() 528 | activity?.startActivity(Intent(Intent.ACTION_VIEW, uri)) 529 | } 530 | 531 | private fun onPurifySearchClicked() { 532 | val checkedValues = sPrefs.getStringSet("purify_search", null) ?: setOf() 533 | val entries = stringArray(R.array.purify_search_entries) 534 | val values = stringArray(R.array.purify_search_values) 535 | showMultiChoiceDialog(entries, values, checkedValues) { 536 | sPrefs.edit { putStringSet("purify_search", it) } 537 | } 538 | } 539 | 540 | private fun onDecryptButtonClicked() { 541 | val activity = activity ?: return 542 | 543 | val storagePerm = Manifest.permission.WRITE_EXTERNAL_STORAGE 544 | if (activity.checkSelfPermission(storagePerm) == PERMISSION_GRANTED) { 545 | chooseDecryptSaveDir() 546 | } else { 547 | activity.requestPermissions(arrayOf(storagePerm), CODE_STORAGE) 548 | } 549 | } 550 | 551 | private fun onBlockCoverAdsClicked() { 552 | val entries = stringArray(R.array.block_cover_ads_entries) 553 | val values = stringArray(R.array.block_cover_ads_values) 554 | val checkedValues = sPrefs.getStringSet("block_cover_ads", null) ?: setOf() 555 | showMultiChoiceDialog(entries, values, checkedValues) { 556 | sPrefs.edit { putStringSet("block_cover_ads", it) } 557 | } 558 | } 559 | 560 | @SuppressLint("InlinedApi") 561 | private fun chooseDecryptSaveDir() { 562 | val initialUri = "content://com.android.externalstorage.documents/document/primary:Music" 563 | val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply { 564 | putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialUri.toUri()) 565 | } 566 | try { 567 | BannerTips.success(R.string.pls_choose_save_dir) 568 | activity?.startActivityForResult(intent, CODE_CHOOSE_DECRYPT_DIR) 569 | } catch (_: ActivityNotFoundException) { 570 | BannerTips.error(R.string.open_file_manager_failed) 571 | } 572 | } 573 | 574 | private fun decryptSongs(saveDir: File) = mainScope.launch { 575 | val (total, success, successOrigSongs) = withContext(Dispatchers.IO) { 576 | Decryptor.batchDecrypt(saveDir) { srcFile, current, total, success -> 577 | val name = srcFile.name 578 | if (success) { 579 | BannerTips.success( 580 | string(R.string.single_decrypt_success, current, total, name) 581 | ) 582 | } else { 583 | BannerTips.failed( 584 | string(R.string.single_decrypt_failed, current, total, name) 585 | ) 586 | } 587 | } 588 | } 589 | if (total != 0) delay(3000) 590 | val failed = total - success 591 | if (total == 0) 592 | activity?.showMessageDialog( 593 | R.string.decrypt_completed_title, 594 | R.string.decrypt_completed_summary_none, 595 | android.R.string.ok 596 | ) 597 | else if (failed == 0) 598 | activity?.showMessageDialogX( 599 | string(R.string.decrypt_completed_title), 600 | string(R.string.decrypt_completed_summary_all, total), 601 | string(R.string.yes), 602 | string(R.string.no) 603 | )?.let { 604 | if (it) withContext(Dispatchers.IO) { 605 | Decryptor.deleteOrigSongs(successOrigSongs) 606 | } 607 | } 608 | else 609 | activity?.showMessageDialogX( 610 | string(R.string.decrypt_completed_title), 611 | string(R.string.decrypt_completed_summary, total, success, failed), 612 | string(R.string.yes), 613 | string(R.string.no) 614 | )?.let { 615 | if (it) withContext(Dispatchers.IO) { 616 | Decryptor.deleteOrigSongs(successOrigSongs) 617 | } 618 | } 619 | } 620 | } 621 | -------------------------------------------------------------------------------- /app/src/main/java/me/kofua/qmhelper/utils/BannerTips.kt: -------------------------------------------------------------------------------- 1 | package me.kofua.qmhelper.utils 2 | 3 | import android.os.Looper 4 | import android.widget.Toast 5 | import androidx.annotation.StringRes 6 | import me.kofua.qmhelper.R 7 | import me.kofua.qmhelper.hookInfo 8 | import me.kofua.qmhelper.qmPackage 9 | 10 | object BannerTips { 11 | 12 | @JvmStatic 13 | private fun showStyledToast(type: Int, message: String) { 14 | val newMessage = string(R.string.app_name) + ":" + message 15 | val action = Runnable { 16 | qmPackage.bannerTipsClass?.also { 17 | it.callStaticMethod( 18 | hookInfo.bannerTips.showStyledToast, 19 | currentContext, type, newMessage, 0, 0, true, 0 20 | ) 21 | } ?: Toast.makeText(currentContext, newMessage, Toast.LENGTH_SHORT).show() 22 | } 23 | if (Looper.myLooper() == Looper.getMainLooper()) { 24 | runCatchingOrNull { action.run() } 25 | } else { 26 | runCatchingOrNull { handler.post(action) } 27 | } 28 | } 29 | 30 | @JvmStatic 31 | fun success(message: String) { 32 | showStyledToast(0, message) 33 | } 34 | 35 | @JvmStatic 36 | fun success(@StringRes resId: Int) { 37 | showStyledToast(0, string(resId)) 38 | } 39 | 40 | @JvmStatic 41 | fun error(message: String) { 42 | showStyledToast(1, message) 43 | } 44 | 45 | @JvmStatic 46 | fun error(@StringRes resId: Int) { 47 | showStyledToast(1, string(resId)) 48 | } 49 | 50 | @JvmStatic 51 | fun failed(message: String) { 52 | showStyledToast(2, message) 53 | } 54 | 55 | @JvmStatic 56 | fun failed(@StringRes resId: Int) { 57 | showStyledToast(2, string(resId)) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/me/kofua/qmhelper/utils/Decryptor.kt: -------------------------------------------------------------------------------- 1 | package me.kofua.qmhelper.utils 2 | 3 | import android.os.Environment 4 | import me.kofua.qmhelper.data.StorageVolume.Companion.toMockVolume 5 | import me.kofua.qmhelper.hookInfo 6 | import me.kofua.qmhelper.qmPackage 7 | import java.io.File 8 | 9 | typealias SingleDecryptListener = ( 10 | srcFile: File, current: Int, total: Int, success: Boolean 11 | ) -> Unit 12 | 13 | object Decryptor { 14 | private val wideEncRegex = Regex("""^(.+)\.(qmc\w+|mgg\w?|mflac\w?|mdolby|mmp4)(\.flac)?$""") 15 | private val pureRegex = Regex("""\s\[mqms(\d)*]""") 16 | 17 | data class Ext(val ext: String, val ver: Int) 18 | 19 | private val decExtMap = mapOf( 20 | "qmcflac" to Ext("flac", 1), 21 | "qmcogg" to Ext("ogg", 1), 22 | "qmc0" to Ext("mp3", 1), 23 | "qmc2" to Ext("m4a", 1), 24 | "qmc3" to Ext("mp3", 1), 25 | "qmc4" to Ext("m4a", 1), 26 | "qmc6" to Ext("m4a", 1), 27 | "qmc8" to Ext("m4a", 1), 28 | "qmcra" to Ext("m4a", 1), 29 | "mgg" to Ext("ogg", 2), 30 | "mgg0" to Ext("ogg", 2), 31 | "mggl" to Ext("ogg", 2), 32 | "mgg1" to Ext("ogg", 2), 33 | "mgg2" to Ext("ogg", 2), 34 | "mflac" to Ext("flac", 2), 35 | "mflac0" to Ext("flac", 2), 36 | "mflac1" to Ext("flac", 2), 37 | "mflac2" to Ext("flac", 2), 38 | "mdolby" to Ext("m4a", 2), 39 | "mmp4" to Ext("mp4", 2) 40 | ) 41 | private val File.isEncrypted: Boolean 42 | get() = name.matches(wideEncRegex) 43 | 44 | fun batchDecrypt( 45 | saveDir: File, 46 | listener: SingleDecryptListener? = null 47 | ): Triple> { 48 | val encSongs = getEncSongs().ifEmpty { 49 | return Triple(0, 0, listOf()) 50 | } 51 | val total = encSongs.size 52 | var current = 1 53 | val successOrigSongs = mutableListOf() 54 | val success = encSongs.count { f -> 55 | decrypt(f, saveDir).also { 56 | listener?.invoke(f, current++, total, it) 57 | it.yes { successOrigSongs.add(f) } 58 | } 59 | } 60 | return Triple(total, success, successOrigSongs) 61 | } 62 | 63 | private fun getEncSongs(): List { 64 | return runCatching { 65 | qmPackage.storageUtilsClass?.callStaticMethodAs>( 66 | hookInfo.storageUtils.getVolumes, currentContext 67 | )?.mapNotNull { it?.toMockVolume()?.path } 68 | ?.flatMap { p -> 69 | File(p, "qqmusic/song") 70 | .takeIf { it.isDirectory } 71 | ?.listFiles()?.toList() 72 | ?: listOf() 73 | }?.filter { it.isEncrypted } 74 | }.onFailure { Log.e(it) }.getOrNull() ?: run { 75 | val externalDir = Environment.getExternalStorageDirectory() 76 | val songDir = File(externalDir, "qqmusic/song") 77 | songDir.listFiles()?.filter { it.isEncrypted } 78 | } ?: listOf() 79 | } 80 | 81 | fun deleteOrigSongs(songs: List) = songs.forEach { it.delete() } 82 | 83 | private fun decrypt(srcFile: File, saveDir: File? = null): Boolean { 84 | srcFile.takeIf { it.isFile } ?: return false 85 | saveDir?.mkdirs() 86 | val srcFilePath = srcFile.absolutePath 87 | if (!srcFile.isEncrypted) return false 88 | val matchResult = wideEncRegex.matchEntire(srcFilePath)!! 89 | val newEnc = matchResult.groups[3] != null 90 | val fileNoExt = if (newEnc) { 91 | matchResult.groups[1]!!.value 92 | } else srcFilePath.substringBeforeLast(".") 93 | val fileExt = if (newEnc) { 94 | matchResult.groups[2]!!.value 95 | } else srcFilePath.substringAfterLast(".", "") 96 | val decExt = decExtMap[fileExt]?.ext 97 | ?: if (fileExt.isEmpty()) "dec" else "$fileExt.dec" 98 | val destFilePath = (if (saveDir == null) { 99 | "$fileNoExt.$decExt" 100 | } else if (newEnc) { 101 | File(saveDir, "${srcFile.name.replace(wideEncRegex, "$1")}.$decExt").absolutePath 102 | } else { 103 | File(saveDir, "${srcFile.nameWithoutExtension}.$decExt").absolutePath 104 | }).replace(pureRegex, "") 105 | File(destFilePath).delete() 106 | val eKey = getFileEKey(srcFilePath) 107 | .ifEmpty { return staticDecrypt(srcFilePath, destFilePath) } 108 | return decrypt(srcFilePath, destFilePath, eKey) 109 | } 110 | 111 | private fun getFileEKey(srcFilePath: String) = runCatching { 112 | qmPackage.eKeyManagerClass?.getStaticObjectField(hookInfo.eKeyManager.instance) 113 | ?.callMethodAs(hookInfo.eKeyManager.getFileEKey, srcFilePath, null) 114 | }.onFailure { Log.e(it) }.getOrNull() ?: "" 115 | 116 | private fun decrypt(srcFilePath: String, destFilePath: String, eKey: String) = runCatching { 117 | qmPackage.eKeyDecryptorClass?.getStaticObjectField(hookInfo.eKeyDecryptor.instance) 118 | ?.callMethod(hookInfo.eKeyDecryptor.decryptFile, srcFilePath, destFilePath, eKey) 119 | true 120 | }.onFailure { Log.e(it) }.getOrNull() ?: false 121 | 122 | private fun staticDecrypt(srcFilePath: String, destFilePath: String) = runCatching { 123 | qmPackage.vipDownloadHelperClass?.callStaticMethod( 124 | hookInfo.vipDownloadHelper.decryptFile, srcFilePath, destFilePath 125 | ) 126 | true 127 | }.onFailure { Log.e(it) }.getOrNull() ?: false 128 | } 129 | -------------------------------------------------------------------------------- /app/src/main/java/me/kofua/qmhelper/utils/DexHelper.kt: -------------------------------------------------------------------------------- 1 | package me.kofua.qmhelper.utils 2 | 3 | import java.lang.reflect.Field 4 | import java.lang.reflect.Member 5 | 6 | class DexHelper(private val classLoader: ClassLoader) : AutoCloseable { 7 | 8 | private val token = load(classLoader) 9 | 10 | external fun findMethodUsingString( 11 | str: String, 12 | matchPrefix: Boolean = false, 13 | returnType: Long = -1, 14 | parameterCount: Short = -1, 15 | parameterShorty: String? = null, 16 | declaringClass: Long = -1, 17 | parameterTypes: LongArray? = null, 18 | containsParameterTypes: LongArray? = null, 19 | dexPriority: IntArray? = null, 20 | findFirst: Boolean = true 21 | ): LongArray 22 | 23 | external fun findMethodInvoking( 24 | methodIndex: Long, 25 | returnType: Long = -1, 26 | parameterCount: Short = -1, 27 | parameterShorty: String? = null, 28 | declaringClass: Long = -1, 29 | parameterTypes: LongArray? = null, 30 | containsParameterTypes: LongArray? = null, 31 | dexPriority: IntArray? = null, 32 | findFirst: Boolean = true 33 | ): LongArray 34 | 35 | external fun findMethodInvoked( 36 | methodIndex: Long, 37 | returnType: Long = -1, 38 | parameterCount: Short = -1, 39 | parameterShorty: String? = null, 40 | declaringClass: Long = -1, 41 | parameterTypes: LongArray? = null, 42 | containsParameterTypes: LongArray? = null, 43 | dexPriority: IntArray? = null, 44 | findFirst: Boolean = true 45 | ): LongArray 46 | 47 | external fun findMethodSettingField( 48 | fieldIndex: Long, 49 | returnType: Long = -1, 50 | parameterCount: Short = -1, 51 | parameterShorty: String? = null, 52 | declaringClass: Long = -1, 53 | parameterTypes: LongArray? = null, 54 | containsParameterTypes: LongArray? = null, 55 | dexPriority: IntArray? = null, 56 | findFirst: Boolean = true 57 | ): LongArray 58 | 59 | external fun findMethodGettingField( 60 | fieldIndex: Long, 61 | returnType: Long = -1, 62 | parameterCount: Short = -1, 63 | parameterShorty: String? = null, 64 | declaringClass: Long = -1, 65 | parameterTypes: LongArray? = null, 66 | containsParameterTypes: LongArray? = null, 67 | dexPriority: IntArray? = null, 68 | findFirst: Boolean = true 69 | ): LongArray 70 | 71 | external fun findField( 72 | type: Long, 73 | dexPriority: IntArray? = null, 74 | findFirst: Boolean = true 75 | ): LongArray 76 | 77 | external fun decodeMethodIndex(methodIndex: Long): Member? 78 | 79 | external fun encodeMethodIndex(method: Member): Long 80 | 81 | external fun decodeFieldIndex(fieldIndex: Long): Field? 82 | 83 | external fun encodeFieldIndex(field: Field): Long 84 | 85 | external fun encodeClassIndex(clazz: Class<*>): Long 86 | 87 | external fun decodeClassIndex(classIndex: Long): Class<*>? 88 | 89 | external fun createFullCache() 90 | 91 | external override fun close() 92 | 93 | protected fun finalize() = close() 94 | 95 | private external fun load(classLoader: ClassLoader): Long 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/me/kofua/qmhelper/utils/Log.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package me.kofua.qmhelper.utils 4 | 5 | import de.robv.android.xposed.XposedBridge 6 | import android.util.Log as ALog 7 | 8 | object Log { 9 | private const val maxLength = 3000 10 | private const val TAG = "QMHelper" 11 | 12 | @JvmStatic 13 | private fun doLog(f: (String, String) -> Int, obj: Any?, toXposed: Boolean = false) { 14 | val str = if (obj is Throwable) ALog.getStackTraceString(obj) else obj.toString() 15 | 16 | if (str.length > maxLength) { 17 | val chunkCount: Int = str.length / maxLength 18 | for (i in 0..chunkCount) { 19 | val max: Int = maxLength * (i + 1) 20 | if (max >= str.length) { 21 | doLog(f, str.substring(maxLength * i)) 22 | } else { 23 | doLog(f, str.substring(maxLength * i, max)) 24 | } 25 | } 26 | } else { 27 | f(TAG, str) 28 | if (toXposed) 29 | XposedBridge.log("$TAG : $str") 30 | } 31 | } 32 | 33 | @JvmStatic 34 | fun d(obj: Any?) { 35 | doLog(ALog::d, obj) 36 | } 37 | 38 | @JvmStatic 39 | fun i(obj: Any?) { 40 | doLog(ALog::i, obj) 41 | } 42 | 43 | @JvmStatic 44 | fun e(obj: Any?) { 45 | doLog(ALog::e, obj, true) 46 | } 47 | 48 | @JvmStatic 49 | fun v(obj: Any?) { 50 | doLog(ALog::v, obj) 51 | } 52 | 53 | @JvmStatic 54 | fun w(obj: Any?) { 55 | doLog(ALog::w, obj) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/me/kofua/qmhelper/utils/api.kt: -------------------------------------------------------------------------------- 1 | package me.kofua.qmhelper.utils 2 | 3 | import android.os.Build 4 | import android.webkit.WebView 5 | import me.kofua.qmhelper.hookInfo 6 | import me.kofua.qmhelper.qmPackage 7 | import org.json.JSONObject 8 | import java.net.HttpURLConnection 9 | import java.net.URL 10 | import java.util.Random 11 | import java.util.zip.DeflaterInputStream 12 | import java.util.zip.GZIPInputStream 13 | 14 | val webViewDefUA by lazy { 15 | runCatching { 16 | qmPackage.x5WebViewClass?.new(currentContext) 17 | ?.callMethod("getSettings") 18 | ?.callMethodAs("getUserAgentString") 19 | ?: WebView(currentContext).settings.userAgentString ?: "" 20 | }.onFailure { Log.e(it) }.getOrNull() ?: "" 21 | } 22 | 23 | val defaultUA = 24 | "Mozilla/5.0 (Linux; Android %s; %s Build/RQ3A.211001.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/89.0.4389.72 MQQBrowser/6.2 TBS/046141 Mobile Safari/537.36" 25 | .format(Build.VERSION.RELEASE, Build.MODEL) 26 | 27 | fun cookies(url: String? = null): String { 28 | return qmPackage.webRequestHeadersClass?.getStaticObjectField( 29 | hookInfo.webRequestHeaders.instance 30 | )?.callMethodAs(hookInfo.webRequestHeaders.getCookies, url) ?: "" 31 | } 32 | 33 | fun webUA(): String { 34 | return qmPackage.webRequestHeadersClass?.getStaticObjectField( 35 | hookInfo.webRequestHeaders.instance 36 | )?.callMethodAs(hookInfo.webRequestHeaders.getUA, null, null)?.let { 37 | //"$webViewDefUA QQJSSDK/1.3 /$it" 38 | "$defaultUA QQJSSDK/1.3 /$it" 39 | } ?: "" 40 | } 41 | 42 | fun uin(): String { 43 | return qmPackage.userManagerClass?.callStaticMethod( 44 | hookInfo.userManager.get 45 | )?.callMethodAs(hookInfo.userManager.getMusicUin) ?: "" 46 | } 47 | 48 | fun guid(): String { 49 | return qmSp.getString("KEY_OPEN_UDID", null) ?: "" 50 | } 51 | 52 | fun uid(): String { 53 | return sessionCacheSp.getString("UID", null) ?: "" 54 | } 55 | 56 | fun isLogin(): Boolean { 57 | return qmPackage.userManagerClass?.callStaticMethod( 58 | hookInfo.userManager.get 59 | )?.callMethodAs(hookInfo.userManager.isLogin) ?: false 60 | } 61 | 62 | fun nativeSign(body: String, query: String): String { 63 | return qmPackage.mERJniClass?.callStaticMethodOrNullAs( 64 | "calc", body.toByteArray(), query.toByteArray() 65 | )?.split(' ')?.firstOrNull() ?: "" 66 | } 67 | 68 | fun webSign(data: String): String { 69 | val minLen = 10 70 | val maxLen = 16 71 | val ran = Random() 72 | val ranLen = ran.nextInt(maxLen - minLen) + minLen 73 | val encNonce = "CJBPACrRuNy7" 74 | val signPrefix = "zza" 75 | val chars = "0123456789abcdefghijklmnopqrstuvwxyz" 76 | val uuid = buildString(ranLen) { 77 | for (i in 0 until ranLen) 78 | append(chars[ran.nextInt(chars.length)]) 79 | } 80 | return signPrefix + uuid + (encNonce + data).md5Hex 81 | } 82 | 83 | fun webSignB(data: String): String { 84 | val md5 = data.md5Hex 85 | val prefixIndex = intArrayOf(21, 4, 9, 26, 16, 20, 27, 30) 86 | val suffixIndex = intArrayOf(18, 11, 3, 2, 1, 7, 6, 25) 87 | val xorKey = intArrayOf(212, 45, 80, 68, 195, 163, 163, 203, 157, 220, 254, 91, 204, 79, 104, 6) 88 | val prefixSign = buildString { prefixIndex.forEach { append(md5[it]) } } 89 | val suffixSign = buildString { suffixIndex.forEach { append(md5[it]) } } 90 | val middleSign = ByteArray(xorKey.size).apply { 91 | xorKey.forEachIndexed { i, k -> 92 | val s = md5.substring(i * 2, i * 2 + 2) 93 | val xor = s.toInt(16) xor k 94 | this[i] = xor.toByte() 95 | } 96 | }.base64.replace("[+/=]".toRegex(), "").lowercase() 97 | return "zzb$prefixSign$middleSign$suffixSign" 98 | } 99 | 100 | fun webJsonRequestBody(module: String, method: String, param: Map) = 101 | JSONObject().apply { 102 | put("comm", JSONObject().apply { 103 | put("g_tk", 635035947) 104 | put("uin", uin()) 105 | put("format", "json") 106 | put("inCharset", "utf-8") 107 | put("outCharset", "utf-8") 108 | put("notice", 0) 109 | put("platform", "h5") 110 | put("needNewCode", 1) 111 | put("ct", 23) 112 | put("cv", 0) 113 | }) 114 | put("req_0", JSONObject().apply { 115 | put("module", module) 116 | put("method", method) 117 | put("param", JSONObject(param)) 118 | }) 119 | } 120 | 121 | fun webJsonPost(url: String, module: String, method: String, param: Map): String? { 122 | val timeout = 10_000 123 | val time = System.currentTimeMillis() 124 | val reqBodyJson = webJsonRequestBody(module, method, param).toString() 125 | val sign = webSignB(reqBodyJson) 126 | val newUrl = if (url.contains("?")) "$url&_=$time&sign=$sign" else "$url?_=$time&sign=$sign" 127 | val connection = URL(newUrl).openConnection() as HttpURLConnection 128 | connection.requestMethod = "POST" 129 | connection.connectTimeout = timeout 130 | connection.readTimeout = timeout 131 | connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded") 132 | connection.setRequestProperty("Accept", "application/json") 133 | connection.setRequestProperty("Cookie", cookies(newUrl)) 134 | connection.setRequestProperty("User-Agent", webUA()) 135 | connection.setRequestProperty("Accept-Encoding", "gzip, deflate") 136 | connection.doOutput = true 137 | connection.outputStream.buffered().use { 138 | it.write(reqBodyJson.toByteArray()) 139 | } 140 | if (connection.responseCode == HttpURLConnection.HTTP_OK) { 141 | val inputStream = when (connection.contentEncoding?.lowercase()) { 142 | "gzip" -> GZIPInputStream(connection.inputStream) 143 | "deflate" -> DeflaterInputStream(connection.inputStream) 144 | else -> connection.inputStream 145 | } 146 | return inputStream.bufferedReader().use { it.readText() } 147 | } 148 | return null 149 | } 150 | 151 | fun webHtmlGet(url: String): String? { 152 | val timeout = 10_000 153 | val connection = URL(url).openConnection() as HttpURLConnection 154 | connection.requestMethod = "GET" 155 | connection.connectTimeout = timeout 156 | connection.readTimeout = timeout 157 | connection.setRequestProperty("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9") 158 | connection.setRequestProperty("Cookie", cookies(url)) 159 | connection.setRequestProperty("User-Agent", webUA()) 160 | connection.setRequestProperty("Accept-Encoding", "gzip, deflate") 161 | if (connection.responseCode == HttpURLConnection.HTTP_OK) { 162 | val inputStream = when (connection.contentEncoding?.lowercase()) { 163 | "gzip" -> GZIPInputStream(connection.inputStream) 164 | "deflate" -> DeflaterInputStream(connection.inputStream) 165 | else -> connection.inputStream 166 | } 167 | return inputStream.bufferedReader().use { it.readText() } 168 | } 169 | return null 170 | } 171 | -------------------------------------------------------------------------------- /app/src/main/java/me/kofua/qmhelper/utils/boolean.kt: -------------------------------------------------------------------------------- 1 | package me.kofua.qmhelper.utils 2 | 3 | import kotlin.contracts.InvocationKind 4 | import kotlin.contracts.contract 5 | 6 | inline fun Boolean.yes(action: () -> Unit): Boolean { 7 | contract { callsInPlace(action, InvocationKind.AT_MOST_ONCE) } 8 | if (this) action() 9 | return this 10 | } 11 | 12 | inline fun Boolean.no(action: () -> Unit): Boolean { 13 | contract { callsInPlace(action, InvocationKind.AT_MOST_ONCE) } 14 | if (!this) action() 15 | return this 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/me/kofua/qmhelper/utils/file.kt: -------------------------------------------------------------------------------- 1 | // copy from android.os.FileUtils.java 2 | package me.kofua.qmhelper.utils 3 | 4 | import androidx.annotation.IntRange 5 | 6 | private fun Char.isValidFatFilenameChar(): Boolean { 7 | return if (code in 0x00..0x1F) { 8 | false 9 | } else when (this) { 10 | '"', '*', '/', ':', '<', '>', '?', '\\', '|' -> false 11 | else -> code != 0x7F 12 | } 13 | } 14 | 15 | private fun Char.isValidExtFilenameChar(): Boolean { 16 | return when (this) { 17 | '\u0000', '/' -> false 18 | else -> true 19 | } 20 | } 21 | 22 | private fun trimFilename(str: String, maxBytes: Int): String { 23 | val res = StringBuilder(str) 24 | trimFilename(res, maxBytes) 25 | return res.toString() 26 | } 27 | 28 | private fun trimFilename(res: StringBuilder, maxBytes: Int) { 29 | var max = maxBytes 30 | var raw = res.toString().toByteArray() 31 | if (raw.size > max) { 32 | max -= 3 33 | while (raw.size > max) { 34 | res.deleteCharAt(res.length / 2) 35 | raw = res.toString().toByteArray() 36 | } 37 | res.insert(res.length / 2, "...") 38 | } 39 | } 40 | 41 | /** 42 | * Mutate the given filename to make it valid for a FAT filesystem, 43 | * replacing any invalid characters with "_". 44 | */ 45 | fun String?.toValidFatFilename(@IntRange(1, 255) maxBytes: Int = 255): String { 46 | require(maxBytes <= 255) { "maxBytes must not greater than 255." } 47 | if (isNullOrEmpty() || "." == this || ".." == this) 48 | return "(invalid)" 49 | return buildString(length) { 50 | for (c in this@toValidFatFilename) { 51 | if (c.isValidFatFilenameChar()) append(c) 52 | else append('_') 53 | } 54 | // Even though vfat allows 255 UCS-2 chars, we might eventually write to 55 | // ext4 through a FUSE layer, so use that limit. 56 | trimFilename(this, maxBytes) 57 | } 58 | } 59 | 60 | /** 61 | * Mutate the given filename to make it valid for an ext4 filesystem, 62 | * replacing any invalid characters with "_". 63 | */ 64 | fun String?.toValidExtFilename(@IntRange(1, 255) maxBytes: Int = 255): String { 65 | require(maxBytes <= 255) { "maxBytes must not greater than 255." } 66 | if (isNullOrEmpty() || "." == this || ".." == this) 67 | return "(invalid)" 68 | return buildString(length) { 69 | for (c in this@toValidExtFilename) { 70 | if (c.isValidExtFilenameChar()) append(c) 71 | else append('_') 72 | } 73 | trimFilename(this, maxBytes) 74 | } 75 | } 76 | 77 | /** 78 | * Check if given filename is valid for a FAT filesystem. 79 | */ 80 | fun String?.isValidFatFilename() = this != null && this == toValidFatFilename() 81 | 82 | /** 83 | * Check if given filename is valid for an ext4 filesystem. 84 | */ 85 | fun String?.isValidExtFilename() = this != null && this == toValidExtFilename() 86 | -------------------------------------------------------------------------------- /app/src/main/java/me/kofua/qmhelper/utils/hash.kt: -------------------------------------------------------------------------------- 1 | package me.kofua.qmhelper.utils 2 | 3 | import android.util.Base64 4 | import java.security.MessageDigest 5 | import java.security.NoSuchAlgorithmException 6 | 7 | private val HEX_DIGITS = 8 | charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f') 9 | 10 | @Suppress("SameParameterValue") 11 | private fun hashTemplate(data: ByteArray, algorithm: String): ByteArray? { 12 | return if (data.isEmpty()) null else try { 13 | val md: MessageDigest = MessageDigest.getInstance(algorithm) 14 | md.update(data) 15 | md.digest() 16 | } catch (e: NoSuchAlgorithmException) { 17 | null 18 | } 19 | } 20 | 21 | fun ByteArray.toHexString(): String { 22 | val hexDigits = HEX_DIGITS 23 | val len = size 24 | if (len <= 0) return "" 25 | val ret = CharArray(len shl 1) 26 | var i = 0 27 | var j = 0 28 | while (i < len) { 29 | ret[j++] = hexDigits[this[i].toInt() shr 4 and 0x0f] 30 | ret[j++] = hexDigits[this[i].toInt() and 0x0f] 31 | i++ 32 | } 33 | return String(ret) 34 | } 35 | 36 | val String.md5Hex: String 37 | get() = hashTemplate(toByteArray(), "MD5")?.toHexString() ?: "" 38 | 39 | val ByteArray.base64: String 40 | get() = Base64.encodeToString(this, Base64.NO_WRAP) 41 | -------------------------------------------------------------------------------- /app/src/main/java/me/kofua/qmhelper/utils/json.kt: -------------------------------------------------------------------------------- 1 | package me.kofua.qmhelper.utils 2 | 3 | import me.kofua.qmhelper.classLoader 4 | import me.kofua.qmhelper.from 5 | import me.kofua.qmhelper.hookInfo 6 | import org.json.JSONArray 7 | import org.json.JSONObject 8 | 9 | fun String?.toJSONObject() = JSONObject(orEmpty()) 10 | 11 | @Suppress("UNCHECKED_CAST") 12 | fun JSONArray.asSequence() = (0 until length()).asSequence().map { get(it) as T } 13 | 14 | operator fun JSONArray.iterator(): Iterator = 15 | (0 until length()).asSequence().map { get(it) as JSONObject }.iterator() 16 | 17 | fun JSONArray?.orEmpty() = this ?: JSONArray() 18 | fun JSONArray?.isEmpty() = this == null || this.length() == 0 19 | fun JSONArray?.isNotEmpty() = !isEmpty() 20 | 21 | val gson by lazy { hookInfo.gson.clazz.from(classLoader)?.new() } 22 | 23 | inline fun String.fromJson(): T? { 24 | return gson?.callMethodAs(hookInfo.gson.fromJson, this, T::class.java) 25 | } 26 | 27 | fun String.fromJson(type: Class<*>?): Any? { 28 | return gson?.callMethod(hookInfo.gson.fromJson, this, type) 29 | } 30 | 31 | fun Any.toJson(): String? { 32 | return gson?.callMethodAs(hookInfo.gson.toJson, this) 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/me/kofua/qmhelper/utils/proxy.kt: -------------------------------------------------------------------------------- 1 | package me.kofua.qmhelper.utils 2 | 3 | import android.content.Context 4 | import com.android.dx.stock.ProxyBuilder 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.launch 7 | import me.kofua.qmhelper.hookInfo 8 | import me.kofua.qmhelper.qmPackage 9 | import java.lang.reflect.InvocationHandler 10 | import java.lang.reflect.Method 11 | 12 | fun Class<*>.proxy( 13 | vararg onlyMethods: String, 14 | ): Class<*> = ProxyBuilder.forClass(this) 15 | .parentClassLoader(classLoader) 16 | .dexCache(currentContext.getDir("qmhelper_dx", Context.MODE_PRIVATE)) 17 | .apply { 18 | if (onlyMethods.isNotEmpty()) 19 | onlyMethods(onlyMethods.map { m -> 20 | this@proxy.findMethod(deep = true) { it.name == m } 21 | }.toTypedArray()) 22 | }.buildProxyClass() 23 | 24 | fun Any.invocationHandler(handler: InvocationHandler) = 25 | ProxyBuilder.setInvocationHandler(this, handler) 26 | 27 | fun Any.callSuper(m: Method, vararg args: Any?): Any? = ProxyBuilder.callSuper(this, m, *args) 28 | 29 | fun preloadProxyClasses() = mainScope.launch(Dispatchers.IO) { 30 | val baseSettingFragmentClass = qmPackage.baseSettingFragmentClass ?: return@launch 31 | val settingPackage = 32 | hookInfo.setting.baseSettingFragment.settingPackage.name.ifEmpty { return@launch } 33 | val title = hookInfo.setting.baseSettingFragment.title.name.ifEmpty { return@launch } 34 | val baseSettingPackClass = qmPackage.baseSettingPackClass ?: return@launch 35 | val createSettingProvider = 36 | hookInfo.setting.baseSettingPack.createSettingProvider.name.ifEmpty { return@launch } 37 | val baseSettingProviderClass = qmPackage.baseSettingProviderClass ?: return@launch 38 | val create = hookInfo.setting.baseSettingProvider.create.name.ifEmpty { return@launch } 39 | 40 | baseSettingFragmentClass.proxy(settingPackage, title) 41 | baseSettingPackClass.proxy(createSettingProvider) 42 | baseSettingProviderClass.proxy(create) 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/me/kofua/qmhelper/utils/ui.kt: -------------------------------------------------------------------------------- 1 | package me.kofua.qmhelper.utils 2 | 3 | import android.app.Activity 4 | import android.app.AlertDialog 5 | import android.app.Dialog 6 | import android.view.View 7 | import androidx.annotation.StringRes 8 | import androidx.annotation.StyleRes 9 | import kotlinx.coroutines.suspendCancellableCoroutine 10 | import me.kofua.qmhelper.hookInfo 11 | import me.kofua.qmhelper.qmPackage 12 | import kotlin.coroutines.resume 13 | 14 | typealias ButtonClickListener = (v: View) -> Unit 15 | 16 | val isBlackSkinInUse: Boolean 17 | get() = qmPackage.skinManagerClass?.callStaticMethod(hookInfo.skinManager.getSkinId) == "901" 18 | 19 | @get:StyleRes 20 | val themeIdForDialog: Int 21 | get() = if (isBlackSkinInUse) 22 | android.R.style.Theme_Material_Dialog_Alert 23 | else 24 | android.R.style.Theme_Material_Light_Dialog_Alert 25 | 26 | fun Activity.showMessageDialog( 27 | title: String, 28 | message: String, 29 | posText: String, 30 | negText: String = "", 31 | negClick: ButtonClickListener? = null, 32 | posClick: ButtonClickListener? = null, 33 | ): Dialog? { 34 | if (isDestroyed || isFinishing) return null 35 | return hookInfo.appStarterActivity.showMessageDialog.name.ifNotEmpty { showMethod -> 36 | callMethodOrNullAs( 37 | showMethod, 38 | title, 39 | message, 40 | posText, 41 | negText, 42 | posClick?.let { View.OnClickListener(it) }, 43 | negClick?.let { View.OnClickListener(it) }, 44 | false, 45 | false 46 | ) 47 | } ?: AlertDialog.Builder(this, themeIdForDialog) 48 | .setTitle(title) 49 | .setMessage(message) 50 | .setNegativeButton(negText, null) 51 | .setPositiveButton(posText, null) 52 | .create().apply { 53 | setOnShowListener { 54 | getButton(AlertDialog.BUTTON_NEGATIVE)?.let { button -> 55 | button.setOnClickListener { 56 | dismiss() 57 | negClick?.invoke(it) 58 | } 59 | } 60 | getButton(AlertDialog.BUTTON_POSITIVE)?.let { button -> 61 | button.setOnClickListener { 62 | dismiss() 63 | posClick?.invoke(it) 64 | } 65 | } 66 | } 67 | }.apply { show() } 68 | } 69 | 70 | suspend fun Activity.showMessageDialogX( 71 | title: String, 72 | message: String, 73 | posText: String, 74 | negText: String = "", 75 | ) = suspendCancellableCoroutine { cont -> 76 | val dialog = showMessageDialog( 77 | title, message, posText, negText, 78 | { cont.resume(false) }, 79 | { cont.resume(true) }, 80 | ) 81 | cont.invokeOnCancellation { dialog?.dismiss() } 82 | } 83 | 84 | fun Activity.showMessageDialog( 85 | @StringRes title: Int, 86 | @StringRes message: Int, 87 | @StringRes posText: Int, 88 | @StringRes negText: Int? = null, 89 | negClick: ButtonClickListener? = null, 90 | posClick: ButtonClickListener? = null, 91 | ) = showMessageDialog( 92 | string(title), 93 | string(message), 94 | string(posText), 95 | negText?.let { string(it) } ?: "", 96 | negClick, 97 | posClick, 98 | ) 99 | 100 | suspend fun Activity.showMessageDialogX( 101 | @StringRes title: Int, 102 | @StringRes message: Int, 103 | @StringRes posText: Int, 104 | @StringRes negText: Int? = null, 105 | ) = showMessageDialogX( 106 | string(title), 107 | string(message), 108 | string(posText), 109 | negText?.let { string(it) } ?: "", 110 | ) 111 | -------------------------------------------------------------------------------- /app/src/main/java/me/kofua/qmhelper/utils/uimode.kt: -------------------------------------------------------------------------------- 1 | package me.kofua.qmhelper.utils 2 | 3 | private const val KEY_UI_MODE = "KEY_SETTING_UI_MODE" 4 | 5 | enum class UiMode(val key: Int) { 6 | NORMAL(1), CLEAN(2), ELDER(3); 7 | 8 | companion object { 9 | fun of(key: Int) = values().find { it.key == key } ?: NORMAL 10 | } 11 | } 12 | 13 | val uiMode get() = UiMode.of(qmSp.getInt(KEY_UI_MODE, UiMode.NORMAL.key)) 14 | -------------------------------------------------------------------------------- /app/src/main/java/me/kofua/qmhelper/utils/utils.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("DEPRECATION") 2 | 3 | package me.kofua.qmhelper.utils 4 | 5 | import android.annotation.SuppressLint 6 | import android.app.AndroidAppHelper 7 | import android.content.* 8 | import android.content.pm.PackageManager 9 | import android.content.pm.PackageManager.NameNotFoundException 10 | import android.net.Uri 11 | import android.os.Environment 12 | import android.os.Handler 13 | import android.os.Looper 14 | import android.provider.DocumentsContract 15 | import android.util.Base64 16 | import androidx.annotation.ArrayRes 17 | import androidx.annotation.StringRes 18 | import kotlinx.coroutines.MainScope 19 | import me.kofua.qmhelper.XposedInit.Companion.modulePath 20 | import me.kofua.qmhelper.XposedInit.Companion.moduleRes 21 | import me.kofua.qmhelper.classLoader 22 | import me.kofua.qmhelper.from 23 | import me.kofua.qmhelper.hookInfo 24 | import org.json.JSONObject 25 | import java.io.File 26 | import java.lang.ref.WeakReference 27 | import kotlin.reflect.KProperty 28 | 29 | class Weak(val initializer: () -> T?) { 30 | private var weakReference: WeakReference? = null 31 | 32 | operator fun getValue(thisRef: Any?, property: KProperty<*>) = weakReference?.get() ?: let { 33 | weakReference = WeakReference(initializer()) 34 | weakReference 35 | }?.get() 36 | 37 | operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) { 38 | weakReference = WeakReference(value) 39 | } 40 | } 41 | 42 | val systemContext: Context 43 | get() { 44 | val activityThread = "android.app.ActivityThread".from(null) 45 | ?.callStaticMethod("currentActivityThread")!! 46 | return activityThread.callMethodAs("getSystemContext") 47 | } 48 | 49 | fun getPackageVersion(packageName: String) = try { 50 | systemContext.packageManager.getPackageInfo(packageName, 0).run { 51 | "${packageName}@%s(%s)".format(versionName, versionCode) 52 | } 53 | } catch (_: Throwable) { 54 | "(unknown)" 55 | } 56 | 57 | fun getVersionCode(packageName: String) = try { 58 | systemContext.packageManager.getPackageInfo(packageName, 0).versionCode 59 | } catch (_: Throwable) { 60 | -1 61 | } 62 | 63 | fun getPackageLastUpdateTime(packageName: String) = try { 64 | systemContext.packageManager.getPackageInfo(packageName, 0).lastUpdateTime 65 | } catch (_: Throwable) { 66 | 0 67 | } 68 | 69 | val currentContext by lazy { AndroidAppHelper.currentApplication() as Context } 70 | 71 | val hostPackageName: String by lazy { currentContext.packageName } 72 | 73 | val isBuiltIn get() = modulePath.endsWith("so") || modulePath.contains("lspatch") 74 | 75 | val is64 get() = currentContext.applicationInfo.nativeLibraryDir.contains("64") 76 | 77 | val logFile by lazy { File(currentContext.externalCacheDir, "log.txt") } 78 | 79 | val oldLogFile by lazy { File(currentContext.externalCacheDir, "old_log.txt") } 80 | 81 | val sPrefs 82 | get() = currentContext.getSharedPreferences("qmhelper", Context.MODE_MULTI_PROCESS)!! 83 | 84 | val sCaches 85 | get() = currentContext.getSharedPreferences("qmhelper_cache", Context.MODE_MULTI_PROCESS)!! 86 | 87 | @SuppressLint("DiscouragedApi") 88 | fun getResId(name: String, type: String) = 89 | currentContext.resources.getIdentifier(name, type, currentContext.packageName) 90 | 91 | val shouldSaveLog get() = sPrefs.getBoolean("save_log", true) 92 | 93 | fun Any?.reflexToString() = this?.javaClass?.declaredFields?.joinToString { 94 | "${it.name}: ${ 95 | it.run { isAccessible = true;get(this@reflexToString) } 96 | }" 97 | } 98 | 99 | fun string(@StringRes resId: Int) = currentContext.runCatchingOrNull { 100 | getString(resId) 101 | } ?: moduleRes.getString(resId) 102 | 103 | fun string(@StringRes resId: Int, vararg args: Any) = currentContext.runCatchingOrNull { 104 | getString(resId, *args) 105 | } ?: moduleRes.getString(resId, *args) 106 | 107 | fun stringArray(@ArrayRes resId: Int): Array = currentContext.resources.runCatchingOrNull { 108 | getStringArray(resId) 109 | } ?: moduleRes.getStringArray(resId) 110 | 111 | val qmSp by lazy { 112 | hookInfo.spManager.clazz.from(classLoader) 113 | ?.callStaticMethodAs(hookInfo.spManager.get) ?: sPrefs 114 | } 115 | val sessionCacheSp: SharedPreferences by lazy { 116 | currentContext.getSharedPreferences("MusicSessionCache", Context.MODE_MULTI_PROCESS) 117 | } 118 | 119 | val handler = Handler(Looper.getMainLooper()) 120 | val mainScope = MainScope() 121 | 122 | fun Handler.post(delayMills: Long = 0L, r: () -> Unit) = postDelayed(r, delayMills) 123 | 124 | @SuppressLint("ApplySharedPref") 125 | fun SharedPreferences.edit(commit: Boolean = false, action: SharedPreferences.Editor.() -> Unit) = 126 | edit().apply(action).run { if (commit) commit() else apply() } 127 | 128 | inline fun T.runCatchingOrNull(func: T.() -> R?) = try { 129 | func() 130 | } catch (e: Throwable) { 131 | null 132 | } 133 | 134 | fun Uri.realDirPath() = when (scheme) { 135 | null, ContentResolver.SCHEME_FILE -> path 136 | 137 | ContentResolver.SCHEME_CONTENT -> { 138 | if (authority == "com.android.externalstorage.documents") { 139 | val treeDocId = runCatchingOrNull { 140 | DocumentsContract.getTreeDocumentId(this) 141 | } ?: "" 142 | if (!treeDocId.contains(":")) { 143 | null 144 | } else { 145 | val type = treeDocId.substringBefore(':') 146 | val dirPath = treeDocId.substringAfter(':') 147 | val externalStorage = if (type == "primary") { 148 | Environment.getExternalStorageDirectory().absolutePath 149 | } else "/storage/$type" 150 | File(externalStorage, dirPath).absolutePath 151 | } 152 | } else null 153 | } 154 | 155 | else -> null 156 | } 157 | 158 | fun CharSequence.copyToClipboard(label: CharSequence = "") { 159 | ClipData.newPlainText(label, this)?.let { 160 | currentContext.getSystemService(ClipboardManager::class.java) 161 | .setPrimaryClip(it) 162 | } 163 | } 164 | 165 | inline fun C?.ifNotEmpty(action: (text: C) -> R) = 166 | if (!isNullOrEmpty()) action(this) else null 167 | 168 | fun String.toUri(): Uri = Uri.parse(this) 169 | 170 | fun Context.addModuleAssets() = assets.callMethod("addAssetPath", modulePath) 171 | 172 | fun isPackageInstalled(packageName: String) = try { 173 | systemContext.packageManager.getPackageInfo(packageName, 0) 174 | true 175 | } catch (_: NameNotFoundException) { 176 | false 177 | } 178 | 179 | fun isFakeSigEnabledFor(packageName: String): Boolean { 180 | try { 181 | val metaData = systemContext.packageManager 182 | .getApplicationInfo(packageName, PackageManager.GET_META_DATA) 183 | .metaData 184 | val encoded = metaData.getString("lspatch") 185 | if (encoded != null) { 186 | val json = Base64.decode(encoded, Base64.DEFAULT).toString(Charsets.UTF_8) 187 | val patchConfig = JSONObject(json) 188 | val sigBypassLevel = patchConfig.optInt("sigBypassLevel", -1) 189 | val lspVerCode = patchConfig.optJSONObject("lspConfig") 190 | ?.optInt("VERSION_CODE", -1) ?: -1 191 | if (sigBypassLevel >= 1 && lspVerCode >= 339) 192 | return true 193 | } 194 | } catch (_: Throwable) { 195 | } 196 | return false 197 | } 198 | -------------------------------------------------------------------------------- /app/src/main/java/me/kofua/qmhelper/utils/view.kt: -------------------------------------------------------------------------------- 1 | package me.kofua.qmhelper.utils 2 | 3 | import android.util.TypedValue 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import kotlin.math.roundToInt 7 | 8 | operator fun ViewGroup.iterator(): MutableIterator = object : MutableIterator { 9 | private var index = 0 10 | override fun hasNext() = index < childCount 11 | override fun next() = getChildAt(index++) ?: throw IndexOutOfBoundsException() 12 | override fun remove() = removeViewAt(--index) 13 | } 14 | 15 | val ViewGroup.children: Sequence 16 | get() = object : Sequence { 17 | override fun iterator() = this@children.iterator() 18 | } 19 | 20 | @Suppress("UNCHECKED_CAST") 21 | inline fun T.click(crossinline action: (v: T) -> Unit) = apply { 22 | setOnClickListener { action(it as T) } 23 | } 24 | 25 | @Suppress("UNCHECKED_CAST") 26 | inline fun T.longClick(crossinline action: (v: T) -> Boolean) = apply { 27 | setOnLongClickListener { action(it as T) } 28 | } 29 | 30 | fun View.addBackgroundRipple() = with(TypedValue()) { 31 | context.theme.resolveAttribute(android.R.attr.selectableItemBackground, this, true) 32 | setBackgroundResource(resourceId) 33 | } 34 | 35 | val Int.dp: Int 36 | inline get() = TypedValue.applyDimension( 37 | TypedValue.COMPLEX_UNIT_DIP, 38 | toFloat(), 39 | currentContext.resources.displayMetrics 40 | ).roundToInt() 41 | -------------------------------------------------------------------------------- /app/src/main/java/me/kofua/qmhelper/utils/xposed.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UNUSED", "UNCHECKED_CAST") 2 | 3 | package me.kofua.qmhelper.utils 4 | 5 | import android.annotation.SuppressLint 6 | import android.content.res.XResources 7 | import dalvik.system.BaseDexClassLoader 8 | import de.robv.android.xposed.XC_MethodHook 9 | import de.robv.android.xposed.XC_MethodHook.MethodHookParam 10 | import de.robv.android.xposed.XC_MethodReplacement 11 | import de.robv.android.xposed.XposedBridge.* 12 | import de.robv.android.xposed.XposedHelpers.* 13 | import de.robv.android.xposed.callbacks.XC_LayoutInflated 14 | import java.lang.reflect.Field 15 | import java.lang.reflect.Member 16 | import java.lang.reflect.Method 17 | import java.lang.reflect.Modifier 18 | import java.util.Enumeration 19 | 20 | typealias MethodHookParam = MethodHookParam 21 | typealias Replacer = (param: MethodHookParam) -> Any? 22 | typealias Hooker = (param: MethodHookParam) -> Unit 23 | 24 | fun Class<*>.hookMethod(method: String?, vararg args: Any?) = try { 25 | findAndHookMethod(this, method, *args) 26 | } catch (e: NoSuchMethodError) { 27 | Log.e(e) 28 | null 29 | } catch (e: ClassNotFoundError) { 30 | Log.e(e) 31 | null 32 | } catch (e: ClassNotFoundException) { 33 | Log.e(e) 34 | null 35 | } 36 | 37 | fun Member.hook(callback: XC_MethodHook) = try { 38 | hookMethod(this, callback) 39 | } catch (e: Throwable) { 40 | Log.e(e) 41 | null 42 | } 43 | 44 | inline fun MethodHookParam.callHooker(crossinline hooker: Hooker) = try { 45 | hooker(this) 46 | } catch (e: Throwable) { 47 | Log.e("Error occurred calling hooker on ${this.method}") 48 | Log.e(e) 49 | } 50 | 51 | inline fun MethodHookParam.callReplacer(crossinline replacer: Replacer) = try { 52 | replacer(this) 53 | } catch (e: Throwable) { 54 | Log.e("Error occurred calling replacer on ${this.method}") 55 | Log.e(e) 56 | null 57 | } 58 | 59 | inline fun Member.replace(crossinline replacer: Replacer) = hook(object : XC_MethodReplacement() { 60 | override fun replaceHookedMethod(param: MethodHookParam) = param.callReplacer(replacer) 61 | }) 62 | 63 | inline fun Member.hookAfter(crossinline hooker: Hooker) = hook(object : XC_MethodHook() { 64 | override fun afterHookedMethod(param: MethodHookParam) = param.callHooker(hooker) 65 | }) 66 | 67 | inline fun Member.hookBefore(crossinline hooker: Hooker) = hook(object : XC_MethodHook() { 68 | override fun beforeHookedMethod(param: MethodHookParam) = param.callHooker(hooker) 69 | }) 70 | 71 | inline fun Class<*>.hookBeforeMethod( 72 | method: String?, 73 | vararg args: Any?, 74 | crossinline hooker: Hooker 75 | ) = hookMethod(method, *args, object : XC_MethodHook() { 76 | override fun beforeHookedMethod(param: MethodHookParam) = param.callHooker(hooker) 77 | }) 78 | 79 | inline fun Class<*>.hookAfterMethod( 80 | method: String?, 81 | vararg args: Any?, 82 | crossinline hooker: Hooker 83 | ) = hookMethod(method, *args, object : XC_MethodHook() { 84 | override fun afterHookedMethod(param: MethodHookParam) = param.callHooker(hooker) 85 | }) 86 | 87 | inline fun Class<*>.hookAfterMethodWithPriority( 88 | method: String?, 89 | priority: Int, 90 | vararg args: Any?, 91 | crossinline hooker: Hooker 92 | ) = hookMethod(method, *args, object : XC_MethodHook(priority) { 93 | override fun afterHookedMethod(param: MethodHookParam) = param.callHooker(hooker) 94 | }) 95 | 96 | inline fun Class<*>.replaceMethod( 97 | method: String?, 98 | vararg args: Any?, 99 | crossinline replacer: Replacer 100 | ) = hookMethod(method, *args, object : XC_MethodReplacement() { 101 | override fun replaceHookedMethod(param: MethodHookParam) = param.callReplacer(replacer) 102 | }) 103 | 104 | fun Class<*>.hookAllMethods(methodName: String?, hooker: XC_MethodHook): Set = 105 | try { 106 | hookAllMethods(this, methodName, hooker) 107 | } catch (e: NoSuchMethodError) { 108 | Log.e(e) 109 | emptySet() 110 | } catch (e: ClassNotFoundError) { 111 | Log.e(e) 112 | emptySet() 113 | } catch (e: ClassNotFoundException) { 114 | Log.e(e) 115 | emptySet() 116 | } 117 | 118 | inline fun Class<*>.hookBeforeAllMethods(methodName: String?, crossinline hooker: Hooker) = 119 | hookAllMethods(methodName, object : XC_MethodHook() { 120 | override fun beforeHookedMethod(param: MethodHookParam) = param.callHooker(hooker) 121 | }) 122 | 123 | inline fun Class<*>.hookAfterAllMethods(methodName: String?, crossinline hooker: Hooker) = 124 | hookAllMethods(methodName, object : XC_MethodHook() { 125 | override fun afterHookedMethod(param: MethodHookParam) = param.callHooker(hooker) 126 | }) 127 | 128 | inline fun Class<*>.replaceAllMethods(methodName: String?, crossinline replacer: Replacer) = 129 | hookAllMethods(methodName, object : XC_MethodReplacement() { 130 | override fun replaceHookedMethod(param: MethodHookParam) = param.callReplacer(replacer) 131 | }) 132 | 133 | fun Class<*>.hookConstructor(vararg args: Any?) = try { 134 | findAndHookConstructor(this, *args) 135 | } catch (e: NoSuchMethodError) { 136 | Log.e(e) 137 | null 138 | } catch (e: ClassNotFoundError) { 139 | Log.e(e) 140 | null 141 | } catch (e: ClassNotFoundException) { 142 | Log.e(e) 143 | null 144 | } 145 | 146 | inline fun Class<*>.hookBeforeConstructor(vararg args: Any?, crossinline hooker: Hooker) = 147 | hookConstructor(*args, object : XC_MethodHook() { 148 | override fun beforeHookedMethod(param: MethodHookParam) = param.callHooker(hooker) 149 | }) 150 | 151 | inline fun Class<*>.hookAfterConstructor(vararg args: Any?, crossinline hooker: Hooker) = 152 | hookConstructor(*args, object : XC_MethodHook() { 153 | override fun afterHookedMethod(param: MethodHookParam) = param.callHooker(hooker) 154 | }) 155 | 156 | inline fun Class<*>.replaceConstructor(vararg args: Any?, crossinline hooker: Hooker) = 157 | hookConstructor(*args, object : XC_MethodReplacement() { 158 | override fun replaceHookedMethod(param: MethodHookParam) = param.callHooker(hooker) 159 | }) 160 | 161 | fun Class<*>.hookAllConstructors(hooker: XC_MethodHook): Set = try { 162 | hookAllConstructors(this, hooker) 163 | } catch (e: NoSuchMethodError) { 164 | Log.e(e) 165 | emptySet() 166 | } catch (e: ClassNotFoundError) { 167 | Log.e(e) 168 | emptySet() 169 | } catch (e: ClassNotFoundException) { 170 | Log.e(e) 171 | emptySet() 172 | } 173 | 174 | inline fun Class<*>.hookAfterAllConstructors(crossinline hooker: Hooker) = 175 | hookAllConstructors(object : XC_MethodHook() { 176 | override fun afterHookedMethod(param: MethodHookParam) = param.callHooker(hooker) 177 | }) 178 | 179 | inline fun Class<*>.hookBeforeAllConstructors(crossinline hooker: Hooker) = 180 | hookAllConstructors(object : XC_MethodHook() { 181 | override fun beforeHookedMethod(param: MethodHookParam) = param.callHooker(hooker) 182 | }) 183 | 184 | inline fun Class<*>.replaceAllConstructors(crossinline hooker: Hooker) = 185 | hookAllConstructors(object : XC_MethodReplacement() { 186 | override fun replaceHookedMethod(param: MethodHookParam) = param.callHooker(hooker) 187 | }) 188 | 189 | fun String.hookMethod(classLoader: ClassLoader, method: String?, vararg args: Any?) = try { 190 | on(classLoader).hookMethod(method, *args) 191 | } catch (e: ClassNotFoundError) { 192 | Log.e(e) 193 | null 194 | } catch (e: ClassNotFoundException) { 195 | Log.e(e) 196 | null 197 | } 198 | 199 | inline fun String.hookBeforeMethod( 200 | classLoader: ClassLoader, 201 | method: String?, 202 | vararg args: Any?, 203 | crossinline hooker: Hooker 204 | ) = try { 205 | on(classLoader).hookBeforeMethod(method, *args, hooker = hooker) 206 | } catch (e: ClassNotFoundError) { 207 | Log.e(e) 208 | null 209 | } catch (e: ClassNotFoundException) { 210 | Log.e(e) 211 | null 212 | } 213 | 214 | inline fun String.hookAfterMethod( 215 | classLoader: ClassLoader, 216 | method: String?, 217 | vararg args: Any?, 218 | crossinline hooker: Hooker 219 | ) = try { 220 | on(classLoader).hookAfterMethod(method, *args, hooker = hooker) 221 | } catch (e: ClassNotFoundError) { 222 | Log.e(e) 223 | null 224 | } catch (e: ClassNotFoundException) { 225 | Log.e(e) 226 | null 227 | } 228 | 229 | inline fun String.replaceMethod( 230 | classLoader: ClassLoader, 231 | method: String?, 232 | vararg args: Any?, 233 | crossinline replacer: Replacer 234 | ) = try { 235 | on(classLoader).replaceMethod(method, *args, replacer = replacer) 236 | } catch (e: ClassNotFoundError) { 237 | Log.e(e) 238 | null 239 | } catch (e: ClassNotFoundException) { 240 | Log.e(e) 241 | null 242 | } 243 | 244 | fun MethodHookParam.invokeOriginalMethod(): Any? = invokeOriginalMethod(method, thisObject, args) 245 | 246 | fun Any.getObjectField(field: String?): Any? = getObjectField(this, field) 247 | 248 | fun Any.getObjectFieldOrNull(field: String?): Any? = runCatchingOrNull { 249 | getObjectField(this, field) 250 | } 251 | 252 | fun Any.getObjectFieldAs(field: String?) = getObjectField(this, field) as T 253 | 254 | fun Any.getObjectFieldOrNullAs(field: String?) = runCatchingOrNull { 255 | getObjectField(this, field) as T 256 | } 257 | 258 | fun Any.getIntField(field: String?) = getIntField(this, field) 259 | 260 | fun Any.getIntFieldOrNull(field: String?) = runCatchingOrNull { 261 | getIntField(this, field) 262 | } 263 | 264 | fun Any.getLongField(field: String?) = getLongField(this, field) 265 | 266 | fun Any.getLongFieldOrNull(field: String?) = runCatchingOrNull { 267 | getLongField(this, field) 268 | } 269 | 270 | fun Any.getBooleanField(field: String?) = getBooleanField(this, field) 271 | 272 | fun Any.getBooleanFieldOrNull(field: String?) = runCatchingOrNull { 273 | getBooleanField(this, field) 274 | } 275 | 276 | fun Any.callMethod(methodName: String?, vararg args: Any?): Any? = 277 | callMethod(this, methodName, *args) 278 | 279 | fun Any.callMethodOrNull(methodName: String?, vararg args: Any?): Any? = runCatchingOrNull { 280 | callMethod(this, methodName, *args) 281 | } 282 | 283 | fun Class<*>.callStaticMethod(methodName: String?, vararg args: Any?): Any? = 284 | callStaticMethod(this, methodName, *args) 285 | 286 | fun Class<*>.callStaticMethodOrNull(methodName: String?, vararg args: Any?): Any? = 287 | runCatchingOrNull { 288 | callStaticMethod(this, methodName, *args) 289 | } 290 | 291 | fun Class<*>.callStaticMethodAs(methodName: String?, vararg args: Any?) = 292 | callStaticMethod(this, methodName, *args) as T 293 | 294 | fun Class<*>.callStaticMethodOrNullAs(methodName: String?, vararg args: Any?) = 295 | runCatchingOrNull { 296 | callStaticMethod(this, methodName, *args) as T 297 | } 298 | 299 | fun Class<*>.getStaticObjectFieldAs(field: String?) = getStaticObjectField(this, field) as T 300 | 301 | fun Class<*>.getStaticObjectFieldOrNullAs(field: String?) = runCatchingOrNull { 302 | getStaticObjectField(this, field) as T 303 | } 304 | 305 | fun Class<*>.getStaticObjectField(field: String?): Any? = getStaticObjectField(this, field) 306 | 307 | fun Class<*>.getStaticObjectFieldOrNull(field: String?): Any? = runCatchingOrNull { 308 | getStaticObjectField(this, field) 309 | } 310 | 311 | fun Class<*>.setStaticObjectField(field: String?, obj: Any?) = apply { 312 | setStaticObjectField(this, field, obj) 313 | } 314 | 315 | fun Class<*>.setStaticObjectFieldIfExist(field: String?, obj: Any?) = apply { 316 | try { 317 | setStaticObjectField(this, field, obj) 318 | } catch (ignored: Throwable) { 319 | } 320 | } 321 | 322 | inline fun Class<*>.findFieldByExactType(): Field? = 323 | findFirstFieldByExactType(this, T::class.java) 324 | 325 | fun Class<*>.findFieldByExactType(type: Class<*>): Field? = 326 | findFirstFieldByExactType(this, type) 327 | 328 | fun Any.callMethodAs(methodName: String?, vararg args: Any?) = 329 | callMethod(this, methodName, *args) as T 330 | 331 | fun Any.callMethodOrNullAs(methodName: String?, vararg args: Any?) = runCatchingOrNull { 332 | callMethod(this, methodName, *args) as T 333 | } 334 | 335 | fun Any.callMethod(methodName: String?, parameterTypes: Array>, vararg args: Any?): Any? = 336 | callMethod(this, methodName, parameterTypes, *args) 337 | 338 | fun Any.callMethodOrNull( 339 | methodName: String?, 340 | parameterTypes: Array>, 341 | vararg args: Any? 342 | ): Any? = runCatchingOrNull { 343 | callMethod(this, methodName, parameterTypes, *args) 344 | } 345 | 346 | fun Class<*>.callStaticMethod( 347 | methodName: String?, 348 | parameterTypes: Array>, 349 | vararg args: Any? 350 | ): Any? = callStaticMethod(this, methodName, parameterTypes, *args) 351 | 352 | fun Class<*>.callStaticMethodOrNull( 353 | methodName: String?, 354 | parameterTypes: Array>, 355 | vararg args: Any? 356 | ): Any? = runCatchingOrNull { 357 | callStaticMethod(this, methodName, parameterTypes, *args) 358 | } 359 | 360 | infix fun String.on(classLoader: ClassLoader?): Class<*> = findClass(this, classLoader) 361 | 362 | infix fun String.from(classLoader: ClassLoader?): Class<*>? = 363 | findClassIfExists(this, classLoader) 364 | 365 | fun Class<*>.new(vararg args: Any?): Any = newInstance(this, *args) 366 | 367 | fun Class<*>.new(parameterTypes: Array>, vararg args: Any?): Any = 368 | newInstance(this, parameterTypes, *args) 369 | 370 | fun Class<*>.findField(field: String?): Field = findField(this, field) 371 | 372 | fun Class<*>.findFieldOrNull(field: String?): Field? = findFieldIfExists(this, field) 373 | 374 | fun T.setIntField(field: String?, value: Int) = apply { 375 | setIntField(this, field, value) 376 | } 377 | 378 | fun T.setLongField(field: String?, value: Long) = apply { 379 | setLongField(this, field, value) 380 | } 381 | 382 | fun T.setObjectField(field: String?, value: Any?) = apply { 383 | setObjectField(this, field, value) 384 | } 385 | 386 | fun T.setBooleanField(field: String?, value: Boolean) = apply { 387 | setBooleanField(this, field, value) 388 | } 389 | 390 | inline fun XResources.hookLayout( 391 | id: Int, 392 | crossinline hooker: (XC_LayoutInflated.LayoutInflatedParam) -> Unit 393 | ) { 394 | try { 395 | hookLayout(id, object : XC_LayoutInflated() { 396 | override fun handleLayoutInflated(liparam: LayoutInflatedParam) { 397 | try { 398 | hooker(liparam) 399 | } catch (e: Throwable) { 400 | Log.e(e) 401 | } 402 | } 403 | }) 404 | } catch (e: Throwable) { 405 | Log.e(e) 406 | } 407 | } 408 | 409 | @SuppressLint("DiscouragedApi") 410 | inline fun XResources.hookLayout( 411 | pkg: String, 412 | type: String, 413 | name: String, 414 | crossinline hooker: (XC_LayoutInflated.LayoutInflatedParam) -> Unit 415 | ) { 416 | try { 417 | val id = getIdentifier(name, type, pkg) 418 | hookLayout(id, hooker) 419 | } catch (e: Throwable) { 420 | Log.e(e) 421 | } 422 | } 423 | 424 | fun Class<*>.findFirstFieldByExactType(type: Class<*>): Field = 425 | findFirstFieldByExactType(this, type) 426 | 427 | fun Class<*>.findFirstFieldByExactTypeOrNull(type: Class<*>?): Field? = runCatchingOrNull { 428 | findFirstFieldByExactType(this, type) 429 | } 430 | 431 | fun Any.getFirstFieldByExactType(type: Class<*>): Any? = 432 | javaClass.findFirstFieldByExactType(type).get(this) 433 | 434 | fun Any.getFirstFieldByExactTypeAs(type: Class<*>) = 435 | javaClass.findFirstFieldByExactType(type).get(this) as? T 436 | 437 | inline fun Any.getFirstFieldByExactType() = 438 | javaClass.findFirstFieldByExactType(T::class.java).get(this) as? T 439 | 440 | fun Any.getFirstFieldByExactTypeOrNull(type: Class<*>?): Any? = runCatchingOrNull { 441 | javaClass.findFirstFieldByExactTypeOrNull(type)?.get(this) 442 | } 443 | 444 | fun Any.getFirstFieldByExactTypeOrNullAs(type: Class<*>?) = 445 | getFirstFieldByExactTypeOrNull(type) as? T 446 | 447 | inline fun Any.getFirstFieldByExactTypeOrNull() = 448 | getFirstFieldByExactTypeOrNull(T::class.java) as? T 449 | 450 | inline fun ClassLoader.findDexClassLoader(crossinline delegator: (BaseDexClassLoader) -> BaseDexClassLoader = { x -> x }): BaseDexClassLoader? { 451 | var classLoader = this 452 | while (classLoader !is BaseDexClassLoader) { 453 | if (classLoader.parent != null) classLoader = classLoader.parent 454 | else return null 455 | } 456 | return delegator(classLoader) 457 | } 458 | 459 | inline fun ClassLoader.allClassesList(crossinline delegator: (BaseDexClassLoader) -> BaseDexClassLoader = { x -> x }): List { 460 | return findDexClassLoader(delegator)?.getObjectField("pathList") 461 | ?.getObjectFieldAs>("dexElements") 462 | ?.flatMap { 463 | it.getObjectField("dexFile")?.callMethodAs>("entries") 464 | ?.toList().orEmpty() 465 | }.orEmpty() 466 | } 467 | 468 | fun Class<*>.findMethod(deep: Boolean = false, condition: (Method) -> Boolean): Method? { 469 | var c: Class<*>? = this 470 | while (c != null) { 471 | (c.declaredMethods.find(condition) ?: if (deep) c.interfaces 472 | .flatMap { it.declaredMethods.toList() } 473 | .find(condition) else null)?.also { 474 | it.isAccessible = true 475 | return it 476 | } ?: run { c = c?.superclass } 477 | } 478 | return null 479 | } 480 | 481 | val Member.isStatic: Boolean 482 | inline get() = Modifier.isStatic(modifiers) 483 | val Member.isFinal: Boolean 484 | inline get() = Modifier.isFinal(modifiers) 485 | val Member.isPublic: Boolean 486 | inline get() = Modifier.isPublic(modifiers) 487 | val Member.isNotStatic: Boolean 488 | inline get() = !isStatic 489 | val Member.isAbstract: Boolean 490 | inline get() = Modifier.isAbstract(modifiers) 491 | val Member.isPrivate: Boolean 492 | inline get() = Modifier.isPrivate(modifiers) 493 | val Class<*>.isAbstract: Boolean 494 | inline get() = !isPrimitive && Modifier.isAbstract(modifiers) 495 | -------------------------------------------------------------------------------- /app/src/main/java/me/kofua/qmhelper/utils/xposedx.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UNUSED", "UNCHECKED_CAST") 2 | 3 | package me.kofua.qmhelper.utils 4 | 5 | import de.robv.android.xposed.XposedHelpers.* 6 | import me.kofua.qmhelper.classLoader 7 | import me.kofua.qmhelper.data.ClassInfo 8 | import me.kofua.qmhelper.data.Field 9 | import me.kofua.qmhelper.data.Method 10 | import me.kofua.qmhelper.from 11 | 12 | inline fun T.hookBeforeMethod( 13 | method: T.() -> Method, 14 | vararg exArgs: Any?, 15 | crossinline hooker: Hooker 16 | ) = method().let { 17 | clazz.from(classLoader)?.hookBeforeMethod(it.name, *it.paramTypes, *exArgs, hooker = hooker) 18 | } 19 | 20 | inline fun T.hookAfterMethod( 21 | method: T.() -> Method, 22 | vararg exArgs: Any?, 23 | crossinline hooker: Hooker 24 | ) = method().let { 25 | clazz.from(classLoader)?.hookAfterMethod(it.name, *it.paramTypes, *exArgs, hooker = hooker) 26 | } 27 | 28 | inline fun T.replaceMethod( 29 | method: T.() -> Method, 30 | vararg exArgs: Any?, 31 | crossinline replacer: Replacer 32 | ) = method().let { 33 | clazz.from(classLoader)?.replaceMethod(it.name, *it.paramTypes, *exArgs, replacer = replacer) 34 | } 35 | 36 | fun Any.getObjectField(field: Field): Any? = getObjectField(this, field.name) 37 | 38 | fun Any.getObjectFieldOrNull(field: Field): Any? = runCatchingOrNull { 39 | getObjectField(this, field.name) 40 | } 41 | 42 | fun Any.getObjectFieldAs(field: Field) = getObjectField(this, field.name) as T 43 | 44 | fun Any.getObjectFieldOrNullAs(field: Field) = runCatchingOrNull { 45 | getObjectField(this, field.name) as T 46 | } 47 | 48 | fun Any.getIntField(field: Field) = getIntField(this, field.name) 49 | 50 | fun Any.getIntFieldOrNull(field: Field) = runCatchingOrNull { 51 | getIntField(this, field.name) 52 | } 53 | 54 | fun Any.getLongField(field: Field) = getLongField(this, field.name) 55 | 56 | fun Any.getLongFieldOrNull(field: Field) = runCatchingOrNull { 57 | getLongField(this, field.name) 58 | } 59 | 60 | fun Any.getBooleanField(field: Field) = getBooleanField(this, field.name) 61 | 62 | fun Any.getBooleanFieldOrNull(field: Field) = runCatchingOrNull { 63 | getBooleanField(this, field.name) 64 | } 65 | 66 | fun Any.callMethod(method: Method, vararg args: Any?): Any? = 67 | callMethod(this, method.name, *args) 68 | 69 | fun Any.callMethodOrNull(method: Method, vararg args: Any?): Any? = runCatchingOrNull { 70 | callMethod(this, method.name, *args) 71 | } 72 | 73 | fun Any.callMethodAs(method: Method, vararg args: Any?) = 74 | callMethod(this, method.name, *args) as T 75 | 76 | fun Any.callMethodOrNullAs(method: Method, vararg args: Any?) = runCatchingOrNull { 77 | callMethod(this, method.name, *args) as T 78 | } 79 | 80 | fun Class<*>.callStaticMethod(method: Method, vararg args: Any?): Any? = 81 | callStaticMethod(this, method.name, *args) 82 | 83 | fun Class<*>.callStaticMethodOrNull(method: Method, vararg args: Any?): Any? = 84 | runCatchingOrNull { 85 | callStaticMethod(this, method.name, *args) 86 | } 87 | 88 | fun Class<*>.callStaticMethodAs(method: Method, vararg args: Any?) = 89 | callStaticMethod(this, method.name, *args) as T 90 | 91 | fun Class<*>.callStaticMethodOrNullAs(method: Method, vararg args: Any?) = 92 | runCatchingOrNull { 93 | callStaticMethod(this, method.name, *args) as T 94 | } 95 | 96 | fun Class<*>.getStaticObjectFieldAs(field: Field) = 97 | getStaticObjectField(this, field.name) as T 98 | 99 | fun Class<*>.getStaticObjectFieldOrNullAs(field: Field) = runCatchingOrNull { 100 | getStaticObjectField(this, field.name) as T 101 | } 102 | 103 | fun Class<*>.getStaticObjectField(field: Field): Any? = 104 | getStaticObjectField(this, field.name) 105 | 106 | fun Class<*>.getStaticObjectFieldOrNull(field: Field): Any? = runCatchingOrNull { 107 | getStaticObjectField(this, field.name) 108 | } 109 | 110 | fun Class<*>.setStaticObjectField(field: Field, obj: Any?) = apply { 111 | setStaticObjectField(this, field.name, obj) 112 | } 113 | 114 | fun Class<*>.setStaticObjectFieldIfExist(field: Field, obj: Any?) = apply { 115 | try { 116 | setStaticObjectField(this, field.name, obj) 117 | } catch (ignored: Throwable) { 118 | } 119 | } 120 | 121 | fun T.setIntField(field: Field, value: Int) = apply { 122 | setIntField(this, field.name, value) 123 | } 124 | 125 | fun T.setLongField(field: Field, value: Long) = apply { 126 | setLongField(this, field.name, value) 127 | } 128 | 129 | fun T.setObjectField(field: Field, value: Any?) = apply { 130 | setObjectField(this, field.name, value) 131 | } 132 | 133 | fun T.setBooleanField(field: Field, value: Boolean) = apply { 134 | setBooleanField(this, field.name, value) 135 | } 136 | -------------------------------------------------------------------------------- /app/src/main/jni/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.4.1) 2 | project(qmhelper) 3 | 4 | find_package(cxx REQUIRED CONFIG) 5 | link_libraries(cxx::cxx) 6 | 7 | add_subdirectory(dex_builder) 8 | 9 | add_library(${PROJECT_NAME} SHARED qmhelper.cc) 10 | 11 | target_link_libraries(${PROJECT_NAME} PUBLIC log dex_builder_static) 12 | 13 | if (NOT DEFINED DEBUG_SYMBOLS_PATH) 14 | set(DEBUG_SYMBOLS_PATH ${CMAKE_BINARY_DIR}/symbols) 15 | endif() 16 | 17 | add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD 18 | COMMAND ${CMAKE_COMMAND} -E make_directory ${DEBUG_SYMBOLS_PATH}/${ANDROID_ABI} 19 | COMMAND ${CMAKE_OBJCOPY} --only-keep-debug $ 20 | ${DEBUG_SYMBOLS_PATH}/${ANDROID_ABI}/${PROJECT_NAME} 21 | COMMAND ${CMAKE_STRIP} --strip-all $) 22 | -------------------------------------------------------------------------------- /app/src/main/res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | com.tencent.qqmusic 5 | 6 | 7 | 8 | 乐馆 9 | 会员 10 | 有声 11 | 直播 12 | 13 | 14 | 6 15 | 20 16 | 7 17 | 10 18 | 19 | 20 | 21 | 音乐推 22 | 创作者中心 23 | 弹一弹 24 | 免流量服务 25 | 青少年模式 26 | 安全中心 27 | 帮助与反馈 28 | 注销帐号 29 | 腾讯视频VIP福利 30 | 百变彩铃 31 | 32 | 33 | 音乐推 34 | 创作者中心 35 | 弹一弹 36 | 免流量服务 37 | 青少年模式 38 | 安全中心 39 | 帮助与反馈 40 | 注销帐号 41 | 腾讯视频VIP福利 42 | 百变彩铃 43 | 44 | 45 | 46 | 滚动热词 47 | 搜索页推广 48 | 搜索页推荐热词 49 | 搜索页热词分类列表 50 | 51 | 52 | scroll 53 | ads 54 | rcmd 55 | rcmd-list 56 | 57 | 58 | 59 | 视频推荐 60 | 类型标签 61 | 62 | 63 | video 64 | genre 65 | 66 | 67 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Q音助手 3 | 还你一个干净的Q音 Android 客户端 4 | Q音助手设置 5 | 返回 6 | 重启客户端 7 | 配置修改后重启客户端才会生效 8 | 导入成功,是否立即重启客户端使之生效? 9 | 主要功能 10 | 净化开屏页面 11 | 去除客户端启动时的推广 12 | 备份与还原 13 | 导出配置 14 | 导入配置 15 | 设置 16 | 保存日志到文件 17 | 启用后,日志将保存到文件%s 18 | 分享日志 19 | 保存配置文件 20 | 选择配置文件 21 | 文件管理器打开失败 22 | 没有发现日志文件 23 | 日志文件不存在 24 | %1$s (崩溃相关发这个) 25 | 出现错误\n%1$s\n部分功能可能失效。 26 | 不支持该架构或框架,部分功能可能失效。 27 | 净化首页顶部标签 28 | 选择要隐藏的标签 29 | 净化红点 30 | 更多及设置页面不显示红点 31 | 净化更多页面 32 | 更多页面选择需要移除的项 33 | 隐藏音乐空间入口 34 | 我的页面隐藏音乐空间入口 35 | 隐藏会员购买横条 36 | 日推及我的页面隐藏会员购买横条 37 | 屏蔽直播入口 38 | 播放页右上角屏蔽直播入口 39 | 禁止上划查看视频 40 | 播放页禁止上划查看视频 41 | Q音助手提示 42 | 设置里开启简洁模式配合食用更佳哟! 43 | 前往设置 44 | 我知道了 45 | 跳转失败 46 | 精简与美化 47 | 关于 48 | 版本 49 | 检查更新 50 | 发现新版本,点击查看 51 | 发现新版本 %1$s 52 | 暂无更新 53 | 立即更新 54 | 更新检查失败 55 | https://api.github.com/repos/zjns/QMHelper/releases 56 | https://github.com/zjns/QMHelper/releases/tag/%1$s 57 | 作者 58 | Kofua 59 | https://github.com/zjns/QMHelper 60 | TG频道 61 | 点击加入TG频道 62 | https://t.me/bb_show 63 | 移除首页直播版块 64 | 净化搜索 65 | 隐藏推广横条 66 | 我的收藏页面隐藏推广横条 67 | 隐藏音乐空间 68 | 我的页面禁止下拉进入音乐空间 69 | 隐藏功能 70 | 启用隐藏功能 71 | 所有隐藏功能仅供学习交流,请低调使用 72 | 已开启隐藏功能,重启客户端后生效 73 | 已关闭隐藏功能,重启客户端后生效 74 | 再按%1$d次开启隐藏功能 75 | 解密已下载音乐 76 | 需要获取外置存储写入权限才能进一步操作,是否继续? 77 | 外置存储写入权限获取失败,请打开APP的系统权限设置页面手动开启权限。 78 | 请选择保存解密歌曲的文件夹 79 | 请选择有效的外置存储文件夹 80 | 解密完成 81 | 没有找到需要解密的音乐! 82 | 解密完成,共%1$d首加密音乐,全部解密成功。是否删除原文件? 83 | 解密完成,共%1$d首加密音乐,%2$d首解密成功,%3$d首解密失败。是否删除原文件? 84 | 85 | 86 | [%1$d/%2$d] %3$s 解密成功 87 | [%1$d/%2$d] %3$s 解密失败 88 | 屏蔽底部提示栏 89 | 屏蔽播放页底部提示栏 90 | 屏蔽封面推广 91 | 屏蔽播放页封面各种推广 92 | 屏蔽关注提醒 93 | 屏蔽播放页音乐人关注提醒 94 | 长按自由复制 95 | 长按评论/专辑简介/专辑详情弹出自由选择对话框 96 | 自由复制 97 | 分享 98 | 分享复制内容 99 | 复制全部 100 | 复制成功 101 | 屏蔽评论页顶部横幅 102 | 屏蔽评论页顶部推广横幅 103 | 移除评论列表推荐 104 | 移除评论列表直播等推荐内容 105 | 移除创作中心 106 | 我的页面移除创作者中心 107 | 杂项 108 | 修正歌曲下载文件名 109 | 歌曲下载文件名移除mqms后缀,长度限制增大到250 110 | 强制允许音乐下载到扩展SD卡 111 | 强制允许音乐下载到\"扩展SD卡/qqmusic/song\"目录,而不是\"扩展SD卡/Android/data/包名/files/qqmusic/song\"。实验性功能,可能导致音乐下载失败,谨慎开启。 112 | 屏蔽分享引导 113 | 隐藏播放页右上角分享引导动画图标 114 | 屏蔽所有三方推广 115 | 屏蔽所有带有标记的三方推广,涵盖轮播图、播放页封面、锁屏页等 116 | 禁用律动光效 117 | 禁用播放页律动光效 118 | 下移最近播放 119 | 我的页面将最近播放位置下移到收藏歌单下 120 | 解锁所有装扮 121 | 自动签到 122 | APP启动后每天自动签到一次 123 | 自动签到完成! 124 | 解锁所有字体 125 | 解锁所有歌词及歌词海报字体 126 | 解锁所有歌词动效 127 | 隐藏“新建/导入歌单”引导 128 | 通过强制指定AB实验策略隐藏我的页面自建歌单“新建/导入歌单”引导项,可能失效 129 | 130 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | tasks.register("clean") { 4 | group = "build" 5 | rootProject.allprojects.forEach { 6 | delete(it.projectDir.resolve(".gradle")) 7 | delete(it.projectDir.resolve(".cxx")) 8 | delete(it.buildDir) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.enableAppCompileTimeRClass=true 24 | android.experimental.enableNewResourceShrinker.preciseShrinking=true 25 | # app version 26 | buildWithGitSuffix=false 27 | appVerName=1.1.7 28 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | kotlin = "1.9.23" 3 | coroutine = "1.8.0" 4 | 5 | [plugins] 6 | kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 7 | agp-app = { id = "com.android.application", version = "8.4.0" } 8 | lsplugin-resopt = { id = "org.lsposed.lsplugin.resopt", version = "1.6" } 9 | lsplugin-cmaker = { id = "org.lsposed.lsplugin.cmaker", version = "1.2" } 10 | 11 | [libraries] 12 | xposed = { module = "de.robv.android.xposed:api", version = "82" } 13 | cxx = { module = "dev.rikka.ndk.thirdparty:cxx", version = "1.2.0" } 14 | dexmaker = { module = "com.linkedin.dexmaker:dexmaker", version = "2.28.3" } 15 | kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } 16 | kotlin-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutine" } 17 | kotlin-coroutines-jdk = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8", version.ref = "coroutine" } 18 | androidx-annotation = { module = "androidx.annotation:annotation", version = "1.7.1" } 19 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MengNianxiaoyao/QMHelper/69436bf0727f9be127d2bc0be2198d255981b687/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "automerge": true, 4 | "automergeType": "branch", 5 | "extends": [ 6 | "config:base" 7 | ], 8 | "dependencyDashboard": false 9 | } 10 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | rootProject.name = "QMHelper" 4 | include(":app") 5 | buildCache { local { removeUnusedEntriesAfterDays = 1 } } 6 | pluginManagement { 7 | repositories { 8 | google() 9 | mavenCentral() 10 | gradlePluginPortal() 11 | } 12 | } 13 | dependencyResolutionManagement { 14 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 15 | repositories { 16 | google() 17 | mavenCentral() 18 | maven(url = "https://api.xposed.info") 19 | } 20 | } 21 | --------------------------------------------------------------------------------