├── .github ├── ISSUE_TEMPLATE │ └── 反馈问题.md └── workflows │ ├── android.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ └── xposed_init │ ├── java │ └── one │ │ └── yufz │ │ └── hmspush │ │ └── app │ │ ├── App.kt │ │ ├── HmsPushClient.kt │ │ ├── MainActivity.kt │ │ ├── NavHost.kt │ │ ├── home │ │ ├── AppInfo.kt │ │ ├── AppListScreen.kt │ │ ├── AppListViewModel.kt │ │ ├── HomeScreen.kt │ │ ├── HomeViewModel.kt │ │ └── Util.kt │ │ ├── icon │ │ ├── IconScreen.kt │ │ └── IconViewModel.kt │ │ ├── settings │ │ ├── SettingsScreent.kt │ │ └── SettingsViewModel.kt │ │ ├── theme │ │ ├── Color.kt │ │ └── Theme.kt │ │ ├── util │ │ └── Context.kt │ │ └── widget │ │ ├── LifecycleAware.kt │ │ ├── LoadingDialog.kt │ │ └── SearchBar.kt │ └── res │ ├── drawable │ └── ic_launcher_foreground.xml │ ├── mipmap-anydpi-v26 │ └── ic_launcher.xml │ ├── values-night │ └── themes.xml │ ├── values-zh │ └── strings.xml │ └── values │ ├── arrays.xml │ ├── colors.xml │ ├── strings.xml │ └── themes.xml ├── build.gradle ├── common ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── aidl │ └── one │ │ └── yufz │ │ └── hmspush │ │ └── common │ │ ├── HmsPushInterface.aidl │ │ └── model │ │ └── models.aidl │ └── java │ └── one │ └── yufz │ └── hmspush │ └── common │ ├── BinderCursor.kt │ ├── BridgeUri.kt │ ├── BridgeWrap.kt │ ├── Constant.kt │ ├── Context.kt │ ├── HmsCoreUtil.kt │ ├── IconData.kt │ ├── Util.kt │ ├── content │ ├── Content.kt │ ├── ContentModel.kt │ ├── ContentProperties.kt │ ├── ContentValues.kt │ ├── Cursor.kt │ └── SharedPreference.kt │ └── model │ ├── IconModel.kt │ ├── ModuleVersionModel.kt │ ├── PrefsModel.kt │ ├── PushHistoryModel.kt │ └── PushSignModel.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── xposed ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src └── main ├── AndroidManifest.xml └── java └── one └── yufz ├── hmspush └── hook │ ├── I18n.kt │ ├── XLog.kt │ ├── XposedMod.kt │ ├── bridge │ ├── BridgeContentProvider.kt │ └── HookContentProvider.kt │ ├── fakedevice │ ├── Alipay.kt │ ├── Common.kt │ ├── CoolApk.kt │ ├── DouYin.kt │ ├── FakeDevice.kt │ ├── FakeEmuiOnly.kt │ ├── FakeHmsSignature.kt │ ├── FakeProperty.kt │ ├── HookHmsDeviceId.kt │ ├── IFakeDevice.kt │ ├── PinDuoDuo.kt │ ├── QQ.kt │ └── XGPush.kt │ ├── hms │ ├── FakeHsf.kt │ ├── HmsPushService.kt │ ├── HookForegroundService.kt │ ├── HookHMS.kt │ ├── HookLegacyTokenRequest.kt │ ├── HookPushNC.kt │ ├── Prefs.kt │ ├── PushHistory.kt │ ├── PushSignWatcher.kt │ ├── RuntimeKitHook.kt │ ├── StorageContext.kt │ ├── dummy │ │ ├── DummyFragment.kt │ │ ├── HookDummyActivity.kt │ │ └── HookDummyActivityTask.kt │ ├── icon │ │ └── IconManager.kt │ └── nm │ │ ├── INotificationManager.kt │ │ ├── NotificationManagerEx.kt │ │ ├── SelfNotificationManager.kt │ │ ├── SystemNotificationManager.kt │ │ └── handler │ │ ├── FinalHandler.kt │ │ ├── GroupByIdHandler.kt │ │ ├── GroupNotificationHandler.kt │ │ ├── IconHandler.kt │ │ ├── LabelHandler.kt │ │ ├── NotificationHandler.kt │ │ └── NotificationHandlers.kt │ ├── system │ ├── HookSystemService.kt │ ├── KeepHmsAlive.kt │ ├── NmsPermissionHooker.kt │ └── ShortcutPermissionHooker.kt │ └── util │ ├── Notification.kt │ └── Util.kt └── xposed ├── Android.kt ├── Layout.kt └── XPosedX.kt /.github/ISSUE_TEMPLATE/反馈问题.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 反馈问题 3 | about: Create a bug report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **描述问题** 11 | 在此描述你的问题 12 | 13 | 14 | **运行环境** 15 | 比如 LSPosed / LSPatch 16 | 17 | **LSPosed 版本** 18 | >比如 1.8.5(6695),请提供详细版本号,不要使用`最新`等词汇,下同 19 | 20 | **HMS Core 版本** 21 | 22 | 23 | **HMS Push 版本** 24 | 25 | 26 | **问题应用版本** 27 | 28 | 29 | **问题应用下载渠道** 30 | 31 | 32 | **LSPosed 日志** 33 | -------------------------------------------------------------------------------- /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ master, develop, alpha ] 7 | pull_request: 8 | branches: [ master, develop, alpha ] 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: set up JDK 17 21 | uses: actions/setup-java@v4 22 | with: 23 | java-version: '17' 24 | distribution: 'adopt' 25 | cache: gradle 26 | 27 | - name: Write key 28 | run: | 29 | if [ ! -z "${{ secrets.SIGNING_KEY }}" ]; then 30 | echo STORE_PASSWORD='${{ secrets.KEY_STORE_PASSWORD }}' >> local.properties 31 | echo KEY_ALIAS='${{ secrets.ALIAS }}' >> local.properties 32 | echo KEY_PASSWORD='${{ secrets.KEY_PASSWORD }}' >> local.properties 33 | echo STORE_FILE_PATH='../release.keystore' >> local.properties 34 | echo ${{ secrets.SIGNING_KEY }} | base64 --decode > release.keystore 35 | fi 36 | 37 | - name: Grant execute permission for gradlew 38 | run: chmod +x gradlew 39 | - name: Build with Gradle 40 | run: ./gradlew assemble 41 | 42 | - name: Collect artifcat name 43 | run: | 44 | echo "debug_artifact=$(basename -s .apk app/build/outputs/apk/debug/*.apk)" >> $GITHUB_ENV 45 | echo "release_artifact=$(basename -s .apk app/build/outputs/apk/release/*.apk)" >> $GITHUB_ENV 46 | 47 | - name: Upload Debug 48 | uses: actions/upload-artifact@v4 49 | with: 50 | name: ${{ env.debug_artifact }} 51 | path: app/build/outputs/apk/debug/*.apk 52 | 53 | - name: Upload Release 54 | uses: actions/upload-artifact@v4 55 | with: 56 | name: ${{ env.release_artifact }} 57 | path: app/build/outputs/apk/release/*.apk 58 | 59 | # https://github.com/LSPosed/LSPosed/blob/594604423c548c5a3596dc590f52480db7aabeaf/.github/workflows/core.yml#L112 60 | - name: Post to Telegram 61 | if: ${{ github.event_name != 'pull_request' && success() && github.ref == 'refs/heads/master' }} 62 | env: 63 | CHANNEL_ID: ${{ secrets.CHANNEL_ID }} 64 | BOT_TOKEN: ${{ secrets.BOT_TOKEN }} 65 | COMMIT_MESSAGE: ${{ github.event.head_commit.message }} 66 | COMMIT_URL: ${{ github.event.head_commit.url }} 67 | run: | 68 | if [ ! -z "${{ secrets.BOT_TOKEN }}" ]; then 69 | export apkRelease=$(find app/build/outputs/apk/release -name "*.apk") 70 | export apkDebug=$(find app/build/outputs/apk/debug -name "*.apk") 71 | ESCAPED=`python3 -c 'import json,os,urllib.parse; msg = json.dumps(os.environ["COMMIT_MESSAGE"]); print(urllib.parse.quote(msg if len(msg) <= 1024 else json.dumps(os.environ["COMMIT_URL"])))'` 72 | curl -v "https://api.telegram.org/bot${BOT_TOKEN}/sendMediaGroup?chat_id=${CHANNEL_ID}&media=%5B%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2FapkRelease%22%7D%2C%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2FapkDebug%22%2C%22caption%22:${ESCAPED}%7D%5D" -F apkRelease="@$apkRelease" -F apkDebug="@$apkDebug" 73 | fi 74 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release CI 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Force fetch Tags 19 | run: | 20 | git fetch --tags --force 21 | 22 | - name: Get Tag 23 | id: var 24 | run: | 25 | echo ::set-output name=tag::${GITHUB_REF#refs/*/} 26 | echo ::set-output name=version::${GITHUB_REF#refs/*/v} 27 | 28 | - name: set up JDK 17 29 | uses: actions/setup-java@v4 30 | with: 31 | java-version: '17' 32 | distribution: 'adopt' 33 | cache: gradle 34 | 35 | - name: Write key 36 | run: | 37 | if [ ! -z "${{ secrets.SIGNING_KEY }}" ]; then 38 | echo STORE_PASSWORD='${{ secrets.KEY_STORE_PASSWORD }}' >> local.properties 39 | echo KEY_ALIAS='${{ secrets.ALIAS }}' >> local.properties 40 | echo KEY_PASSWORD='${{ secrets.KEY_PASSWORD }}' >> local.properties 41 | echo STORE_FILE_PATH='../release.keystore' >> local.properties 42 | echo ${{ secrets.SIGNING_KEY }} | base64 --decode > release.keystore 43 | fi 44 | 45 | - name: Grant execute permission for gradlew 46 | run: chmod +x gradlew 47 | - name: Build with Gradle 48 | run: ./gradlew assembleRelease 49 | 50 | - name: Collect artifcat name 51 | run: | 52 | echo "release_artifact=$(basename -s .apk app/build/outputs/apk/release/*.apk)" >> $GITHUB_ENV 53 | 54 | - name: Upload a Build Artifact 55 | uses: actions/upload-artifact@v4 56 | with: 57 | name: ${{ env.release_artifact }} 58 | path: app/build/outputs/apk/release/*.apk 59 | 60 | - name: Upload Mapping 61 | uses: actions/upload-artifact@v4 62 | with: 63 | name: mapping 64 | path: app/build/outputs/mapping/release/mapping.txt 65 | 66 | - uses: ericcornelissen/git-tag-annotation-action@v2 67 | id: tag-data 68 | 69 | - name: Create Release 70 | uses: ncipollo/release-action@v1 71 | with: 72 | tag: ${{ steps.var.outputs.tag }} 73 | token: ${{ secrets.GH_TOKEN }} 74 | body: ${{ steps.tag-data.outputs.git-tag-annotation }} 75 | artifacts: "app/build/outputs/apk/release/*.apk,app/build/outputs/mapping/release/mapping.txt" 76 | allowUpdates: true 77 | removeArtifacts: true 78 | 79 | - name: Draft Release to XPosed Repo 80 | uses: ncipollo/release-action@v1 81 | with: 82 | name: ${{ steps.var.outputs.tag }} 83 | tag: ${{ steps.var.outputs.version }} 84 | commit: main 85 | owner: Xposed-Modules-Repo 86 | repo: one.yufz.hmspush 87 | token: ${{ secrets.GH_TOKEN }} 88 | body: ${{ steps.tag-data.outputs.git-tag-annotation }} 89 | artifacts: "app/build/outputs/apk/release/*.apk" 90 | draft: true 91 | allowUpdates: true 92 | removeArtifacts: true 93 | 94 | - name: Release to XPosed Repo 95 | uses: ncipollo/release-action@v1 96 | with: 97 | tag: ${{ steps.var.outputs.version }} 98 | allowUpdates: true 99 | owner: Xposed-Modules-Repo 100 | repo: one.yufz.hmspush 101 | token: ${{ secrets.GH_TOKEN }} 102 | omitNameDuringUpdate: true 103 | omitBodyDuringUpdate: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .idea/ 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | app/release/ 12 | *.keystore 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HMS Push 2 | 3 | 4 | HMS Core 是华为提供的一套服务,其中包含了推送功能,可以在华为和非华为设备上使用, 5 | 6 | 但是在非华为设备上由于缺乏系统服务配合,只能唤醒目标应用让其自己弹出通知 7 | 8 | 同时大部分应用在非华为设备上不会主动启用 HMS 推送服务 9 | 10 | 该模块借助 [LSPosed](https://github.com/LSPosed/LSPosed) 为 HMS Core 提供发送系统通知的能力, 11 | 同时支持将应用运行环境伪装成华为设备,以此来实现无后台系统级别的推送通道。 12 | 13 | > **Warning** 14 | > 对应用进行设备伪装会导致应用环境异常,从而导致封号等后果,请自行承担使用风险! 15 | 16 | ### 安装步骤: 17 | - 从应用市场下载并安装 `HMS Core`,比如 [腾讯应用宝](https://sj.qq.com/appdetail/com.huawei.hwid)、[酷安](https://www.coolapk.com/apk/com.huawei.hwid)、[APKMirror](https://www.apkmirror.com/apk/huawei-internet-services/huawei-mobile-services) 18 | 19 | - 下载最新版本 HMS Push 安装,在 LSPosed 中启用 HMSPush 模块,并勾选 「系统框架」、「HMS Core 」作用域,然后重启设备,[下载地址](https://github.com/fei-ke/HMSPush/releases/latest) 20 | 21 | - LSPosed 里 HMSPush 模块里勾选你需要支持推送的目标应用(这一步目的是将应用环境伪装成华为设备,如果你使用了其他方式伪装设备,可以不进行这一步),然后重启一到两次目标应用使其注册上推送通道 22 | 23 | - 杀掉应用测试推送是否生效(可以使用QQ测试) 24 |    25 | ### 注意: 26 | - 并不是所有应用都支持 HMS 推送,目前测试已支持大部分应用,比如 QQ、抖音、知乎、酷安等,闲鱼、淘宝、饿了么等 v0.0.13 起已支持 27 | 28 | - **微信不支持,因为微信没有接入 HMS 服务** 29 | 30 | - 请保证 HMS Core 在后台运行,不要禁用其自启权限和访问目标推送应用的权限 31 | 32 | - 如遇到点击通知未能进入目标应用,可尝试将 HMS Core 转为系统应用,不知道如何操作可直接刷入此 [Magisk 模块](https://github.com/fei-ke/HMSPush/releases/download/v0.0.5/HMSCore-v0.3.zip) 33 | 34 | - 反馈问题请带上 LSP 日志,到 Github 提 [Issue](https://github.com/fei-ke/HMSPush/issues) 或者加入 [Telegram 群组](https://t.me/HMSPush),或者发送至我的邮箱 [Email](mailto:hmspush@yufz.one) 35 | 36 | ### 鸣谢 37 | 包括但不限于: 38 | - [LSPosed](https://github.com/LSPosed/LSPosed) XPosed 框架 39 | - [XposedBridge](https://github.com/rovo89/XposedBridge) Xposed framework APIs 40 | - [LSPatch](https://github.com/LSPosed/LSPatch) 免 Root Xposed 框架 41 | - [AndroidNotifyIconAdapt](https://github.com/fankes/AndroidNotifyIconAdapt) 图标库 42 | 43 | ### 反馈 44 | [Github Issues](https://github.com/fei-ke/HMSPush/issues)、[Telegram Group](https://t.me/HMSPush)、[Email](mailto:hmspush@yufz.one) 45 | 46 | ### License 47 | [GNU General Public License v3 (GPL-3)](http://www.gnu.org/copyleft/gpl.html). 48 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | id 'kotlin-parcelize' 5 | } 6 | 7 | android { 8 | compileSdk COMPILE_SDK 9 | 10 | defaultConfig { 11 | applicationId APPLICATION_ID 12 | minSdk MIN_SDK 13 | targetSdk TARGET_SDK 14 | versionCode gitVersionCode 15 | versionName gitVersionName 16 | 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | 19 | archivesBaseName = "${rootProject.name}-v${versionName}-${versionCode}" 20 | } 21 | 22 | signingConfigs { 23 | release { 24 | def locale, keystorePwd, alias, pwd 25 | if (project.rootProject.file('local.properties').exists()) { 26 | Properties properties = new Properties() 27 | properties.load(project.rootProject.file('local.properties').newDataInputStream()) 28 | locale = properties.getProperty("STORE_FILE_PATH") 29 | alias = properties.getProperty("KEY_ALIAS") 30 | pwd = properties.getProperty("KEY_PASSWORD") 31 | keystorePwd = properties.getProperty("STORE_PASSWORD") 32 | } 33 | if (locale != null) { 34 | storeFile file(locale) 35 | storePassword keystorePwd 36 | keyAlias alias 37 | keyPassword pwd 38 | } 39 | } 40 | } 41 | 42 | buildTypes { 43 | debug { 44 | if (signingConfigs.release.storeFile != null && 45 | signingConfigs.release.storeFile.exists()) { 46 | signingConfig signingConfigs.release 47 | } 48 | } 49 | release { 50 | if (signingConfigs.release.storeFile != null && 51 | signingConfigs.release.storeFile.exists()) { 52 | signingConfig signingConfigs.release 53 | } 54 | minifyEnabled true 55 | shrinkResources true 56 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 57 | } 58 | } 59 | compileOptions { 60 | sourceCompatibility JavaVersion.VERSION_1_8 61 | targetCompatibility JavaVersion.VERSION_1_8 62 | } 63 | kotlinOptions { 64 | jvmTarget = "1.8" 65 | } 66 | buildFeatures { 67 | compose true 68 | } 69 | composeOptions { 70 | kotlinCompilerExtensionVersion kotlin_compiler_extension_version 71 | } 72 | } 73 | def lifecycle_version = "2.6.1" 74 | def activity_version = "1.7.2" 75 | def nav_version = "2.7.2" 76 | 77 | dependencies { 78 | api project(":common") 79 | api project(":xposed") 80 | 81 | //remove this line will cause proguard to remove code from library module 82 | compileOnly 'de.robv.android.xposed:api:82' 83 | 84 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" 85 | implementation "androidx.activity:activity-ktx:$activity_version" 86 | 87 | //compose 88 | implementation platform('androidx.compose:compose-bom:2023.08.00') 89 | implementation "androidx.compose.material3:material3" 90 | implementation "androidx.compose.animation:animation" 91 | implementation "androidx.compose.ui:ui" 92 | debugImplementation "androidx.compose.ui:ui-tooling" 93 | implementation "androidx.compose.ui:ui-tooling-preview" 94 | implementation "androidx.compose.foundation:foundation" 95 | implementation "androidx.compose.material:material-icons-core" 96 | implementation "androidx.compose.material:material-icons-extended" 97 | 98 | implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version" 99 | implementation "com.google.accompanist:accompanist-drawablepainter:0.25.1" 100 | 101 | implementation "androidx.activity:activity-compose:$activity_version" 102 | implementation "androidx.navigation:navigation-runtime-ktx:$nav_version" 103 | implementation "androidx.navigation:navigation-compose:$nav_version" 104 | 105 | implementation 'de.charlex.compose:html-text:1.3.1' 106 | } 107 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 31 | 35 | 39 | 43 | 47 | 48 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /app/src/main/assets/xposed_init: -------------------------------------------------------------------------------- 1 | one.yufz.hmspush.hook.XposedMod -------------------------------------------------------------------------------- /app/src/main/java/one/yufz/hmspush/app/App.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.app 2 | 3 | import android.app.Application 4 | 5 | class App : Application() { 6 | companion object { 7 | lateinit var instance: App 8 | private set 9 | } 10 | 11 | override fun onCreate() { 12 | super.onCreate() 13 | instance = this 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/one/yufz/hmspush/app/HmsPushClient.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.app 2 | 3 | import android.content.Context 4 | import kotlinx.coroutines.flow.Flow 5 | import one.yufz.hmspush.common.BinderCursor 6 | import one.yufz.hmspush.common.BridgeUri 7 | import one.yufz.hmspush.common.BridgeWrap 8 | import one.yufz.hmspush.common.HmsPushInterface 9 | import one.yufz.hmspush.common.IconData 10 | import one.yufz.hmspush.common.model.IconModel 11 | import one.yufz.hmspush.common.model.ModuleVersionModel 12 | import one.yufz.hmspush.common.model.PrefsModel 13 | import one.yufz.hmspush.common.model.PushHistoryModel 14 | import one.yufz.hmspush.common.model.PushSignModel 15 | import java.lang.reflect.InvocationHandler 16 | import java.lang.reflect.Method 17 | import java.lang.reflect.Proxy 18 | 19 | fun createHmsPushServiceProxy(context: Context): HmsPushInterface = Proxy.newProxyInstance(HmsPushInterface::class.java.classLoader, arrayOf(HmsPushInterface::class.java), object : InvocationHandler { 20 | private lateinit var service: HmsPushInterface 21 | 22 | private fun getService(): HmsPushInterface { 23 | if (this::service.isInitialized && service.asBinder().isBinderAlive) { 24 | return service 25 | } 26 | BridgeWrap.query(context, BridgeUri.HMS_PUSH_SERVICE.toUri())?.use { 27 | service = HmsPushInterface.Stub.asInterface(BinderCursor.getBinder(it)) 28 | return service 29 | } 30 | return HmsPushInterface.Default() 31 | } 32 | 33 | override fun invoke(proxy: Any, method: Method, args: Array?): Any? { 34 | return try { 35 | call(getService(), method, args) 36 | } catch (t: Throwable) { 37 | try { 38 | call(getService(), method, args) 39 | } catch (e: Throwable) { 40 | call(HmsPushInterface.Default(), method, args) 41 | } 42 | } 43 | } 44 | 45 | private fun call(obj: Any, method: Method, args: Array?): Any? { 46 | return if (args != null) { 47 | method.invoke(obj, *args) 48 | } else { 49 | method.invoke(obj) 50 | } 51 | } 52 | }) as HmsPushInterface 53 | 54 | object HmsPushClient : HmsPushInterface.Stub() { 55 | private val service = createHmsPushServiceProxy(App.instance) 56 | 57 | fun getHmsPushServiceFlow(): Flow = 58 | BridgeWrap.registerContentAsFlow(App.instance, BridgeUri.HMS_PUSH_SERVICE.toUri()) {} 59 | 60 | fun getPushSignFlow(): Flow> = 61 | BridgeWrap.registerContentAsFlow(App.instance, BridgeUri.PUSH_SIGN.toUri()) { pushSignList } 62 | 63 | fun getPushHistoryFlow(): Flow> = 64 | BridgeWrap.registerContentAsFlow(App.instance, BridgeUri.PUSH_HISTORY.toUri()) { pushHistoryList } 65 | 66 | fun isHmsPushServiceAlive(): Boolean { 67 | return moduleVersion != null 68 | } 69 | 70 | override fun getModuleVersion(): ModuleVersionModel? { 71 | return service.moduleVersion 72 | } 73 | 74 | override fun getPushSignList(): List { 75 | return service.pushSignList ?: emptyList() 76 | } 77 | 78 | override fun unregisterPush(packageName: String) { 79 | return service.unregisterPush(packageName) 80 | } 81 | 82 | override fun getPushHistoryList(): List { 83 | return service.pushHistoryList ?: emptyList() 84 | } 85 | 86 | override fun getPreference(): PrefsModel { 87 | return service.preference ?: PrefsModel() 88 | } 89 | 90 | override fun updatePreference(model: PrefsModel) { 91 | service.updatePreference(model) 92 | } 93 | 94 | override fun getAllIcon(): List { 95 | return service.allIcon ?: emptyList() 96 | } 97 | 98 | override fun saveIcon(iconModel: IconModel?) { 99 | service.saveIcon(iconModel) 100 | } 101 | 102 | override fun deleteIcon(vararg packageName: String) { 103 | service.deleteIcon(packageName) 104 | } 105 | 106 | fun saveIcon(iconData: IconData) { 107 | saveIcon(IconModel(iconData.packageName, iconData.toJson())) 108 | } 109 | 110 | override fun killHmsCore(): Boolean { 111 | return service.killHmsCore() 112 | } 113 | 114 | override fun clearHmsNotificationChannels(packageName: String) { 115 | service.clearHmsNotificationChannels(packageName) 116 | } 117 | } -------------------------------------------------------------------------------- /app/src/main/java/one/yufz/hmspush/app/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.app 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.core.view.WindowCompat 7 | import one.yufz.hmspush.app.theme.AppTheme 8 | 9 | class MainActivity : ComponentActivity() { 10 | override fun onCreate(savedInstanceState: Bundle?) { 11 | super.onCreate(savedInstanceState) 12 | WindowCompat.setDecorFitsSystemWindows(window, false) 13 | setContent { 14 | AppTheme { 15 | AppNavHost() 16 | } 17 | } 18 | } 19 | } 20 | 21 | val mainActivityAlias = "${MainActivity::class.java.name}Alias" -------------------------------------------------------------------------------- /app/src/main/java/one/yufz/hmspush/app/NavHost.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.app 2 | 3 | import androidx.compose.animation.core.tween 4 | import androidx.compose.animation.fadeIn 5 | import androidx.compose.animation.fadeOut 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.CompositionLocalProvider 8 | import androidx.compose.runtime.staticCompositionLocalOf 9 | import androidx.compose.ui.Modifier 10 | import androidx.navigation.NavHostController 11 | import androidx.navigation.compose.NavHost 12 | import androidx.navigation.compose.composable 13 | import androidx.navigation.compose.rememberNavController 14 | import one.yufz.hmspush.app.home.HomeScreen 15 | import one.yufz.hmspush.app.icon.IconScreen 16 | import one.yufz.hmspush.app.settings.SettingsScreen 17 | 18 | val LocalNavHostController = staticCompositionLocalOf { error("shouldn't happen") } 19 | 20 | @Composable 21 | fun AppNavHost( 22 | modifier: Modifier = Modifier, 23 | navController: NavHostController = rememberNavController(), 24 | startDestination: String = "home" 25 | ) { 26 | CompositionLocalProvider(LocalNavHostController provides navController) { 27 | NavHost( 28 | modifier = modifier, 29 | navController = navController, 30 | startDestination = startDestination, 31 | enterTransition = { fadeIn(animationSpec = tween()) }, 32 | exitTransition = { fadeOut(animationSpec = tween()) }, 33 | ) { 34 | composable("home") { HomeScreen() } 35 | composable("settings") { SettingsScreen() } 36 | composable("icon") { IconScreen() } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/one/yufz/hmspush/app/home/AppInfo.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.app.home 2 | 3 | data class AppInfo( 4 | val packageName: String, 5 | val name: String, 6 | var registered: Boolean = false, 7 | val lastPushTime: Long? = null 8 | ) -------------------------------------------------------------------------------- /app/src/main/java/one/yufz/hmspush/app/home/AppListViewModel.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.app.home 2 | 3 | import android.app.Application 4 | import android.content.Intent 5 | import android.content.pm.PackageManager 6 | import androidx.lifecycle.AndroidViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.async 10 | import kotlinx.coroutines.flow.Flow 11 | import kotlinx.coroutines.flow.MutableSharedFlow 12 | import kotlinx.coroutines.flow.MutableStateFlow 13 | import kotlinx.coroutines.flow.combine 14 | import kotlinx.coroutines.launch 15 | import kotlinx.coroutines.withContext 16 | import one.yufz.hmspush.app.HmsPushClient 17 | import one.yufz.hmspush.app.util.registerPackageChangeFlow 18 | import one.yufz.hmspush.common.HMS_CORE_PUSH_ACTION_NOTIFY_MSG 19 | import one.yufz.hmspush.common.HMS_CORE_PUSH_ACTION_REGISTRATION 20 | import one.yufz.hmspush.common.model.PushHistoryModel 21 | import one.yufz.hmspush.common.model.PushSignModel 22 | 23 | class AppListViewModel(val context: Application) : AndroidViewModel(context) { 24 | companion object { 25 | private const val TAG = "AppListViewModel" 26 | } 27 | 28 | private val filterKeywords = MutableStateFlow("") 29 | 30 | private val supportedAppListFlow: MutableSharedFlow> = MutableStateFlow(emptyList()) 31 | 32 | private val registeredListFlow = HmsPushClient.getPushSignFlow() 33 | 34 | private val historyListFlow = HmsPushClient.getPushHistoryFlow() 35 | val appListFlow: Flow> = combine(supportedAppListFlow, registeredListFlow, historyListFlow, ::mergeSource) 36 | .combine(filterKeywords, ::filterAppList) 37 | 38 | init { 39 | viewModelScope.launch { 40 | supportedAppListFlow.emit(loadSupportedAppList()) 41 | context.registerPackageChangeFlow().collect { 42 | supportedAppListFlow.emit(loadSupportedAppList()) 43 | } 44 | } 45 | } 46 | 47 | private suspend fun loadSupportedAppList(): List { 48 | return withContext(Dispatchers.Default) { 49 | val queryByReceiver = async(Dispatchers.IO) { 50 | val intent = Intent(HMS_CORE_PUSH_ACTION_REGISTRATION) 51 | context.packageManager.queryBroadcastReceivers( 52 | intent, 53 | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS 54 | or PackageManager.MATCH_DISABLED_COMPONENTS 55 | ).map { it.activityInfo.packageName } 56 | } 57 | val queryByService = async(Dispatchers.IO) { 58 | val intent = Intent(HMS_CORE_PUSH_ACTION_NOTIFY_MSG) 59 | context.packageManager.queryIntentServices( 60 | intent, 61 | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS 62 | or PackageManager.MATCH_DISABLED_COMPONENTS 63 | ).map { it.serviceInfo.packageName } 64 | } 65 | (queryByReceiver.await() + queryByService.await()).distinct() 66 | } 67 | } 68 | 69 | 70 | private fun filterAppList(list: List, keywords: String): List { 71 | if (keywords.isEmpty()) return list 72 | 73 | return list.filter { 74 | it.name.contains(keywords, true) || it.packageName.contains(keywords, true) 75 | } 76 | } 77 | 78 | private fun mergeSource(appList: List, registered: List, history: List): List { 79 | val pm = context.packageManager 80 | val registeredSet = registered.map { it.packageName } 81 | val historyMap = history.associateBy { it.packageName } 82 | return appList.map { packageName -> 83 | AppInfo( 84 | packageName = packageName, 85 | name = try { 86 | pm.getApplicationInfo(packageName, 0).loadLabel(pm).toString() 87 | } catch (e: PackageManager.NameNotFoundException) { 88 | packageName 89 | }, 90 | registered = registeredSet.contains(packageName), 91 | lastPushTime = historyMap[packageName]?.pushTime 92 | ) 93 | } 94 | .sortedWith(compareBy({ !it.registered }, { Long.MAX_VALUE - (it.lastPushTime ?: 0L) })) 95 | } 96 | 97 | fun filter(keywords: String) { 98 | viewModelScope.launch { 99 | filterKeywords.emit(keywords) 100 | } 101 | } 102 | 103 | fun unregisterPush(packageName: String) { 104 | HmsPushClient.unregisterPush(packageName) 105 | } 106 | } -------------------------------------------------------------------------------- /app/src/main/java/one/yufz/hmspush/app/home/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.app.home 2 | 3 | import android.app.Application 4 | import android.content.Intent 5 | import android.content.pm.PackageManager 6 | import androidx.lifecycle.AndroidViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import kotlinx.coroutines.Job 9 | import kotlinx.coroutines.flow.Flow 10 | import kotlinx.coroutines.flow.MutableStateFlow 11 | import kotlinx.coroutines.flow.StateFlow 12 | import kotlinx.coroutines.flow.filter 13 | import kotlinx.coroutines.flow.launchIn 14 | import kotlinx.coroutines.flow.onEach 15 | import one.yufz.hmspush.R 16 | import one.yufz.hmspush.app.HmsPushClient 17 | import one.yufz.hmspush.app.util.registerPackageChangeFlow 18 | import one.yufz.hmspush.common.API_VERSION 19 | import one.yufz.hmspush.common.HMS_PACKAGE_NAME 20 | import one.yufz.hmspush.common.VERSION_NAME 21 | 22 | class HomeViewModel(val app: Application) : AndroidViewModel(app) { 23 | enum class Reason { 24 | None, 25 | Checking, 26 | HmsCoreNotInstalled, 27 | HmsCoreNotActivated, 28 | HmsPushVersionNotMatch 29 | } 30 | 31 | data class UiState(val usable: Boolean, val tips: String, val reason: Reason) 32 | 33 | private val _uiState = MutableStateFlow(UiState(false, "", Reason.Checking)) 34 | val uiState: StateFlow = _uiState 35 | 36 | private var _searchState = MutableStateFlow(false) 37 | val searchState: Flow = _searchState 38 | 39 | private var _searchText = MutableStateFlow("") 40 | val searchText: Flow = _searchText 41 | 42 | private var registerJob: Job? = null 43 | 44 | init { 45 | app.registerPackageChangeFlow() 46 | .filter { it.dataString?.removePrefix("package:") == HMS_PACKAGE_NAME } 47 | .onEach { onHmsPackageChanged(it) } 48 | .launchIn(viewModelScope) 49 | 50 | registerServiceChange() 51 | } 52 | 53 | private fun onHmsPackageChanged(intent: Intent) { 54 | when (intent.action) { 55 | Intent.ACTION_PACKAGE_ADDED -> registerServiceChange() 56 | Intent.ACTION_PACKAGE_REMOVED -> registerJob?.cancel() 57 | } 58 | checkHmsCore() 59 | } 60 | 61 | private fun registerServiceChange() { 62 | registerJob?.cancel() 63 | registerJob = HmsPushClient.getHmsPushServiceFlow() 64 | .onEach { checkHmsCore() } 65 | .launchIn(viewModelScope) 66 | } 67 | 68 | fun setSearching(searching: Boolean) { 69 | _searchState.value = searching 70 | } 71 | 72 | fun checkHmsCore() { 73 | try { 74 | app.packageManager.getApplicationInfo(HMS_PACKAGE_NAME, 0) 75 | } catch (e: PackageManager.NameNotFoundException) { 76 | _uiState.value = UiState(false, app.getString(R.string.hms_core_not_found), Reason.HmsCoreNotInstalled) 77 | return 78 | } 79 | 80 | val moduleVersion = HmsPushClient.moduleVersion 81 | if (moduleVersion == null) { 82 | _uiState.value = UiState(false, app.getString(R.string.hms_not_activated), Reason.HmsCoreNotActivated) 83 | return 84 | } 85 | 86 | if (moduleVersion.apiVersion != API_VERSION) { 87 | _uiState.value = UiState(false, app.getString(R.string.hms_version_not_match), Reason.HmsPushVersionNotMatch) 88 | return 89 | } 90 | 91 | _uiState.value = UiState(true, "", Reason.None) 92 | } 93 | 94 | fun setSearchText(text: String) { 95 | _searchText.value = text 96 | } 97 | } -------------------------------------------------------------------------------- /app/src/main/java/one/yufz/hmspush/app/home/Util.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.app.home 2 | 3 | import android.content.ActivityNotFoundException 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.net.Uri 7 | import android.provider.Settings 8 | 9 | object Util { 10 | fun launchApp(context: Context, packageName: String) { 11 | val launchIntentForPackage = context.packageManager.getLaunchIntentForPackage(packageName) 12 | if (launchIntentForPackage != null) { 13 | context.startActivity(launchIntentForPackage) 14 | } 15 | } 16 | 17 | fun launchAppInfo(context: Context, packageName: String) { 18 | val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { 19 | addCategory(Intent.CATEGORY_DEFAULT) 20 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 21 | data = Uri.parse("package:${packageName}") 22 | } 23 | try { 24 | context.startActivity(intent) 25 | } catch (e: ActivityNotFoundException) { 26 | //ignore 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/java/one/yufz/hmspush/app/icon/IconViewModel.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.app.icon 2 | 3 | import android.app.Application 4 | import androidx.lifecycle.AndroidViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.flow.Flow 8 | import kotlinx.coroutines.flow.MutableStateFlow 9 | import kotlinx.coroutines.flow.StateFlow 10 | import kotlinx.coroutines.flow.combine 11 | import kotlinx.coroutines.launch 12 | import kotlinx.coroutines.withContext 13 | import one.yufz.hmspush.R 14 | import one.yufz.hmspush.app.HmsPushClient 15 | import one.yufz.hmspush.common.BridgeWrap 16 | import one.yufz.hmspush.common.IconData 17 | import org.json.JSONArray 18 | import org.json.JSONObject 19 | import java.net.URL 20 | 21 | class IconViewModel(val app: Application) : AndroidViewModel(app) { 22 | companion object { 23 | private const val TAG = "IconViewModel" 24 | const val ICON_URL = "https://raw.githubusercontent.com/fankes/AndroidNotifyIconAdapt/main/APP/NotifyIconsSupportConfig.json" 25 | } 26 | 27 | data class ImportState(val loading: Boolean, val info: String? = null) 28 | 29 | private val _iconsFlow = MutableStateFlow>(emptyList()) 30 | 31 | private val _importState = MutableStateFlow(ImportState(false)) 32 | val importState: StateFlow = _importState 33 | 34 | private val filterKeywords = MutableStateFlow("") 35 | 36 | val iconsFlow: Flow> = _iconsFlow.combine(filterKeywords) { list, keywords -> 37 | if (keywords.isEmpty()) return@combine list 38 | 39 | list.filter { it.appName.contains(keywords, true) || it.packageName.contains(keywords, true) } 40 | } 41 | 42 | init { 43 | loadIcon() 44 | } 45 | 46 | fun fetchIconFromUrl(url: String) { 47 | viewModelScope.launch(Dispatchers.IO) { 48 | _importState.emit(ImportState(true)) 49 | 50 | try { 51 | readIconFromUrl(url).forEach { 52 | HmsPushClient.saveIcon(it) 53 | } 54 | } catch (e: Throwable) { 55 | _importState.emit(ImportState(false, e.message)) 56 | return@launch 57 | } 58 | 59 | _importState.emit(ImportState(false, getApplication().getString(R.string.import_complete))) 60 | 61 | loadIcon() 62 | } 63 | } 64 | 65 | fun loadIcon() { 66 | viewModelScope.launch(Dispatchers.IO) { 67 | _iconsFlow.value = HmsPushClient.allIcon.mapNotNull { it.toIconData() } 68 | } 69 | } 70 | 71 | private suspend fun readIconFromUrl(url: String): List { 72 | return withContext(Dispatchers.IO) { 73 | val jsonString = URL(url).readText() 74 | val jsonArray = JSONArray(jsonString) 75 | val iconList = ArrayList(jsonArray.length()) 76 | 77 | for (i in 0 until jsonArray.length()) { 78 | val obj = jsonArray.get(i) as JSONObject 79 | iconList.add(IconData.fromJson(obj)) 80 | } 81 | iconList 82 | } 83 | } 84 | 85 | fun cancelImport() { 86 | _importState.value = ImportState(false) 87 | } 88 | 89 | fun filter(keywords: String) { 90 | viewModelScope.launch { 91 | filterKeywords.emit(keywords) 92 | } 93 | } 94 | 95 | fun clearIcons() { 96 | viewModelScope.launch(Dispatchers.IO) { 97 | HmsPushClient.deleteIcon() 98 | loadIcon() 99 | } 100 | } 101 | 102 | fun deleteIcon(vararg packageName: String) { 103 | viewModelScope.launch() { 104 | val set = packageName.toHashSet() 105 | _iconsFlow.value = _iconsFlow.value.filterNot { it.packageName in set } 106 | 107 | HmsPushClient.deleteIcon(*packageName) 108 | } 109 | } 110 | } -------------------------------------------------------------------------------- /app/src/main/java/one/yufz/hmspush/app/settings/SettingsViewModel.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.app.settings 2 | 3 | import android.app.Application 4 | import android.content.ComponentName 5 | import android.content.pm.PackageManager 6 | import androidx.lifecycle.AndroidViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.flow.Flow 10 | import kotlinx.coroutines.flow.MutableStateFlow 11 | import kotlinx.coroutines.launch 12 | import one.yufz.hmspush.app.HmsPushClient 13 | import one.yufz.hmspush.app.mainActivityAlias 14 | import one.yufz.hmspush.common.HmsCoreUtil 15 | import one.yufz.hmspush.common.model.PrefsModel 16 | 17 | class SettingsViewModel(val context: Application) : AndroidViewModel(context) { 18 | private val _preferences = MutableStateFlow(PrefsModel()) 19 | 20 | val preferences: Flow = _preferences 21 | 22 | init { 23 | queryPreferences() 24 | } 25 | 26 | fun queryPreferences() { 27 | viewModelScope.launch(Dispatchers.IO) { 28 | _preferences.emit(HmsPushClient.preference) 29 | } 30 | } 31 | 32 | fun updatePreference(updateAction: PrefsModel. () -> Unit) { 33 | val copy = _preferences.value.copy() 34 | updateAction(copy) 35 | _preferences.value = copy 36 | viewModelScope.launch(Dispatchers.IO) { 37 | HmsPushClient.updatePreference(_preferences.value) 38 | } 39 | } 40 | 41 | fun setHmsCoreForeground(foreground: Boolean) { 42 | HmsCoreUtil.startHmsCoreService(context, foreground) 43 | } 44 | 45 | fun toggleAppIcon(hide: Boolean) { 46 | updatePreference { hideAppIcon = hide } 47 | val newState = if (hide) { 48 | PackageManager.COMPONENT_ENABLED_STATE_DISABLED 49 | } else { 50 | PackageManager.COMPONENT_ENABLED_STATE_ENABLED 51 | } 52 | context.packageManager.setComponentEnabledSetting( 53 | ComponentName(context, mainActivityAlias), 54 | newState, 55 | PackageManager.DONT_KILL_APP 56 | ) 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/one/yufz/hmspush/app/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.app.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple80 = Color(0xFFD0BCFF) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink80 = Color(0xFFEFB8C8) 8 | 9 | val Purple40 = Color(0xFF6650a4) 10 | val PurpleGrey40 = Color(0xFF625b71) 11 | val Pink40 = Color(0xFF7D5260) 12 | 13 | val Green = Color(0xFF4CAF50) 14 | val GreenDark = Color(0xFF78DC77) 15 | 16 | val Grey = Color(0xFF808080) 17 | val GreyDark = Color(0xFFD9D9D9) 18 | -------------------------------------------------------------------------------- /app/src/main/java/one/yufz/hmspush/app/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.app.theme 2 | 3 | import android.os.Build 4 | import androidx.compose.foundation.isSystemInDarkTheme 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.material3.darkColorScheme 7 | import androidx.compose.material3.dynamicDarkColorScheme 8 | import androidx.compose.material3.dynamicLightColorScheme 9 | import androidx.compose.material3.lightColorScheme 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.CompositionLocalProvider 12 | import androidx.compose.runtime.ReadOnlyComposable 13 | import androidx.compose.runtime.SideEffect 14 | import androidx.compose.runtime.staticCompositionLocalOf 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.platform.LocalContext 17 | import androidx.compose.ui.platform.LocalView 18 | import androidx.core.view.ViewCompat 19 | 20 | private val DarkColorScheme = darkColorScheme( 21 | primary = Purple80, 22 | secondary = PurpleGrey80, 23 | tertiary = Pink80 24 | ) 25 | 26 | private val LightColorScheme = lightColorScheme( 27 | primary = Purple40, 28 | secondary = PurpleGrey40, 29 | tertiary = Pink40 30 | 31 | /* Other default colors to override 32 | background = Color(0xFFFFFBFE), 33 | surface = Color(0xFFFFFBFE), 34 | onPrimary = Color.White, 35 | onSecondary = Color.White, 36 | onTertiary = Color.White, 37 | onBackground = Color(0xFF1C1B1F), 38 | onSurface = Color(0xFF1C1B1F), 39 | */ 40 | ) 41 | 42 | data class CustomColors( 43 | val active: Color, 44 | val grey: Color 45 | ) 46 | 47 | private val LightCustomColors = CustomColors( 48 | active = Green, 49 | grey = Grey 50 | ) 51 | 52 | private val DarkCustomColors = CustomColors( 53 | active = GreenDark, 54 | grey = GreyDark 55 | ) 56 | 57 | private val LocalCustomColors = staticCompositionLocalOf { LightCustomColors } 58 | 59 | val MaterialTheme.customColors: CustomColors 60 | @Composable 61 | @ReadOnlyComposable 62 | get() = LocalCustomColors.current 63 | 64 | @Composable 65 | fun AppTheme( 66 | darkTheme: Boolean = isSystemInDarkTheme(), 67 | // Dynamic color is available on Android 12+ 68 | dynamicColor: Boolean = true, 69 | content: @Composable () -> Unit 70 | ) { 71 | val colorScheme = when { 72 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 73 | val context = LocalContext.current 74 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 75 | } 76 | darkTheme -> DarkColorScheme 77 | else -> LightColorScheme 78 | } 79 | val view = LocalView.current 80 | if (!view.isInEditMode) { 81 | SideEffect { 82 | 83 | ViewCompat.getWindowInsetsController(view)?.apply { 84 | isAppearanceLightStatusBars = !darkTheme 85 | isAppearanceLightNavigationBars = !darkTheme 86 | } 87 | } 88 | } 89 | 90 | val customColors = if (darkTheme) { 91 | DarkCustomColors 92 | } else { 93 | LightCustomColors 94 | } 95 | 96 | CompositionLocalProvider(LocalCustomColors provides customColors) { 97 | MaterialTheme( 98 | colorScheme = colorScheme, 99 | content = content 100 | ) 101 | } 102 | } -------------------------------------------------------------------------------- /app/src/main/java/one/yufz/hmspush/app/util/Context.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.app.util 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.content.IntentFilter 7 | import kotlinx.coroutines.channels.awaitClose 8 | import kotlinx.coroutines.channels.trySendBlocking 9 | import kotlinx.coroutines.flow.Flow 10 | import kotlinx.coroutines.flow.callbackFlow 11 | 12 | fun Context.registerReceiverAsFlow(intentFilter: IntentFilter): Flow = callbackFlow { 13 | val receiver = object : BroadcastReceiver() { 14 | override fun onReceive(context: Context, intent: Intent) { 15 | trySendBlocking(intent) 16 | } 17 | } 18 | registerReceiver(receiver, intentFilter) 19 | awaitClose { 20 | unregisterReceiver(receiver) 21 | } 22 | } 23 | 24 | fun Context.registerPackageChangeFlow(): Flow { 25 | val intentFilter = IntentFilter().apply { 26 | addAction(Intent.ACTION_PACKAGE_ADDED) 27 | addAction(Intent.ACTION_PACKAGE_CHANGED) 28 | addAction(Intent.ACTION_PACKAGE_REMOVED) 29 | addDataScheme("package") 30 | } 31 | return registerReceiverAsFlow(intentFilter) 32 | } -------------------------------------------------------------------------------- /app/src/main/java/one/yufz/hmspush/app/widget/LifecycleAware.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.app.widget 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.DisposableEffect 5 | import androidx.compose.runtime.getValue 6 | import androidx.compose.runtime.rememberUpdatedState 7 | import androidx.compose.ui.platform.LocalLifecycleOwner 8 | import androidx.lifecycle.Lifecycle 9 | import androidx.lifecycle.LifecycleEventObserver 10 | import androidx.lifecycle.LifecycleOwner 11 | 12 | //https://developer.android.com/jetpack/compose/side-effects#disposableeffect 13 | @Composable 14 | fun LifecycleAware( 15 | lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, 16 | onStart: () -> Unit = {}, // Send the 'started' analytics event 17 | onResume: () -> Unit = {}, // Send the 'resumed' analytics event 18 | onPause: () -> Unit = {}, // Send the 'paused' analytics event 19 | onStop: () -> Unit = {}, // Send the 'stopped' analytics event 20 | content: @Composable () -> Unit 21 | ) { 22 | // Safely update the current lambdas when a new one is provided 23 | val currentOnStart by rememberUpdatedState(onStart) 24 | val currentOnResume by rememberUpdatedState(onResume) 25 | val currentOnPause by rememberUpdatedState(onPause) 26 | val currentOnStop by rememberUpdatedState(onStop) 27 | 28 | // If `lifecycleOwner` changes, dispose and reset the effect 29 | DisposableEffect(lifecycleOwner) { 30 | // Create an observer that triggers our remembered callbacks 31 | // for sending analytics events 32 | val observer = LifecycleEventObserver { _, event -> 33 | when (event) { 34 | Lifecycle.Event.ON_START -> currentOnStart() 35 | Lifecycle.Event.ON_RESUME -> currentOnResume() 36 | Lifecycle.Event.ON_PAUSE -> currentOnPause() 37 | Lifecycle.Event.ON_STOP -> currentOnStop() 38 | else -> Unit 39 | } 40 | } 41 | 42 | // Add the observer to the lifecycle 43 | lifecycleOwner.lifecycle.addObserver(observer) 44 | 45 | // When the effect leaves the Composition, remove the observer 46 | onDispose { 47 | lifecycleOwner.lifecycle.removeObserver(observer) 48 | } 49 | } 50 | 51 | content() 52 | } -------------------------------------------------------------------------------- /app/src/main/java/one/yufz/hmspush/app/widget/LoadingDialog.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.app.widget 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.foundation.layout.size 8 | import androidx.compose.material3.AlertDialogDefaults 9 | import androidx.compose.material3.CircularProgressIndicator 10 | import androidx.compose.material3.Surface 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.unit.dp 16 | import androidx.compose.ui.window.Dialog 17 | import androidx.compose.ui.window.DialogProperties 18 | 19 | @Composable 20 | fun LoadingDialog(onDismissRequest: () -> Unit, text: String? = null) { 21 | Dialog( 22 | onDismissRequest = onDismissRequest, 23 | properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false) 24 | ) { 25 | Surface( 26 | shape = AlertDialogDefaults.shape, 27 | color = AlertDialogDefaults.containerColor, 28 | tonalElevation = AlertDialogDefaults.TonalElevation, 29 | ) { 30 | Column( 31 | modifier = Modifier 32 | .size(160.dp), 33 | horizontalAlignment = Alignment.CenterHorizontally, 34 | verticalArrangement = Arrangement.Center 35 | ) { 36 | CircularProgressIndicator() 37 | text?.let { 38 | Spacer(modifier = Modifier.height(8.dp)) 39 | Text(text = text, color = AlertDialogDefaults.textContentColor) 40 | } 41 | 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/one/yufz/hmspush/app/widget/SearchBar.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.app.widget 2 | 3 | import androidx.activity.compose.BackHandler 4 | import androidx.compose.animation.AnimatedVisibility 5 | import androidx.compose.animation.ExperimentalAnimationApi 6 | import androidx.compose.animation.fadeIn 7 | import androidx.compose.animation.fadeOut 8 | import androidx.compose.foundation.layout.Row 9 | import androidx.compose.foundation.layout.Spacer 10 | import androidx.compose.foundation.layout.fillMaxWidth 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.width 13 | import androidx.compose.foundation.text.KeyboardActions 14 | import androidx.compose.foundation.text.KeyboardOptions 15 | import androidx.compose.material.icons.Icons 16 | import androidx.compose.material.icons.filled.ArrowBack 17 | import androidx.compose.material.icons.filled.Close 18 | import androidx.compose.material3.ExperimentalMaterial3Api 19 | import androidx.compose.material3.Icon 20 | import androidx.compose.material3.IconButton 21 | import androidx.compose.material3.OutlinedTextField 22 | import androidx.compose.material3.Text 23 | import androidx.compose.material3.TextFieldDefaults 24 | import androidx.compose.runtime.Composable 25 | import androidx.compose.runtime.LaunchedEffect 26 | import androidx.compose.runtime.getValue 27 | import androidx.compose.runtime.mutableStateOf 28 | import androidx.compose.runtime.remember 29 | import androidx.compose.runtime.setValue 30 | import androidx.compose.ui.Alignment 31 | import androidx.compose.ui.ExperimentalComposeUiApi 32 | import androidx.compose.ui.Modifier 33 | import androidx.compose.ui.focus.FocusRequester 34 | import androidx.compose.ui.focus.focusRequester 35 | import androidx.compose.ui.focus.onFocusChanged 36 | import androidx.compose.ui.graphics.Color 37 | import androidx.compose.ui.platform.LocalSoftwareKeyboardController 38 | import androidx.compose.ui.text.TextRange 39 | import androidx.compose.ui.text.input.ImeAction 40 | import androidx.compose.ui.text.input.TextFieldValue 41 | import androidx.compose.ui.tooling.preview.Preview 42 | import androidx.compose.ui.unit.dp 43 | 44 | @OptIn(ExperimentalMaterial3Api::class) 45 | @ExperimentalAnimationApi 46 | @ExperimentalComposeUiApi 47 | @Composable 48 | fun SearchBar( 49 | searchText: String = "", 50 | placeholderText: String = "", 51 | onNavigateBack: () -> Unit = {}, 52 | onSearchTextChanged: (String) -> Unit = {}, 53 | ) { 54 | var showClearButton by remember { mutableStateOf(false) } 55 | val keyboardController = LocalSoftwareKeyboardController.current 56 | val focusRequester = remember { FocusRequester() } 57 | val textState = remember { mutableStateOf(TextFieldValue(searchText, selection = TextRange(searchText.length))) } 58 | 59 | fun clear() { 60 | textState.value = TextFieldValue() 61 | onSearchTextChanged("") 62 | } 63 | 64 | fun back() { 65 | clear() 66 | onNavigateBack() 67 | } 68 | BackHandler { 69 | back() 70 | } 71 | Row { 72 | Spacer(modifier = Modifier.width(4.dp)) 73 | IconButton(onClick = ::back, modifier = Modifier.align(alignment = Alignment.CenterVertically)) { 74 | Icon( 75 | imageVector = Icons.Default.ArrowBack, 76 | contentDescription = "Back" 77 | ) 78 | } 79 | OutlinedTextField( 80 | value = textState.value, 81 | onValueChange = { 82 | textState.value = it 83 | onSearchTextChanged(it.text) 84 | }, 85 | modifier = Modifier 86 | .fillMaxWidth() 87 | .padding(vertical = 2.dp) 88 | .onFocusChanged { focusState -> showClearButton = (focusState.isFocused) } 89 | .focusRequester(focusRequester), 90 | placeholder = { Text(text = placeholderText) }, 91 | colors = TextFieldDefaults.textFieldColors( 92 | focusedIndicatorColor = Color.Transparent, 93 | unfocusedIndicatorColor = Color.Transparent, 94 | containerColor = Color.Transparent, 95 | ), 96 | trailingIcon = { 97 | AnimatedVisibility( 98 | visible = showClearButton, 99 | enter = fadeIn(), 100 | exit = fadeOut() 101 | ) { 102 | IconButton( 103 | onClick = { 104 | if (textState.value.text.isEmpty()) { 105 | back() 106 | } else { 107 | clear() 108 | } 109 | } 110 | ) { 111 | Icon( 112 | imageVector = Icons.Filled.Close, 113 | contentDescription = "clear" 114 | ) 115 | } 116 | } 117 | }, 118 | maxLines = 1, 119 | singleLine = true, 120 | keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done), 121 | keyboardActions = KeyboardActions(onDone = { 122 | keyboardController?.hide() 123 | }), 124 | ) 125 | } 126 | LaunchedEffect(Unit) { 127 | focusRequester.requestFocus() 128 | } 129 | } 130 | 131 | @ExperimentalAnimationApi 132 | @OptIn(ExperimentalComposeUiApi::class) 133 | @Preview 134 | @Composable 135 | fun preView() { 136 | SearchBar("1234") { 137 | 138 | } 139 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 9 | 15 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | HMSPush 3 | 启用 HMSCore 系统级推送、伪装华为设备 4 | HMS Core 未安装或没有权限访问 5 | HMSPush 模块未激活或需要重启 HMS Core 6 | 模块未启用或者需要重启应用 7 | 模块已更新,需要重启 HMS Core 8 | 9 | 启动 10 | 应用信息 11 | 搜索 12 | 取消注册 13 | 设置 14 | 打开 HMS Core 应用详情 15 | 重启 HMS Core 16 | 正在尝试重启 HMS Core 17 | 请手动停止 HMS Core 18 | 重启 19 | 20 | 禁用签名检查 21 | LSPatch 或者修改版应用需要禁用签名检查 22 | 23 | 未注册 24 | 已注册 25 |   •  最近推送:%s 26 | 27 | 确定取消注册 28 | 确定 29 | 取消 30 | 31 | 保留历史消息 32 | 一些应用,比如 QQ 默认只显示最新一条消息,开启此开关可保留历史消息 33 | 34 | 自定义通知图标 35 | 通知栏图标染色 36 | 在某些设备上可能无效果 37 | 正在导入… 38 | 导入完成 39 | 输入图标库地址 40 | 从 URL 导入 41 | 清空图标 42 | 贡献者: 43 | 44 | AndroidNotifyIconAdapt, 46 | 其是由 @fankes 47 | 发起并维护的一个在线规则平台,旨在为国内 Android 不规范的 APP 和厂商适配原生通知图标与规范图标修复。 48 |
49 | 图标全部数据所有权属于 @fankes 与所有贡献者所有。 50 |
51 | 你也可以填入其他兼容的图标库地址。 52 | ]]> 53 |
54 | 55 | 保持 HMS Core 运行 56 | 该选项会为HMS Core显示一个前台通知,可长按通知将其隐藏 57 | 清除通知分类 58 | 操作已执行,请自行在通知分类页查看结果 59 | 60 | 隐藏应用图标 61 |
-------------------------------------------------------------------------------- /app/src/main/res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | com.huawei.hwid 5 | android 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | 11 | #018786 12 | #FFFFFF 13 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | HMSPush 3 | Enable HMSCore System Push Channel、Fake Huawei device 4 | HMS Core is not installed or has no permission to access 5 | HMSPush module not activated or HMS Core needs a reboot 6 | Module not activated or Application needs a reboot 7 | HMSPush module updated, HMS Core needs a reboot 8 | 9 | Launch 10 | App Info 11 | Search 12 | Unregister 13 | Settings 14 | Open HMS Core App Info 15 | Reboot HMS Core 16 | Trying to restart HMS Core 17 | Please restart HMS Core manually 18 | Reboot 19 | 20 | Disable Signature Verification 21 | LSPatch or modified applications need to disable signature verification 22 | 23 | Unregistered 24 | Registered 25 |   •  Latest Push: %s 26 | 27 | Confirm Unregister 28 | Confirm 29 | Cancel 30 | 31 | Keep History Messages 32 | Some Apps like QQ only show the latest message, turn on this switch to keep the history 33 | 34 | Custom Notification Icon 35 | Tint Icon Color On Notification Panel 36 | May not work on some device 37 | Importing… 38 | Import Complete 39 | Input Icon Library Url 40 | Import from URL 41 | Clear All Icon 42 | Contributors: 43 | 44 | AndroidNotifyIconAdapt, 46 | which is an online rule repository founded and maintained by @fankes, 47 | the project aims to adapt notification icons and repair non-standard APP and vendor icons for Android in China. 48 |
49 | All icon data is owned by @fankes and all contributors. 50 |
51 | You can also input other compatible icon library addresses. 52 | ]]> 53 |
54 | Keep HMS Core Alive 55 | This option will show a foreground notification for HMS Core, You can long press and hide it 56 | Clear notification categories 57 | Operation is done, please check the result on the Notification categories page 58 | Hide App Icon 59 |
-------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | //https://developer.android.com/jetpack/androidx/releases/compose-kotlin 4 | ext.kotlin_version = '1.8.10' 5 | ext.kotlin_compiler_extension_version = '1.4.3' 6 | 7 | repositories { 8 | mavenCentral() 9 | } 10 | dependencies { 11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 12 | } 13 | } 14 | plugins { 15 | id 'com.android.application' version '7.4.1' apply false 16 | id 'com.android.library' version '7.4.1' apply false 17 | id 'org.jetbrains.kotlin.android' version '1.7.10' apply false 18 | } 19 | 20 | task clean(type: Delete) { 21 | delete rootProject.buildDir 22 | } 23 | def getVersionCode = { -> 24 | try { 25 | def stdout = new ByteArrayOutputStream() 26 | exec { 27 | commandLine 'git', 'rev-list', '--first-parent', '--count', 'HEAD' 28 | standardOutput = stdout 29 | } 30 | return Integer.parseInt(stdout.toString().trim()) 31 | } catch (ignored) { 32 | return -1; 33 | } 34 | } 35 | 36 | def getVersionName = { -> 37 | try { 38 | def stdout = new ByteArrayOutputStream() 39 | exec { 40 | commandLine 'git', 'describe', '--tags', '--dirty' 41 | standardOutput = stdout 42 | } 43 | 44 | def versionName = stdout.toString().trim() 45 | if (versionName.startsWith("v")) { 46 | return versionName.substring(1) 47 | } else { 48 | return versionName 49 | } 50 | } catch (ignored) { 51 | return null; 52 | } 53 | } 54 | ext { 55 | gitVersionCode = getVersionCode() 56 | gitVersionName = getVersionName() 57 | APPLICATION_ID = "one.yufz.hmspush" 58 | 59 | COMPILE_SDK = 34 60 | MIN_SDK = 26 61 | TARGET_SDK = 34 62 | } -------------------------------------------------------------------------------- /common/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /common/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'kotlin-android' 4 | id 'kotlin-parcelize' 5 | } 6 | 7 | android { 8 | namespace 'one.yufz.hmspush.common' 9 | compileSdk COMPILE_SDK 10 | 11 | defaultConfig { 12 | minSdk MIN_SDK 13 | targetSdk TARGET_SDK 14 | 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | consumerProguardFiles "consumer-rules.pro" 17 | 18 | buildConfigField("String", "APPLICATION_ID", "\"$APPLICATION_ID\"") 19 | buildConfigField("String", "VERSION_NAME", "\"$gitVersionName\"") 20 | buildConfigField("int", "VERSION_CODE", "$gitVersionCode") 21 | } 22 | 23 | buildTypes { 24 | release { 25 | minifyEnabled false 26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 27 | } 28 | } 29 | compileOptions { 30 | sourceCompatibility JavaVersion.VERSION_1_8 31 | targetCompatibility JavaVersion.VERSION_1_8 32 | } 33 | kotlinOptions { 34 | jvmTarget = '1.8' 35 | } 36 | } 37 | 38 | dependencies { 39 | api "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4" 40 | } -------------------------------------------------------------------------------- /common/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -keepclassmembers class * implements one.yufz.hmspush.common.content.ContentModel { 2 | public (); 3 | public static final one.yufz.hmspush.common.content.ContentProperties PROPERTIES; 4 | } -------------------------------------------------------------------------------- /common/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /common/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /common/src/main/aidl/one/yufz/hmspush/common/HmsPushInterface.aidl: -------------------------------------------------------------------------------- 1 | // HmsPushInterface.aidl 2 | package one.yufz.hmspush.common; 3 | 4 | import one.yufz.hmspush.common.model.models; 5 | 6 | interface HmsPushInterface { 7 | ModuleVersionModel getModuleVersion(); 8 | List getPushSignList(); 9 | void unregisterPush(String packageName); 10 | List getPushHistoryList(); 11 | PrefsModel getPreference(); 12 | void updatePreference(in PrefsModel model); 13 | 14 | List getAllIcon(); 15 | void saveIcon(in IconModel iconModel); 16 | void deleteIcon(in String[] packageNames); 17 | 18 | boolean killHmsCore(); 19 | void clearHmsNotificationChannels(String packageName); 20 | } -------------------------------------------------------------------------------- /common/src/main/aidl/one/yufz/hmspush/common/model/models.aidl: -------------------------------------------------------------------------------- 1 | // models.aidl 2 | package one.yufz.hmspush.common.model; 3 | 4 | parcelable ModuleVersionModel; 5 | parcelable PushHistoryModel; 6 | parcelable PushSignModel; 7 | parcelable PrefsModel; 8 | parcelable IconModel; 9 | -------------------------------------------------------------------------------- /common/src/main/java/one/yufz/hmspush/common/BinderCursor.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.common 2 | 3 | import android.database.Cursor 4 | import android.database.MatrixCursor 5 | import android.os.Bundle 6 | import android.os.IBinder 7 | 8 | class BinderCursor(service: IBinder) : MatrixCursor(emptyArray()) { 9 | companion object { 10 | private const val KEY_BINDER = "binder" 11 | 12 | fun getBinder(cursor: Cursor): IBinder? { 13 | return cursor.extras.getBinder(KEY_BINDER) 14 | } 15 | } 16 | 17 | init { 18 | extras = Bundle().apply { 19 | putBinder(KEY_BINDER, service) 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /common/src/main/java/one/yufz/hmspush/common/BridgeUri.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.common 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.net.Uri 6 | 7 | const val AUTHORITIES = "com.huawei.hms" 8 | 9 | //android.content.ContentResolver#NOTIFY_NO_DELAY 10 | private const val NOTIFY_NO_DELAY = 1 shl 15 11 | 12 | enum class BridgeUri(val path: String) { 13 | PUSH_SIGN("hmspush/sign"), 14 | PUSH_HISTORY("hmspush/history"), 15 | DISABLE_SIGNATURE("hmspush/disableSignature"), 16 | HMS_PUSH_SERVICE("hmspush/service"); 17 | 18 | override fun toString(): String { 19 | return "content://$AUTHORITIES/$path" 20 | } 21 | 22 | @SuppressLint("WrongConstant") 23 | fun notifyContentChanged(context: Context) { 24 | context.contentResolver.notifyChange(toUri(), null, NOTIFY_NO_DELAY) 25 | } 26 | 27 | fun toUri(): Uri = Uri.parse(toString()) 28 | } -------------------------------------------------------------------------------- /common/src/main/java/one/yufz/hmspush/common/BridgeWrap.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.common 2 | 3 | import android.content.Context 4 | import android.database.ContentObserver 5 | import android.database.Cursor 6 | import android.net.Uri 7 | import kotlinx.coroutines.channels.awaitClose 8 | import kotlinx.coroutines.channels.trySendBlocking 9 | import kotlinx.coroutines.flow.Flow 10 | import kotlinx.coroutines.flow.callbackFlow 11 | 12 | object BridgeWrap { 13 | fun registerContentAsFlow(context: Context, uri: Uri, onChangedSendBlocking: () -> T): Flow = callbackFlow { 14 | val onChange: () -> Unit = { trySendBlocking(onChangedSendBlocking()) } 15 | 16 | onChange() 17 | 18 | val observer = ObserverWrap(onChange) 19 | 20 | registerObserve(context, uri, observer) 21 | 22 | awaitClose { 23 | unregisterObserve(context, observer) 24 | } 25 | } 26 | 27 | private class ObserverWrap(val observer: () -> Unit) : ContentObserver(null) { 28 | override fun onChange(selfChange: Boolean) { 29 | observer() 30 | } 31 | } 32 | 33 | fun registerObserve(context: Context, uri: Uri, observer: ContentObserver) { 34 | try { 35 | context.contentResolver.registerContentObserver(uri, false, observer) 36 | } catch (e: SecurityException) { 37 | e.printStackTrace() 38 | } 39 | } 40 | 41 | fun unregisterObserve(context: Context, observer: ContentObserver) { 42 | try { 43 | context.contentResolver.unregisterContentObserver(observer) 44 | } catch (e: SecurityException) { 45 | e.printStackTrace() 46 | } 47 | } 48 | 49 | fun query(context: Context, uri: Uri): Cursor? { 50 | return try { 51 | context.contentResolver.query(uri, null, null, null, null) 52 | } catch (e: SecurityException) { 53 | e.printStackTrace() 54 | null 55 | } 56 | } 57 | 58 | fun isDisableSignature(context: Context): Boolean { 59 | query(context, BridgeUri.DISABLE_SIGNATURE.toUri())?.use { 60 | val indexDisableSignature = it.getColumnIndex("disableSignature") 61 | 62 | if (it.moveToNext()) { 63 | return it.getInt(indexDisableSignature) == 1 64 | } 65 | } 66 | return false 67 | } 68 | } -------------------------------------------------------------------------------- /common/src/main/java/one/yufz/hmspush/common/Constant.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.common 2 | 3 | private const val TAG = "Constant" 4 | 5 | const val APPLICATION_ID = BuildConfig.APPLICATION_ID 6 | const val VERSION_NAME = BuildConfig.VERSION_NAME 7 | const val VERSION_CODE = BuildConfig.VERSION_CODE 8 | const val API_VERSION = 2 9 | 10 | const val ANDROID_PACKAGE_NAME = "android" 11 | 12 | const val HMS_PACKAGE_NAME = "com.huawei.hwid" 13 | const val HMS_CORE_PROCESS = "com.huawei.hwid.core" 14 | const val HMS_CORE_SERVICE = "com.huawei.hms.core.service.HMSCoreService" 15 | const val HMS_CORE_SERVICE_ACTION = "com.huawei.hms.core.aidlservice" 16 | const val HMS_CORE_PUSH_ACTION_REGISTRATION = "com.huawei.android.push.intent.REGISTRATION" 17 | const val HMS_CORE_PUSH_ACTION_NOTIFY_MSG = "com.huawei.push.msg.NOTIFY_MSG" 18 | const val HMS_CORE_DUMMY_ACTIVITY = "com.huawei.hms.core.activity.JumpActivity" 19 | const val FLAG_HMS_DUMMY_HOOKED = "hms_dummy_hooked" 20 | 21 | const val KEY_HMS_CORE_EXPLICIT_FOREGROUND = "explicit_foreground" 22 | 23 | const val IS_SYSTEM_HOOK_READY = "is_system_hook_ready" 24 | const val READY = "ready" 25 | 26 | const val HMS_CORE_SIGNATURE = "MIIEtTCCA52gAwIBAgIJAPIEVquWT6DwMA0GCSqGSIb3DQEBBQUAMIGYMQswCQYDVQQGEwJDTjESMBAGA1UECBMJR3Vhbmdkb25nMRIwEAYDVQQHEwlTaGVuZ3poZW4xDzANBgNVBAoTBkh1YXdlaTEYMBYGA1UECxMPVGVybWluYWxDb21wYW55MRQwEgYDVQQDEwtBbmRyb2lkVGVhbTEgMB4GCSqGSIb3DQEJARYRbW9iaWxlQGh1YXdlaS5jb20wHhcNMTEwNTI1MDYxMDQ5WhcNMzYwNTE4MDYxMDQ5WjCBmDELMAkGA1UEBhMCQ04xEjAQBgNVBAgTCUd1YW5nZG9uZzESMBAGA1UEBxMJU2hlbmd6aGVuMQ8wDQYDVQQKEwZIdWF3ZWkxGDAWBgNVBAsTD1Rlcm1pbmFsQ29tcGFueTEUMBIGA1UEAxMLQW5kcm9pZFRlYW0xIDAeBgkqhkiG9w0BCQEWEW1vYmlsZUBodWF3ZWkuY29tMIIBIDANBgkqhkiG9w0BAQEFAAOCAQ0AMIIBCAKCAQEA4CxauXorOopZliI83ga4Ky1P9bFcr2W4YNXHo9aJlasIYgu3WiL+dnOooaugPhe2UdH8TVy9uunnPu6vWh1NL7c+cAAjHg2yFm0Pxd2X5wX9ZlRsnaOO1O+izM3SOK0y45ghJCsBld8B2blyQtvyCe2o5EbgQyRLhOa/ynnXuzwZJM3SSO29YA7/j3MAGomkxmPbiXDjKIuUMVJMNh6FO4+ingTmHr5vvb2Hzb0+60ewJ7WFG96qE6I/Q5Z6Aw50fqQyZSy7NP3eYQSb9QYMgT+w6T9rrZ029NRVEZXqO7SekgGqbfl1rhaeIUkF3iV518w8PqxFlLFKwZ1+OcXCZwIBA6OCAQAwgf0wHQYDVR0OBBYEFD7GkN6BG8OeUaMDAa0jzzAG1n3gMIHNBgNVHSMEgcUwgcKAFD7GkN6BG8OeUaMDAa0jzzAG1n3goYGepIGbMIGYMQswCQYDVQQGEwJDTjESMBAGA1UECBMJR3Vhbmdkb25nMRIwEAYDVQQHEwlTaGVuZ3poZW4xDzANBgNVBAoTBkh1YXdlaTEYMBYGA1UECxMPVGVybWluYWxDb21wYW55MRQwEgYDVQQDEwtBbmRyb2lkVGVhbTEgMB4GCSqGSIb3DQEJARYRbW9iaWxlQGh1YXdlaS5jb22CCQDyBFarlk+g8DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBtrS/FkM8AeaxM4IZaiEMR3BatgydaKwMCQFd23R0fcEopmTyKE0qN/dVMWRUaBhVWEtvTAGRurPyfZPrC41JwmwNZ91avlsH1ZJUwTnIoe+R5igQzNWy8zdjVfN4ff/HABMuWKtWVsdoi7yBN4USQiGG7qWjgx0OB4RnHcrLPIsPQyDJanzHJeHsVbJR3GvZvT/sa2ZbD++dk87xQt6JkPCM3JhLyUJlGoDut+/6mH449KJIzhbvACHWs7Jm22SrEaPDcUMCZf/QJ46JdyNlmwg1YipcT/yF+LeSUV6Ms8j3xr1j0vN2UqPJrwckMWlGD22TUY1PdRhBHjHfyyJmI" 27 | 28 | const val HMSPUSH_PREF_NAME = "hmspush_pref" 29 | const val PREF_KEY_DISABLE_SIGNATURE = "disable_signature" -------------------------------------------------------------------------------- /common/src/main/java/one/yufz/hmspush/common/Context.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.common 2 | 3 | import android.content.Context 4 | 5 | fun Context.dp2px(dp: Number): Int { 6 | val scale = resources.displayMetrics.density 7 | return (dp.toFloat() * scale + 0.5f).toInt() 8 | } -------------------------------------------------------------------------------- /common/src/main/java/one/yufz/hmspush/common/HmsCoreUtil.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.common 2 | 3 | import android.content.ActivityNotFoundException 4 | import android.content.Context 5 | import android.content.Intent 6 | 7 | object HmsCoreUtil { 8 | fun startHmsCoreService(context: Context, foreground: Boolean) { 9 | val intent = createHmsCoreServiceIntent().apply { 10 | putExtra(KEY_HMS_CORE_EXPLICIT_FOREGROUND, foreground) 11 | } 12 | if (foreground) { 13 | context.startForegroundService(intent) 14 | } else { 15 | context.startService(intent) 16 | } 17 | } 18 | 19 | fun createHmsCoreServiceIntent(): Intent { 20 | return Intent(HMS_CORE_SERVICE_ACTION).apply { 21 | setClassName(HMS_PACKAGE_NAME, HMS_CORE_SERVICE) 22 | } 23 | } 24 | 25 | fun createHmsCoreDummyActivityIntent(): Intent { 26 | return Intent().apply { 27 | setClassName(HMS_PACKAGE_NAME, HMS_CORE_DUMMY_ACTIVITY) 28 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 29 | putExtra(FLAG_HMS_DUMMY_HOOKED, true) 30 | } 31 | } 32 | 33 | fun startHmsCoreDummyActivity(context: Context) { 34 | try { 35 | context.startActivity(createHmsCoreDummyActivityIntent()) 36 | } catch (e: ActivityNotFoundException) { 37 | e.printStackTrace() 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /common/src/main/java/one/yufz/hmspush/common/IconData.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.common 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.graphics.BitmapFactory 6 | import android.graphics.Color 7 | import android.util.Base64 8 | import org.json.JSONObject 9 | import java.io.ByteArrayOutputStream 10 | import kotlin.math.max 11 | 12 | data class IconData( 13 | val appName: String, 14 | val packageName: String, 15 | val iconBitmap: Bitmap, 16 | val iconColor: Int?, 17 | val contributorName: String? 18 | ) { 19 | companion object { 20 | fun fromJson(json: String): IconData { 21 | return fromJson(JSONObject(json)) 22 | } 23 | 24 | fun fromJson(obj: JSONObject): IconData { 25 | return IconData( 26 | appName = obj.getString("appName"), 27 | packageName = obj.getString("packageName"), 28 | iconBitmap = parseBitmap(obj.getString("iconBitmap")), 29 | iconColor = parseColor(obj.optString("iconColor")), 30 | contributorName = obj.optString("contributorName") 31 | ) 32 | } 33 | 34 | fun IconData.scaleForNotification(context: Context): IconData { 35 | val size = max(iconBitmap.width, iconBitmap.height) 36 | val dp24 = (context.resources.displayMetrics.density * 24 + 0.5f).toInt() 37 | if (size >= dp24) { 38 | return this 39 | } 40 | 41 | val scale = dp24 / size.toFloat() 42 | return this.copy( 43 | iconBitmap = Bitmap.createScaledBitmap( 44 | iconBitmap, 45 | (iconBitmap.width * scale).toInt(), 46 | (iconBitmap.height * scale).toInt(), 47 | false 48 | ) 49 | ) 50 | } 51 | 52 | private fun parseColor(colorString: String?): Int? { 53 | if (colorString == null || colorString.isEmpty()) { 54 | return null 55 | } 56 | 57 | return try { 58 | Color.parseColor(colorString) 59 | } catch (e: IllegalArgumentException) { 60 | e.printStackTrace() 61 | return null 62 | } 63 | } 64 | 65 | private fun parseBitmap(content: String): Bitmap { 66 | val bytes = Base64.decode(content, Base64.DEFAULT) 67 | return BitmapFactory.decodeByteArray(bytes, 0, bytes.size) 68 | } 69 | 70 | private fun Bitmap.toBase64(): String { 71 | val outputStream = ByteArrayOutputStream() 72 | compress(Bitmap.CompressFormat.PNG, 100, outputStream) 73 | return Base64.encodeToString(outputStream.toByteArray(), Base64.DEFAULT) 74 | } 75 | 76 | private fun Int.toHexColorString(): String = String.format( 77 | "#%02X%02X%02X%02X", 78 | Color.alpha(this), 79 | Color.red(this), 80 | Color.green(this), 81 | Color.blue(this) 82 | ) 83 | } 84 | 85 | fun toJson(): String { 86 | val obj = JSONObject().apply { 87 | put("appName", appName) 88 | put("packageName", packageName) 89 | put("iconBitmap", iconBitmap.toBase64()) 90 | put("iconColor", iconColor?.toHexColorString()) 91 | put("contributorName", contributorName) 92 | } 93 | return obj.toString() 94 | } 95 | } 96 | 97 | -------------------------------------------------------------------------------- /common/src/main/java/one/yufz/hmspush/common/Util.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.common 2 | 3 | import java.util.* 4 | 5 | private val objMap: MutableMap> = WeakHashMap() 6 | 7 | fun Any.doOnce(action: () -> Unit) { 8 | doOnce(this, action) 9 | } 10 | 11 | fun Any.doOnce(key: Any, action: () -> Unit) { 12 | val keyMap = synchronized(objMap) { 13 | objMap.getOrPut(this) { HashMap() } 14 | } 15 | 16 | synchronized(keyMap) { 17 | if (keyMap.containsKey(key)) { 18 | return 19 | } 20 | keyMap[key] = true 21 | } 22 | action() 23 | } -------------------------------------------------------------------------------- /common/src/main/java/one/yufz/hmspush/common/content/Content.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.common.content 2 | 3 | import android.content.ContentValues 4 | import android.content.SharedPreferences 5 | import android.database.Cursor 6 | import android.database.MatrixCursor 7 | import kotlin.reflect.KClass 8 | 9 | private val CACHE = HashMap, ContentProperties>() 10 | 11 | fun KClass.getContentProperties(): ContentProperties { 12 | @Suppress("UNCHECKED_CAST") return CACHE.getOrPut(this) { java.getField("PROPERTIES").get(null) as ContentProperties } 13 | } 14 | 15 | fun ContentModel.getContentProperties(): ContentProperties { 16 | return this::class.getContentProperties() 17 | } 18 | 19 | fun ContentModel.toContentValues(): ContentValues { 20 | val values = ContentValues() 21 | 22 | getContentProperties().properties.forEach { (name, prop) -> 23 | values.putValue(name, prop.type, prop.get(this)) 24 | } 25 | 26 | return values 27 | } 28 | 29 | inline fun ContentValues.toContent() = toContent(T::class) 30 | 31 | fun ContentValues.toContent(type: KClass): T { 32 | val model = type.java.newInstance() 33 | 34 | type.getContentProperties().properties.forEach { (name, prop) -> 35 | prop.set(model, get(name)) 36 | } 37 | return model 38 | } 39 | 40 | inline fun Cursor.toContent() = toContent(T::class) 41 | 42 | fun Cursor.toContent(type: KClass): T { 43 | val model = type.java.newInstance() 44 | 45 | if (position == -1) { 46 | if (!moveToNext()) { 47 | return model 48 | } 49 | } 50 | 51 | type.getContentProperties().properties.forEach { (name, prop) -> 52 | val columnIndex = getColumnIndex(name) 53 | 54 | if (columnIndex == -1) return@forEach 55 | if (isNull(columnIndex)) return@forEach 56 | prop.set(model, getValue(columnIndex, prop.type)) 57 | } 58 | return model 59 | } 60 | 61 | fun ContentModel.toCursor(cursor: MatrixCursor = MatrixCursor(getContentProperties().properties.keys.toTypedArray())): Cursor { 62 | val newRow = cursor.newRow() 63 | getContentProperties().properties.forEach { (name, prop) -> 64 | val value = prop.get(this) 65 | 66 | if (value is Boolean) { 67 | newRow.add(name, if (value) 1 else 0) 68 | } else { 69 | newRow.add(name, value) 70 | } 71 | } 72 | return cursor 73 | } 74 | 75 | inline fun Cursor.toContentList() = inflateCollection(ArrayList(), T::class) 76 | 77 | inline fun Cursor.toContentSet() = inflateCollection(HashSet(), T::class) 78 | 79 | fun > Cursor.inflateCollection(collection: C, type: KClass): C { 80 | while (moveToNext()) { 81 | collection.add(toContent(type)) 82 | } 83 | return collection 84 | } 85 | 86 | inline fun Iterable.toCursor() = toCursor(T::class) 87 | 88 | fun Iterable.toCursor(type: KClass): Cursor { 89 | val cursor = MatrixCursor(type.getContentProperties().properties.keys.toTypedArray()) 90 | forEach { it.toCursor(cursor) } 91 | return cursor 92 | } 93 | 94 | inline fun SharedPreferences.toContent(): T { 95 | return toContent(T::class) 96 | } 97 | 98 | fun SharedPreferences.toContent(type: KClass): T { 99 | val model = type.java.newInstance() 100 | 101 | type.getContentProperties().properties.forEach { (name, prop) -> 102 | prop.set(model, getValue(name, prop.type)) 103 | } 104 | 105 | return model 106 | } 107 | 108 | fun ContentModel.storeToSharedPreference(editor: SharedPreferences.Editor) { 109 | getContentProperties().properties.forEach { (name, prop) -> 110 | editor.putValue(name, prop.type, prop.get(this)) 111 | } 112 | } -------------------------------------------------------------------------------- /common/src/main/java/one/yufz/hmspush/common/content/ContentModel.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.common.content 2 | 3 | interface ContentModel -------------------------------------------------------------------------------- /common/src/main/java/one/yufz/hmspush/common/content/ContentProperties.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.common.content 2 | 3 | import kotlin.reflect.KClass 4 | import kotlin.reflect.KMutableProperty1 5 | 6 | open class ContentProperties(val properties: Map>) { 7 | class Property(val type: KClass<*>, val nullable: Boolean, private val property: KMutableProperty1) { 8 | fun set(receiver: M, value: Any?) { 9 | if (value != null) { 10 | property.set(receiver, value) 11 | } else if (nullable) { 12 | property.set(receiver, null) 13 | } 14 | } 15 | 16 | inline fun getValue(receiver: M): T? { 17 | return get(receiver) as T? 18 | } 19 | 20 | fun get(receiver: M): Any? { 21 | val value = property.get(receiver) 22 | if (!nullable && value == null) { 23 | throw IllegalStateException("Property [$property] is NonNull, but got null") 24 | } 25 | return value 26 | } 27 | } 28 | 29 | class Builder { 30 | private val properties: HashMap> = LinkedHashMap() 31 | 32 | inline fun property(name: String, property: KMutableProperty1): Builder { 33 | return property(name, T::class, false, property) 34 | } 35 | 36 | inline fun nullableProperty(name: String, property: KMutableProperty1): Builder { 37 | return property(name, T::class, true, property) 38 | } 39 | 40 | fun property(name: String, type: KClass<*>, nullable: Boolean, property: KMutableProperty1): Builder { 41 | @Suppress("UNCHECKED_CAST") 42 | properties[name] = Property(type, nullable, property as KMutableProperty1) 43 | return this 44 | } 45 | 46 | fun build(): ContentProperties { 47 | return ContentProperties(properties) 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /common/src/main/java/one/yufz/hmspush/common/content/ContentValues.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.common.content 2 | 3 | import android.content.ContentValues 4 | import kotlin.reflect.KClass 5 | 6 | fun ContentValues.putValue(key: String?, type: KClass<*>, value: Any?) { 7 | when (type) { 8 | String::class -> put(key, value as String?) 9 | Byte::class -> put(key, value as Byte?) 10 | Short::class -> put(key, value as Short?) 11 | Int::class -> put(key, value as Int?) 12 | Long::class -> put(key, value as Long?) 13 | Float::class -> put(key, value as Float?) 14 | Double::class -> put(key, value as Double?) 15 | Boolean::class -> put(key, value as Boolean?) 16 | ByteArray::class -> put(key, value as ByteArray?) 17 | else -> throw IllegalStateException("Unsupported type: $type") 18 | } 19 | } -------------------------------------------------------------------------------- /common/src/main/java/one/yufz/hmspush/common/content/Cursor.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.common.content 2 | 3 | import android.database.Cursor 4 | import kotlin.reflect.KClass 5 | 6 | fun Cursor.getValue(columnIndex: Int, type: KClass<*>): Any { 7 | return when (type) { 8 | String::class -> getString(columnIndex) 9 | Short::class -> getShort(columnIndex) 10 | Int::class -> getInt(columnIndex) 11 | Long::class -> getLong(columnIndex) 12 | Float::class -> getFloat(columnIndex) 13 | Double::class -> getDouble(columnIndex) 14 | Boolean::class -> getInt(columnIndex) == 1 15 | ByteArray::class -> getBlob(columnIndex) 16 | else -> throw IllegalStateException("Unsupported type: $type") 17 | } 18 | } -------------------------------------------------------------------------------- /common/src/main/java/one/yufz/hmspush/common/content/SharedPreference.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.common.content 2 | 3 | import android.content.SharedPreferences 4 | import kotlin.reflect.KClass 5 | 6 | fun SharedPreferences.getValue(key: String, type: KClass<*>): Any? { 7 | if (!contains(key)) return null 8 | 9 | return when (type) { 10 | String::class -> getString(key, null) 11 | Short::class -> getInt(key, 0).toShort() 12 | Int::class -> getInt(key, 0) 13 | Long::class -> getLong(key, 0L) 14 | Float::class -> getFloat(key, 0F) 15 | Boolean::class -> getBoolean(key, false) 16 | else -> throw IllegalStateException("Unsupported type: $type") 17 | } 18 | } 19 | 20 | fun SharedPreferences.Editor.putValue(key: String, type: KClass<*>, value: Any?): SharedPreferences.Editor { 21 | when (type) { 22 | String::class -> putString(key, value as String?) 23 | Short::class -> putInt(key, value as Int) 24 | Int::class -> putInt(key, value as Int) 25 | Long::class -> putLong(key, value as Long) 26 | Float::class -> putFloat(key, value as Float) 27 | Boolean::class -> putBoolean(key, value as Boolean) 28 | else -> throw IllegalStateException("Unsupported type: $type") 29 | } 30 | return this 31 | } -------------------------------------------------------------------------------- /common/src/main/java/one/yufz/hmspush/common/model/IconModel.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.common.model 2 | 3 | import android.os.ParcelFileDescriptor 4 | import android.os.Parcelable 5 | import kotlinx.parcelize.Parcelize 6 | import one.yufz.hmspush.common.IconData 7 | import java.io.FileReader 8 | 9 | @Parcelize 10 | class IconModel(val packageName: String, val iconData: String? = null, val dataFD: ParcelFileDescriptor? = null) : Parcelable { 11 | 12 | fun toIconData(): IconData? { 13 | if (iconData != null) { 14 | return try { 15 | IconData.fromJson(iconData) 16 | } catch (e: Throwable) { 17 | e.printStackTrace() 18 | null 19 | } 20 | } 21 | 22 | if (dataFD != null) { 23 | try { 24 | FileReader(dataFD.fileDescriptor).use { 25 | return IconData.fromJson(it.readText()) 26 | } 27 | } catch (t: Throwable) { 28 | t.printStackTrace() 29 | return null 30 | } 31 | } 32 | throw IllegalStateException("iconData and dataFD all be null") 33 | } 34 | } -------------------------------------------------------------------------------- /common/src/main/java/one/yufz/hmspush/common/model/ModuleVersionModel.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.common.model 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | 6 | @Parcelize 7 | class ModuleVersionModel( 8 | var versionName: String, 9 | var versionCode: Int = -1, 10 | var apiVersion: Int = 0 11 | ) : Parcelable -------------------------------------------------------------------------------- /common/src/main/java/one/yufz/hmspush/common/model/PrefsModel.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.common.model 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | import one.yufz.hmspush.common.content.ContentModel 6 | import one.yufz.hmspush.common.content.ContentProperties 7 | 8 | @Parcelize 9 | data class PrefsModel constructor( 10 | var disableSignature: Boolean, 11 | var groupMessageById: Boolean, 12 | var useCustomIcon: Boolean, 13 | var tintIconColor: Boolean, 14 | var keepAlive: Boolean, 15 | var hideAppIcon: Boolean, 16 | ) : ContentModel, Parcelable { 17 | 18 | constructor() : this( 19 | disableSignature = false, 20 | groupMessageById = true, 21 | useCustomIcon = false, 22 | tintIconColor = true, 23 | keepAlive = false, 24 | hideAppIcon = false, 25 | ) 26 | 27 | companion object { 28 | @JvmField 29 | val PROPERTIES = ContentProperties.Builder() 30 | .property("disableSignature", PrefsModel::disableSignature) 31 | .property("groupMessageById", PrefsModel::groupMessageById) 32 | .property("useCustomIcon", PrefsModel::useCustomIcon) 33 | .property("tintIconColor", PrefsModel::tintIconColor) 34 | .property("keepAlive", PrefsModel::keepAlive) 35 | .property("hideAppIcon", PrefsModel::hideAppIcon) 36 | .build() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /common/src/main/java/one/yufz/hmspush/common/model/PushHistoryModel.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.common.model 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | 6 | @Parcelize 7 | class PushHistoryModel constructor(var packageName: String, var pushTime: Long) : Parcelable -------------------------------------------------------------------------------- /common/src/main/java/one/yufz/hmspush/common/model/PushSignModel.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.common.model 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | 6 | @Parcelize 7 | class PushSignModel(var packageName: String, var userId: Int) : Parcelable -------------------------------------------------------------------------------- /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 | # Enables namespacing of each library's R class so that its R class includes only the 19 | # resources declared in the library itself and none from the library's dependencies, 20 | # thereby reducing the size of the R class for that library 21 | android.nonTransitiveRClass=true 22 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fei-ke/HMSPush/939d07d4eaa3961ed50b6c23699b96e63aefa958/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Jan 02 23:49:55 CST 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or 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 UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /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 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | maven { url = "https://api.xposed.info/" } 14 | } 15 | } 16 | rootProject.name = "HMSPush" 17 | include ':app' 18 | include ':xposed' 19 | include ':common' 20 | -------------------------------------------------------------------------------- /xposed/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /xposed/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | namespace 'one.yufz.hmspush.xposed' 8 | compileSdk COMPILE_SDK 9 | 10 | defaultConfig { 11 | minSdk MIN_SDK 12 | targetSdk TARGET_SDK 13 | 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | consumerProguardFiles "consumer-rules.pro" 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | compileOptions { 25 | sourceCompatibility JavaVersion.VERSION_1_8 26 | targetCompatibility JavaVersion.VERSION_1_8 27 | } 28 | kotlinOptions { 29 | jvmTarget = '1.8' 30 | } 31 | } 32 | 33 | dependencies { 34 | implementation project(":common") 35 | compileOnly 'de.robv.android.xposed:api:82' 36 | implementation 'org.lsposed.hiddenapibypass:hiddenapibypass:4.3' 37 | } -------------------------------------------------------------------------------- /xposed/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | # Proguard for Xposed. 2 | -keep class * implements de.robv.android.xposed.IXposedHookZygoteInit 3 | -keep class * implements de.robv.android.xposed.IXposedHookLoadPackage 4 | -keep class * implements de.robv.android.xposed.IXposedHookInitPackageResources 5 | 6 | -keep class one.yufz.hmspush.hook.XposedMod{ 7 | *; 8 | } 9 | -keep class com.huawei.android.app.NotificationManagerEx{ 10 | *; 11 | } -------------------------------------------------------------------------------- /xposed/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /xposed/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/I18n.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook 2 | 3 | import android.content.Context 4 | import java.util.* 5 | 6 | sealed interface I18n { 7 | companion object { 8 | fun get(context: Context): I18n { 9 | return when (context.resources.configuration.locales.get(0).language) { 10 | Locale.CHINESE.language -> Chinese 11 | else -> Default 12 | } 13 | } 14 | } 15 | 16 | val hmsCoreRunning: String 17 | val hmsCoreRunningState: String 18 | val dummyFragmentDesc: String 19 | val tipsOptimizeBattery: String 20 | } 21 | 22 | object Chinese : I18n { 23 | override val hmsCoreRunning = "HMS Core 正在后台运行" 24 | override val hmsCoreRunningState = "HMS Core 运行状态" 25 | override val dummyFragmentDesc = "这是一个空白页面,你可以将该页面在最近任务中锁定,以帮助 HMS Core 保持后台运行" 26 | override val tipsOptimizeBattery = "建议对 HMS Core 关闭电池优化,以帮助 HMS Core 保持后台运行" 27 | } 28 | 29 | object Default : I18n { 30 | override val hmsCoreRunning = "HMS Core is running in the background" 31 | override val hmsCoreRunningState = "HMS Core Running State" 32 | override val dummyFragmentDesc = "This is a blank page, you can lock this page in recent tasks to help HMS Core keep running in the background" 33 | override val tipsOptimizeBattery = "It is recommended to turn off battery optimization for HMS Core to help keep it running in the background" 34 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/XLog.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook 2 | 3 | import android.util.Log 4 | import de.robv.android.xposed.XC_MethodHook 5 | import de.robv.android.xposed.XposedBridge 6 | import java.lang.reflect.Method 7 | 8 | object XLog { 9 | fun d(tag: String, message: String?) { 10 | XposedBridge.log("[HMSPush] $tag $message") 11 | } 12 | 13 | fun i(tag: String, message: String?) { 14 | XposedBridge.log("[HMSPush] $tag $message") 15 | } 16 | 17 | fun e(tag: String, message: String?, throwable: Throwable?) { 18 | i(tag, message) 19 | i(tag, Log.getStackTraceString(throwable)) 20 | } 21 | 22 | fun XC_MethodHook.MethodHookParam.logMethod(tag: String, stackTrace: Boolean = false) { 23 | d(tag, "╔═══════════════════════════════════════════════════════") 24 | d(tag, method.toString()) 25 | d(tag, "${method.name} called with ${args.contentDeepToString()}") 26 | if (stackTrace) { 27 | d(tag, Log.getStackTraceString(Throwable())) 28 | } 29 | if (hasThrowable()) { 30 | e(tag, "${method.name} thrown", throwable) 31 | } else if (method is Method && (method as Method).returnType != Void.TYPE) { 32 | d(tag, "${method.name} return $result") 33 | } 34 | d(tag, "╚═══════════════════════════════════════════════════════") 35 | } 36 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/XposedMod.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook 2 | 3 | import de.robv.android.xposed.IXposedHookLoadPackage 4 | import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam 5 | import one.yufz.hmspush.common.ANDROID_PACKAGE_NAME 6 | import one.yufz.hmspush.common.HMS_CORE_PROCESS 7 | import one.yufz.hmspush.common.HMS_PACKAGE_NAME 8 | import one.yufz.hmspush.common.doOnce 9 | import one.yufz.hmspush.hook.fakedevice.FakeDevice 10 | import one.yufz.hmspush.hook.hms.HookHMS 11 | import one.yufz.hmspush.hook.system.HookSystemService 12 | 13 | class XposedMod : IXposedHookLoadPackage { 14 | companion object { 15 | private const val TAG = "XposedMod" 16 | } 17 | 18 | @Throws(Throwable::class) 19 | override fun handleLoadPackage(lpparam: LoadPackageParam) { 20 | doOnce(lpparam.classLoader) { 21 | hook(lpparam) 22 | } 23 | } 24 | 25 | private fun hook(lpparam: LoadPackageParam) { 26 | XLog.d(TAG, "Loaded app: " + lpparam.packageName + " process:" + lpparam.processName) 27 | 28 | if (lpparam.processName == ANDROID_PACKAGE_NAME) { 29 | if (lpparam.packageName == ANDROID_PACKAGE_NAME) { 30 | HookSystemService().hook(lpparam.classLoader) 31 | } 32 | return 33 | } 34 | 35 | if (lpparam.packageName == HMS_PACKAGE_NAME) { 36 | if (lpparam.processName == HMS_CORE_PROCESS) { 37 | HookHMS().hook(lpparam) 38 | } 39 | return 40 | } 41 | 42 | if (lpparam.packageName == "com.android.systemui") { 43 | return 44 | } 45 | 46 | FakeDevice.fake(lpparam) 47 | } 48 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/bridge/BridgeContentProvider.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.bridge 2 | 3 | import android.content.UriMatcher 4 | import android.database.Cursor 5 | import android.database.MatrixCursor 6 | import android.net.Uri 7 | import one.yufz.hmspush.common.AUTHORITIES 8 | import one.yufz.hmspush.common.BinderCursor 9 | import one.yufz.hmspush.common.BridgeUri 10 | import one.yufz.hmspush.hook.XLog 11 | import one.yufz.hmspush.hook.hms.HmsPushService 12 | import one.yufz.hmspush.hook.hms.Prefs 13 | 14 | class BridgeContentProvider { 15 | companion object { 16 | private const val TAG = "BridgeContentProvider" 17 | 18 | private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH) 19 | 20 | init { 21 | BridgeUri.values().forEach { 22 | uriMatcher.addURI(AUTHORITIES, it.path, it.ordinal) 23 | } 24 | } 25 | } 26 | 27 | fun query(uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?, sortOrder: String?): Cursor? { 28 | XLog.d(TAG, "query() called with: uri = $uri, projection = $projection, selection = $selection, selectionArgs = $selectionArgs, sortOrder = $sortOrder") 29 | 30 | val code = uriMatcher.match(uri) 31 | return if (code != -1) { 32 | when (BridgeUri.values()[code]) { 33 | BridgeUri.DISABLE_SIGNATURE -> queryIsDisableSignature() 34 | BridgeUri.HMS_PUSH_SERVICE -> BinderCursor(HmsPushService) 35 | else -> throw IllegalStateException("Unsupported") 36 | } 37 | } else { 38 | null 39 | } 40 | } 41 | 42 | private fun queryIsDisableSignature(): Cursor? { 43 | return MatrixCursor(arrayOf("disableSignature")).apply { 44 | addRow(arrayOf(if (Prefs.isDisableSignature()) 1 else 0)) 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/bridge/HookContentProvider.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.bridge 2 | 3 | import android.app.AndroidAppHelper 4 | import android.net.Uri 5 | import android.os.Binder 6 | import one.yufz.hmspush.common.APPLICATION_ID 7 | import one.yufz.xposed.findClass 8 | import one.yufz.xposed.hookMethod 9 | 10 | class HookContentProvider { 11 | fun hook(classLoader: ClassLoader) { 12 | val classModuleQueryProvider = classLoader.findClass("com.huawei.hms.dynamic.module.manager.query.ModuleQueryProvider") 13 | 14 | val bridge = BridgeContentProvider() 15 | // public abstract @Nullable Cursor query(@NonNull Uri uri, @Nullable String[] projection, 16 | // @Nullable String selection, @Nullable String[] selectionArgs, 17 | // @Nullable String sortOrder); 18 | classModuleQueryProvider.hookMethod("query", Uri::class.java, Array::class.java, String::class.java, Array::class.java, String::class.java) { 19 | doBefore { 20 | result = bridge.query(args[0] as Uri, args[1] as Array?, args[2] as String?, args[3] as Array?, args[4] as String?) 21 | } 22 | } 23 | } 24 | 25 | private fun fromHmsPush() = try { 26 | val callingUid = Binder.getCallingUid() 27 | callingUid == AndroidAppHelper.currentApplication().packageManager.getPackageUid(APPLICATION_ID, 0) 28 | || callingUid == 2000 || callingUid == 0 29 | } catch (e: Throwable) { 30 | false 31 | } 32 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/fakedevice/Alipay.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.fakedevice 2 | 3 | import de.robv.android.xposed.callbacks.XC_LoadPackage 4 | import one.yufz.xposed.findClass 5 | import one.yufz.xposed.hook 6 | 7 | class Alipay : IFakeDevice { 8 | override fun fake(lpparam: XC_LoadPackage.LoadPackageParam): Boolean { 9 | lpparam.classLoader.findClass("com.alipay.pushsdk.thirdparty.hw.HuaWeiPushWorker") 10 | .declaredMethods 11 | .find { it.returnType == Boolean::class.java } 12 | ?.hook { replace { true } } 13 | 14 | return true 15 | } 16 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/fakedevice/Common.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.fakedevice 2 | 3 | import de.robv.android.xposed.callbacks.XC_LoadPackage 4 | import one.yufz.hmspush.hook.XLog 5 | 6 | open class Common : IFakeDevice { 7 | companion object { 8 | private const val TAG = "Common" 9 | } 10 | 11 | override fun fake(lpparam: XC_LoadPackage.LoadPackageParam): Boolean { 12 | XLog.d(TAG, "fake() called with: packageName = ${lpparam.packageName}") 13 | fakeAllBuildInProperties() 14 | return true 15 | } 16 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/fakedevice/CoolApk.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.fakedevice 2 | 3 | import android.app.Application 4 | import android.content.ComponentName 5 | import android.content.Context 6 | import android.content.pm.PackageManager 7 | import de.robv.android.xposed.callbacks.XC_LoadPackage 8 | import one.yufz.hmspush.hook.XLog 9 | import one.yufz.xposed.hookMethod 10 | 11 | class CoolApk : XGPush() { 12 | companion object { 13 | private const val TAG = "CoolApk" 14 | } 15 | 16 | override fun fake(lpparam: XC_LoadPackage.LoadPackageParam): Boolean { 17 | Application::class.java.hookMethod("attach", Context::class.java) { 18 | doAfter { 19 | super.fake(lpparam) 20 | 21 | val context = thisObject as Context 22 | listOf( 23 | "com.huawei.agconnect.core.provider.AGConnectInitializeProvider", 24 | "com.huawei.agconnect.core.ServiceDiscovery", 25 | "com.tencent.android.hwpushv3.HWHmsMessageService", 26 | "com.huawei.hms.support.api.push.PushMsgReceiver", 27 | "com.huawei.hms.support.api.push.PushReceiver", 28 | "com.huawei.hms.support.api.push.PushProvider", 29 | ) 30 | .map { ComponentName(context, it) } 31 | .filter { context.packageManager.getComponentEnabledSetting(it) == PackageManager.COMPONENT_ENABLED_STATE_DISABLED } 32 | .forEach { 33 | try { 34 | context.packageManager.setComponentEnabledSetting(it, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 0) 35 | } catch (t: Throwable) { 36 | XLog.d(TAG, t.message) 37 | } 38 | } 39 | } 40 | } 41 | return true 42 | } 43 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/fakedevice/DouYin.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.fakedevice 2 | 3 | import de.robv.android.xposed.callbacks.XC_LoadPackage 4 | import one.yufz.hmspush.hook.XLog 5 | import one.yufz.hmspush.hook.XLog.logMethod 6 | import one.yufz.xposed.findClass 7 | import one.yufz.xposed.hookMethod 8 | 9 | 10 | class DouYin : Common() { 11 | companion object { 12 | private const val TAG = "DouYin" 13 | } 14 | 15 | override fun fake(lpparam: XC_LoadPackage.LoadPackageParam): Boolean { 16 | super.fake(lpparam) 17 | //public java.lang.String com.bytedance.common.network.DefaultNetWorkClient.post(java.lang.String,java.util.List,java.util.Map,com.bytedance.common.utility.NetworkClient$ReqContext) 18 | val classAppLogNetworkClient = lpparam.classLoader.findClass("com.ss.android.ugc.aweme.statistic.AppLogNetworkClient") 19 | val classReqContext = lpparam.classLoader.findClass("com.bytedance.common.utility.NetworkClient\$ReqContext") 20 | classAppLogNetworkClient.hookMethod("post", String::class.java, List::class.java, Map::class.java, classReqContext) { 21 | doBefore { 22 | val url = args[0] as String 23 | if (!url.contains("/cloudpush/update_sender/")) return@doBefore 24 | 25 | //&rom=EMUI-EmotionUI_xxx 26 | if (!url.contains("rom=EMUI-")) { 27 | XLog.d(TAG, "update_sender: url = $url") 28 | val fakeEmuiRom = "EMUI-${Property.EMUI_VERSION.value}" 29 | args[0] = url.replace("rom=[^&]*&".toRegex(), "rom=$fakeEmuiRom&") 30 | } 31 | } 32 | doAfter { 33 | val url = args[0] as String 34 | if (!url.contains("/cloudpush/update_sender/")) return@doAfter 35 | 36 | logMethod(TAG) 37 | } 38 | } 39 | return true 40 | } 41 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/fakedevice/FakeDevice.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.fakedevice 2 | 3 | import de.robv.android.xposed.callbacks.XC_LoadPackage 4 | import one.yufz.hmspush.common.BridgeWrap 5 | import one.yufz.hmspush.hook.XLog 6 | import one.yufz.xposed.onApplicationAttachContext 7 | 8 | object FakeDevice { 9 | private const val TAG = "FakeDevice" 10 | 11 | private val Default = arrayOf(Common::class.java) 12 | private val FakeDeviceConfig: Map>> = mapOf( 13 | "com.coolapk.market" to arrayOf(CoolApk::class.java), 14 | "com.tencent.mobileqq" to arrayOf(QQ::class.java), 15 | "com.tencent.tim" to arrayOf(QQ::class.java), 16 | "com.sankuai.meituan" to arrayOf(FakeEmuiOnly::class.java), 17 | "com.sankuai.meituan.takeoutnew" to arrayOf(FakeEmuiOnly::class.java), 18 | "com.dianping.v1" to arrayOf(FakeEmuiOnly::class.java), 19 | "com.eg.android.AlipayGphone" to arrayOf(Alipay::class.java), 20 | "com.xunmeng.pinduoduo" to arrayOf(PinDuoDuo::class.java), 21 | "com.ss.android.ugc.aweme" to arrayOf(DouYin::class.java), 22 | "com.tencent.tmgp.sgame" to arrayOf(XGPush::class.java), 23 | ) 24 | 25 | fun fake(lpparam: XC_LoadPackage.LoadPackageParam) { 26 | XLog.d(TAG, "fake() called with: packageName = ${lpparam.packageName}, processName = ${lpparam.processName}") 27 | if (lpparam.packageName == "com.google.android.webview") { 28 | XLog.d(TAG, "fake() called, ignore ${lpparam.packageName}") 29 | return 30 | } 31 | 32 | val fakes = FakeDeviceConfig[lpparam.packageName] ?: Default 33 | fakes.forEach { it.newInstance().fake(lpparam) } 34 | 35 | fakeOthers(lpparam) 36 | } 37 | 38 | private fun fakeOthers(lpparam: XC_LoadPackage.LoadPackageParam) { 39 | onApplicationAttachContext { 40 | XLog.d(TAG, "${this}.attachBaseContext() called") 41 | try { 42 | if (BridgeWrap.isDisableSignature(this)) { 43 | FakeHmsSignature.hook(lpparam) 44 | } 45 | } catch (t: Throwable) { 46 | XLog.e(TAG, "disable signature error", t) 47 | } 48 | HookHmsDeviceId.hook(lpparam) 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/fakedevice/FakeEmuiOnly.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.fakedevice 2 | 3 | import de.robv.android.xposed.callbacks.XC_LoadPackage 4 | 5 | class FakeEmuiOnly : IFakeDevice { 6 | override fun fake(lpparam: XC_LoadPackage.LoadPackageParam): Boolean { 7 | fakeProperty(Property.EMUI_VERSION) 8 | return true 9 | } 10 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/fakedevice/FakeHmsSignature.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.fakedevice 2 | 3 | import android.content.pm.PackageInfo 4 | import android.util.Base64 5 | import dalvik.system.DexClassLoader 6 | import de.robv.android.xposed.XC_MethodHook.Unhook 7 | import de.robv.android.xposed.XposedHelpers 8 | import de.robv.android.xposed.callbacks.XC_LoadPackage 9 | import one.yufz.hmspush.common.HMS_CORE_SIGNATURE 10 | import one.yufz.hmspush.common.HMS_PACKAGE_NAME 11 | import one.yufz.hmspush.hook.XLog 12 | import one.yufz.xposed.* 13 | 14 | object FakeHmsSignature { 15 | private const val TAG = "FakeHmsSignature" 16 | 17 | private var verifyApkHashHooked = false 18 | private var verifyApkHashUnhook: Unhook? = null 19 | 20 | fun hook(lpparam: XC_LoadPackage.LoadPackageParam) { 21 | XLog.d(TAG, "hook() called with: processName = ${lpparam.processName}") 22 | 23 | tryHookVerifyApkHash(lpparam.classLoader) 24 | 25 | if (!verifyApkHashHooked) { 26 | verifyApkHashUnhook = DexClassLoader::class.java.hookConstructor(String::class.java, String::class.java, String::class.java, ClassLoader::class.java) { 27 | doAfter { tryHookVerifyApkHash(thisObject as ClassLoader) } 28 | } 29 | } 30 | 31 | val classApplicationPackageManager = lpparam.classLoader.findClass("android.app.ApplicationPackageManager") 32 | classApplicationPackageManager.hookMethod("getPackageInfo", String::class.java, Int::class.java) { 33 | doAfter { 34 | val packageName = args[0] as String 35 | if (packageName == HMS_PACKAGE_NAME) { 36 | val info = result as PackageInfo 37 | info.signatures?.firstOrNull()?.let { 38 | info.signatures[0]["mSignature"] = Base64.decode(HMS_CORE_SIGNATURE, Base64.NO_WRAP) 39 | } 40 | } 41 | } 42 | } 43 | } 44 | 45 | private fun tryHookVerifyApkHash(classLoader: ClassLoader) { 46 | if (verifyApkHashHooked) return 47 | 48 | try { 49 | classLoader.findClass("com.huawei.hms.utils.ReadApkFileUtil") 50 | .hookMethod("verifyApkHash", String::class.java) { replace { true } } 51 | 52 | XLog.d(TAG, "tryHookVerifyApkHash: verifyApkHash() hooked") 53 | 54 | verifyApkHashHooked = true 55 | verifyApkHashUnhook?.unhook() 56 | } catch (e: XposedHelpers.ClassNotFoundError) { 57 | XLog.d(TAG, "tryHookVerifyApkHash: ClassNotFoundError") 58 | } catch (e: NoSuchMethodError) { 59 | XLog.d(TAG, "tryHookVerifyApkHash: NoSuchMethodError") 60 | } catch (e: Throwable) { 61 | //ignore 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/fakedevice/FakeProperty.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.fakedevice 2 | 3 | import android.os.Build 4 | import one.yufz.hmspush.hook.XLog 5 | import one.yufz.xposed.* 6 | import java.util.concurrent.atomic.AtomicBoolean 7 | 8 | private const val TAG = "FakeProperties" 9 | 10 | enum class Property(val entry: Pair) { 11 | EMUI_API("ro.build.hw_emui_api_level" to "21"), 12 | EMUI_VERSION("ro.build.version.emui" to "EmotionUI_8.0.0"), 13 | BRAND("ro.product.brand" to "Huawei"), 14 | MANUFACTURER("ro.product.manufacturer" to "HUAWEI"), 15 | MIUI_VERSION("ro.miui.ui.version.name" to ""); 16 | 17 | val key: String 18 | get() = entry.first 19 | 20 | val value: String 21 | get() = entry.second 22 | } 23 | 24 | 25 | fun fakeProperty(property: Property, overrideValue: String) = fakeProperty(Pair(property.key, overrideValue)) 26 | 27 | fun fakeAllBuildInProperties() = fakeProperty(*Property.values().map { it.entry }.toTypedArray()) 28 | 29 | fun fakeProperty(vararg properties: Property) { 30 | fakeProperty(*properties.map { it.entry }.toTypedArray()) 31 | } 32 | 33 | private val propertyMap: MutableMap = HashMap() 34 | private val hooked = AtomicBoolean(false) 35 | 36 | fun fakeProperty(vararg properties: Pair) { 37 | propertyMap.putAll(properties) 38 | 39 | if (propertyMap.containsKey(Property.BRAND.key)) { 40 | Build::class.java["BRAND"] = propertyMap[Property.BRAND.key] 41 | } 42 | 43 | if (propertyMap.containsKey(Property.MANUFACTURER.key)) { 44 | Build::class.java["MANUFACTURER"] = propertyMap[Property.MANUFACTURER.key] 45 | } 46 | 47 | if (propertyMap.containsKey("ro.product.model")) { 48 | Build::class.java["MODEL"] = propertyMap["ro.product.model"] 49 | } 50 | 51 | if (propertyMap.containsKey("ro.build.display.id")) { 52 | Build::class.java["DISPLAY"] = propertyMap["ro.build.display.id"] 53 | } 54 | 55 | if (propertyMap.containsKey("ro.build.user")) { 56 | Build::class.java["USER"] = propertyMap["ro.build.user"] 57 | } 58 | 59 | if (hooked.getAndSet(true)) return 60 | 61 | val classSystemProperties = Build::class.java.classLoader.findClass("android.os.SystemProperties") 62 | 63 | val callback: HookContext.() -> Unit = { 64 | doBefore { 65 | val key = args[0] as String 66 | propertyMap[key]?.let { 67 | result = it 68 | } 69 | } 70 | } 71 | 72 | classSystemProperties.hookMethod("get", String::class.java, callback = callback) 73 | classSystemProperties.hookMethod("get", String::class.java, String::class.java, callback = callback) 74 | 75 | Runtime::class.java.hookMethod("exec", String::class.java) { 76 | doBefore { 77 | val cmd = args[0] as String 78 | if (cmd.startsWith("getprop")) { 79 | val key = cmd.removePrefix("getprop").trim() 80 | propertyMap[key]?.let { 81 | XLog.d(TAG, "hook getprop $key") 82 | args[0] = "echo $it" 83 | } 84 | } 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/fakedevice/HookHmsDeviceId.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.fakedevice 2 | 3 | import android.content.ContentResolver 4 | import android.provider.Settings 5 | import de.robv.android.xposed.callbacks.XC_LoadPackage 6 | import one.yufz.hmspush.hook.XLog 7 | import one.yufz.xposed.hookMethod 8 | 9 | object HookHmsDeviceId { 10 | private const val TAG = "HookHmsDeviceId" 11 | 12 | fun hook(lpparam: XC_LoadPackage.LoadPackageParam) { 13 | XLog.d(TAG, "hook() called with: processName = ${lpparam.processName}") 14 | 15 | Settings.Global::class.java.hookMethod("getString", ContentResolver::class.java, String::class.java) { 16 | doBefore { 17 | if (args[1] == "pps_oaid") { 18 | result = "00000000-0000-0000-0000-000000000000" 19 | } else if (args[1] == "pps_track_limit") { 20 | result = "true" 21 | } 22 | } 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/fakedevice/IFakeDevice.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.fakedevice 2 | 3 | import de.robv.android.xposed.callbacks.XC_LoadPackage 4 | 5 | interface IFakeDevice { 6 | fun fake(lpparam: XC_LoadPackage.LoadPackageParam): Boolean 7 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/fakedevice/PinDuoDuo.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.fakedevice 2 | 3 | import android.app.Application 4 | import android.content.ComponentName 5 | import android.content.Context 6 | import android.content.pm.PackageManager 7 | import de.robv.android.xposed.callbacks.XC_LoadPackage 8 | import one.yufz.xposed.hookMethod 9 | 10 | class PinDuoDuo : Common() { 11 | companion object { 12 | private const val TAG = "PddCommon" 13 | } 14 | 15 | override fun fake(lpparam: XC_LoadPackage.LoadPackageParam): Boolean { 16 | super.fake(lpparam) 17 | Application::class.java.hookMethod("attach", Context::class.java) { 18 | doAfter { 19 | val context: Context = thisObject as Context 20 | val hwPushReceiver = ComponentName(context, "com.aimi.android.common.push.huawei.HwPushReceiver") 21 | context.packageManager.setComponentEnabledSetting(hwPushReceiver, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 0) 22 | } 23 | } 24 | return true 25 | } 26 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/fakedevice/QQ.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.fakedevice 2 | 3 | import de.robv.android.xposed.callbacks.XC_LoadPackage 4 | 5 | class QQ : Common() { 6 | 7 | override fun fake(lpparam: XC_LoadPackage.LoadPackageParam): Boolean { 8 | if (lpparam.packageName == lpparam.processName || lpparam.processName.endsWith(":MSF")) { 9 | return super.fake(lpparam) 10 | } 11 | return false 12 | } 13 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/fakedevice/XGPush.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.fakedevice 2 | 3 | import de.robv.android.xposed.XC_MethodHook 4 | import de.robv.android.xposed.XposedBridge 5 | import de.robv.android.xposed.XposedHelpers 6 | import de.robv.android.xposed.callbacks.XC_LoadPackage 7 | import one.yufz.hmspush.hook.XLog 8 | import one.yufz.xposed.findClass 9 | import java.lang.reflect.Method 10 | 11 | open class XGPush : IFakeDevice { 12 | companion object { 13 | private const val TAG = "FakeForXGPush" 14 | } 15 | 16 | override fun fake(lpparam: XC_LoadPackage.LoadPackageParam): Boolean { 17 | val classLoader = lpparam.classLoader 18 | 19 | XLog.d(TAG, "fake() called with: classLoader = $classLoader") 20 | 21 | return try { 22 | val classChannelUtils = classLoader.findClass("com.tencent.tpns.baseapi.base.util.ChannelUtils") 23 | fakeChannels(classChannelUtils) 24 | true 25 | } catch (e: XposedHelpers.ClassNotFoundError) { 26 | XLog.e(TAG, "fake ClassNotFoundError", e) 27 | false 28 | } catch (e: Throwable) { 29 | XLog.e(TAG, "fake error: ", e) 30 | false 31 | } 32 | } 33 | 34 | private fun fakeChannels(classChannelUtils: Class<*>): Boolean { 35 | XLog.d(TAG, "fakeChannels() called") 36 | 37 | classChannelUtils.declaredMethods.forEach { 38 | XposedBridge.hookMethod(it, object : XC_MethodHook() { 39 | override fun beforeHookedMethod(param: MethodHookParam) { 40 | val method = param.method as Method 41 | if (method.name == "isBrandHuaWei") { 42 | param.result = true 43 | } else if (method.returnType == Boolean::class.java) { 44 | param.result = false 45 | } else if (method.returnType == String::class.java) { 46 | param.result = "" 47 | } 48 | } 49 | }) 50 | } 51 | return true 52 | } 53 | 54 | 55 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/hms/FakeHsf.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.hms 2 | 3 | import android.content.Context 4 | import one.yufz.hmspush.hook.XLog 5 | import one.yufz.xposed.findClass 6 | import one.yufz.xposed.hookAllMethods 7 | import one.yufz.xposed.hookMethod 8 | 9 | object FakeHsf { 10 | private const val TAG = "FakeHsf" 11 | 12 | fun hook(classLoader: ClassLoader) { 13 | XLog.d(TAG, "hook() called with: classLoader = $classLoader") 14 | 15 | classLoader.findClass("com.huawei.hsf.common.api.HsfAvailability").hookMethod("getInstance") { 16 | doAfter { 17 | unhook() 18 | hookHsfAvailabilityImpl(result.javaClass) 19 | } 20 | } 21 | 22 | classLoader.findClass("com.huawei.hsf.common.api.HsfApi").hookAllMethods("newInstance") { 23 | doAfter { 24 | unhook() 25 | hookHsfApiImpl(result.javaClass) 26 | } 27 | } 28 | } 29 | 30 | private fun hookHsfAvailabilityImpl(classHsfAvailabilityImpl: Class<*>) { 31 | XLog.d(TAG, "hookHsfAvailabilityImpl() called with: classHsfAvailabilityImpl = $classHsfAvailabilityImpl") 32 | 33 | //int isHuaweiMobileServicesAvailable(Context context); 34 | classHsfAvailabilityImpl.hookMethod("isHuaweiMobileServicesAvailable", Context::class.java) { 35 | replace { 0 } 36 | } 37 | } 38 | 39 | private fun hookHsfApiImpl(classHsfApiImpl: Class<*>) { 40 | XLog.d(TAG, "hookHsfApiImpl() called with: classHsfApiImpl = $classHsfApiImpl") 41 | 42 | classHsfApiImpl.hookMethod("isConnected") { replace { true } } 43 | } 44 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/hms/HmsPushService.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.hms 2 | 3 | import android.app.AndroidAppHelper 4 | import android.os.Binder 5 | import android.os.Handler 6 | import android.os.Looper 7 | import com.huawei.android.app.NotificationManagerEx 8 | import kotlinx.coroutines.runBlocking 9 | import one.yufz.hmspush.common.API_VERSION 10 | import one.yufz.hmspush.common.BridgeUri 11 | import one.yufz.hmspush.common.HmsPushInterface 12 | import one.yufz.hmspush.common.VERSION_CODE 13 | import one.yufz.hmspush.common.VERSION_NAME 14 | import one.yufz.hmspush.common.model.IconModel 15 | import one.yufz.hmspush.common.model.ModuleVersionModel 16 | import one.yufz.hmspush.common.model.PrefsModel 17 | import one.yufz.hmspush.common.model.PushHistoryModel 18 | import one.yufz.hmspush.common.model.PushSignModel 19 | import one.yufz.hmspush.hook.hms.icon.IconManager 20 | 21 | object HmsPushService : HmsPushInterface.Stub() { 22 | private const val TAG = "HmsPushService" 23 | 24 | fun notifyHmsPushServiceCreated() { 25 | BridgeUri.HMS_PUSH_SERVICE.notifyContentChanged(AndroidAppHelper.currentApplication()) 26 | } 27 | 28 | fun notifyPushSignChanged() { 29 | BridgeUri.PUSH_SIGN.notifyContentChanged(AndroidAppHelper.currentApplication()) 30 | } 31 | 32 | fun notifyPushHistoryChanged() { 33 | BridgeUri.PUSH_HISTORY.notifyContentChanged(AndroidAppHelper.currentApplication()) 34 | } 35 | 36 | override fun getModuleVersion(): ModuleVersionModel { 37 | return ModuleVersionModel(VERSION_NAME, VERSION_CODE, API_VERSION) 38 | } 39 | 40 | override fun getPushSignList(): List { 41 | return PushSignWatcher.getRegisterPackages() 42 | } 43 | 44 | override fun unregisterPush(packageName: String) { 45 | PushSignWatcher.unregisterSign(packageName) 46 | } 47 | 48 | override fun getPushHistoryList(): List { 49 | return PushHistory.getAll() 50 | } 51 | 52 | override fun getPreference(): PrefsModel { 53 | return Prefs.prefModel 54 | } 55 | 56 | override fun updatePreference(model: PrefsModel) { 57 | Prefs.updatePreference(model) 58 | } 59 | 60 | override fun getAllIcon(): List { 61 | return runBlocking { IconManager.getAllIconModel() } 62 | } 63 | 64 | override fun saveIcon(iconModel: IconModel) { 65 | runBlocking { IconManager.saveToLocal(iconModel.packageName, iconModel.iconData!!) } 66 | } 67 | 68 | override fun deleteIcon(packageNames: Array) { 69 | runBlocking { IconManager.deleteIcon(packageNames) } 70 | } 71 | 72 | override fun killHmsCore(): Boolean { 73 | Handler(Looper.getMainLooper()).post { android.os.Process.killProcess(android.os.Process.myPid()) } 74 | return true 75 | } 76 | 77 | override fun clearHmsNotificationChannels(packageName: String) { 78 | NotificationManagerEx.clearHmsNotificationChannels(packageName, Binder.getCallingUid() / 100000) 79 | } 80 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/hms/HookForegroundService.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.hms 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Application 5 | import android.app.Notification 6 | import android.app.NotificationChannel 7 | import android.app.NotificationManager 8 | import android.app.PendingIntent 9 | import android.app.Service 10 | import android.app.ServiceStartNotAllowedException 11 | import android.content.Context 12 | import android.content.Intent 13 | import android.graphics.Bitmap 14 | import android.graphics.Canvas 15 | import android.graphics.drawable.Drawable 16 | import android.graphics.drawable.Icon 17 | import android.os.Build 18 | import android.os.PowerManager 19 | import android.widget.Toast 20 | import one.yufz.hmspush.common.HMS_PACKAGE_NAME 21 | import one.yufz.hmspush.common.HmsCoreUtil 22 | import one.yufz.hmspush.common.KEY_HMS_CORE_EXPLICIT_FOREGROUND 23 | import one.yufz.hmspush.hook.I18n 24 | import one.yufz.hmspush.hook.XLog 25 | import one.yufz.xposed.findClass 26 | import one.yufz.xposed.hookMethod 27 | 28 | object HookForegroundService { 29 | private const val TAG = "HookForegroundService" 30 | 31 | fun hook(classLoader: ClassLoader) { 32 | Application::class.java.hookMethod("onCreate") { 33 | doAfter { 34 | val application = thisObject as Application 35 | 36 | if (Prefs.prefModel.keepAlive) { 37 | tryStartForegroundService(application) 38 | } 39 | } 40 | } 41 | 42 | val classHMSCoreService = classLoader.findClass("com.huawei.hms.core.service.HMSCoreService") 43 | classHMSCoreService.hookMethod("onCreate") { 44 | doAfter { 45 | XLog.d(TAG, "onCreate() called") 46 | HmsPushService.notifyHmsPushServiceCreated() 47 | setupForegroundState(thisObject as Service) 48 | } 49 | } 50 | classHMSCoreService.hookMethod("onStartCommand", Intent::class.java, Int::class.java, Int::class.java) { 51 | doAfter { 52 | XLog.d(TAG, "onStartCommand() called") 53 | setupForegroundState(thisObject as Service, args[0] as Intent) 54 | } 55 | } 56 | } 57 | 58 | private fun setupForegroundState(service: Service, intent: Intent? = null) { 59 | XLog.d(TAG, "setupForeground() called") 60 | 61 | if (Prefs.prefModel.keepAlive) { 62 | val explicitForeground = intent?.getBooleanExtra(KEY_HMS_CORE_EXPLICIT_FOREGROUND, false) == true 63 | if (explicitForeground) { 64 | makeServiceForeground(service) 65 | } else { 66 | tryStartForegroundService(service) 67 | } 68 | } else { 69 | stopForeground(service) 70 | } 71 | } 72 | 73 | private fun tryStartForegroundService(context: Context) { 74 | val ignoringBatteryOptimizations = context.getSystemService(PowerManager::class.java).isIgnoringBatteryOptimizations(HMS_PACKAGE_NAME) 75 | XLog.d(TAG, "tryStartForegroundService() called: ignoringBatteryOptimizations = $ignoringBatteryOptimizations") 76 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { 77 | HmsCoreUtil.startHmsCoreService(context, true) 78 | return 79 | } 80 | if (ignoringBatteryOptimizations) { 81 | HmsCoreUtil.startHmsCoreService(context, true) 82 | return 83 | } 84 | try { 85 | HmsCoreUtil.startHmsCoreService(context, true) 86 | } catch (e: ServiceStartNotAllowedException) { 87 | Toast.makeText(context, I18n.get(context).tipsOptimizeBattery, Toast.LENGTH_SHORT).show() 88 | } 89 | } 90 | 91 | @SuppressLint("DiscouragedApi") 92 | private fun makeServiceForeground(service: Service) { 93 | XLog.d(TAG, "startForeground() called") 94 | val channelId = "hms_core_service" 95 | val channel = NotificationChannel(channelId, I18n.get(service).hmsCoreRunningState, NotificationManager.IMPORTANCE_LOW) 96 | val manager = service.getSystemService(NotificationManager::class.java) 97 | manager.createNotificationChannel(channel) 98 | 99 | val iconId = service.resources.getIdentifier("update_notification_icon", "drawable", HMS_PACKAGE_NAME) 100 | .takeIf { it != 0 } ?: android.R.drawable.ic_dialog_info 101 | 102 | val contentIntent = PendingIntent.getActivity(service, 1, HmsCoreUtil.createHmsCoreDummyActivityIntent(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) 103 | 104 | val builder = Notification.Builder(service, channelId) 105 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 106 | builder.setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE) 107 | } 108 | builder.setSmallIcon(Icon.createWithBitmap(drawableToGrayscaleBitmap(service.getDrawable(iconId)!!))) 109 | .setContentIntent(contentIntent) 110 | .setContentText(I18n.get(service).hmsCoreRunning) 111 | .setAutoCancel(false) 112 | .build() 113 | service.startForeground(11111, builder.build()) 114 | } 115 | 116 | private fun stopForeground(service: Service) { 117 | XLog.d(TAG, "stopForeground() called") 118 | service.stopForeground(Service.STOP_FOREGROUND_REMOVE) 119 | } 120 | 121 | private fun drawableToGrayscaleBitmap(drawable: Drawable): Bitmap { 122 | val bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ALPHA_8) 123 | val canvas = Canvas(bitmap) 124 | drawable.setBounds(0, 0, canvas.width, canvas.height) 125 | drawable.draw(canvas) 126 | return bitmap 127 | } 128 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/hms/HookHMS.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.hms 2 | 3 | import android.app.PendingIntent 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.database.CursorWindow 7 | import android.os.Build 8 | import com.huawei.android.app.NotificationManagerEx 9 | import dalvik.system.DexClassLoader 10 | import de.robv.android.xposed.XposedHelpers.ClassNotFoundError 11 | import de.robv.android.xposed.callbacks.XC_LoadPackage 12 | import one.yufz.hmspush.hook.XLog 13 | import one.yufz.hmspush.hook.bridge.HookContentProvider 14 | import one.yufz.hmspush.hook.hms.dummy.HookDummyActivity 15 | import one.yufz.xposed.* 16 | 17 | class HookHMS { 18 | companion object { 19 | private const val TAG = "HookHMS" 20 | } 21 | 22 | fun hook(lpparam: XC_LoadPackage.LoadPackageParam) { 23 | //android.app.PendingIntent.getActivity(android.content.Context, int, android.content.Intent, int) 24 | PendingIntent::class.java.hookMethod("getActivity", Context::class.java, Int::class.java, Intent::class.java, Int::class.java) { 25 | doBefore { 26 | val intent = args[2] as Intent 27 | if (intent.component?.className == "com.huawei.hms.runtimekit.stubexplicit.PushEarthquakeActivity") { 28 | intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) 29 | } 30 | } 31 | } 32 | 33 | DexClassLoader::class.java.hookAllConstructor { 34 | doAfter { 35 | val dexPath = args[0] as String 36 | if (dexPath.contains("com.huawei.hms.push")) { 37 | XLog.d(TAG, "load push related dex path: $dexPath") 38 | 39 | val paths = dexPath.split("/") 40 | val version = paths.getOrNull(paths.size - 2)?.toIntOrNull() ?: 0 41 | 42 | XLog.d(TAG, "load push version: $version") 43 | 44 | val classLoader = thisObject as ClassLoader 45 | 46 | if (HookPushNC.canHook(classLoader)) { 47 | HookPushNC.hook(classLoader) 48 | } else { 49 | hookLegacyPush(classLoader) 50 | } 51 | } else if (dexPath.contains("com.huawei.hms.runtimekit")) { 52 | RuntimeKitHook.hook(thisObject as ClassLoader) 53 | HookLegacyTokenRequest.hook(thisObject as ClassLoader) 54 | } 55 | } 56 | } 57 | 58 | if (Build.VERSION.SDK_INT >= 33) { 59 | CursorWindow::class.java["sCursorWindowSize"] = 1024 * 1024 * 8 60 | } 61 | 62 | HookContentProvider().hook(lpparam.classLoader) 63 | fakeFingerprint(lpparam) 64 | 65 | HookForegroundService.hook(lpparam.classLoader) 66 | 67 | HookDummyActivity.hook(lpparam.classLoader) 68 | } 69 | 70 | private fun hookLegacyPush(classLoader: ClassLoader) { 71 | XLog.d(TAG, "hookLegacyPush() called with: classLoader = $classLoader") 72 | 73 | try { 74 | classLoader.findClass("com.huawei.hms.pushnc.entity.PushSelfShowMessage") 75 | } catch (e: ClassNotFoundError) { 76 | XLog.d(TAG, "PushSelfShowMessage not Found, stop hook") 77 | return 78 | } 79 | 80 | PushSignWatcher.watch() 81 | 82 | Class::class.java.hookMethod("forName", String::class.java, Boolean::class.java, ClassLoader::class.java) { 83 | doBefore { 84 | if (args[0] == NotificationManagerEx::class.java.name) { 85 | result = NotificationManagerEx::class.java 86 | } 87 | } 88 | } 89 | } 90 | 91 | private fun fakeFingerprint(lpparam: XC_LoadPackage.LoadPackageParam) { 92 | lpparam.classLoader.findClass("com.huawei.hms.auth.api.CheckFingerprintRequest") 93 | .hookMethod("parseEntity", String::class.java) { 94 | doBefore { 95 | if (Prefs.isDisableSignature()) { 96 | val request = args[0] as String 97 | if (request.contains("auth.checkFingerprint")) { 98 | val response = """{"header":{"auth_rtnCode":"0"},"body":{}}""" 99 | thisObject.callMethod("call", response) 100 | result = null 101 | } 102 | } 103 | } 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/hms/HookLegacyTokenRequest.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.hms 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import de.robv.android.xposed.XposedHelpers 6 | import one.yufz.hmspush.common.HMS_CORE_PUSH_ACTION_REGISTRATION 7 | import one.yufz.hmspush.hook.XLog 8 | import one.yufz.xposed.callMethod 9 | import one.yufz.xposed.findClass 10 | import one.yufz.xposed.get 11 | import one.yufz.xposed.hook 12 | import one.yufz.xposed.hookMethod 13 | 14 | object HookLegacyTokenRequest { 15 | private const val TAG = "HookLegacyTokenRequest" 16 | 17 | fun hook(classLoader: ClassLoader) { 18 | val classKmsMessageCenter = try { 19 | classLoader.findClass("com.huawei.hms.fwkit.message.KmsMessageCenter") 20 | } catch (e: Throwable) { 21 | null 22 | } 23 | XLog.d(TAG, "hook() called with: classKmsMessageCenter = ${classKmsMessageCenter?.classLoader}") 24 | 25 | classKmsMessageCenter?.hookMethod("register", String::class.java, Class::class.java, Boolean::class.java, Boolean::class.java) { 26 | doBefore { 27 | val uri = args[0] as String 28 | if (uri == "push.gettoken") { 29 | unhook() 30 | hookGetTokenProcess(args[1] as Class<*>) 31 | } 32 | } 33 | } 34 | } 35 | 36 | private fun hookGetTokenProcess(clazz: Class<*>) { 37 | XLog.d(TAG, "hookGetTokenProcess() called with: clazz = $clazz") 38 | val classLoader = clazz.classLoader 39 | val classIMessageEntity = classLoader.findClass("com.huawei.hms.support.api.transport.IMessageEntity") 40 | val classTokenResp = classLoader.findClass("com.huawei.hms.support.api.entity.push.TokenResp") 41 | 42 | arrayOf( 43 | *XposedHelpers.findMethodsByExactParameters(clazz.superclass, Void.TYPE, classIMessageEntity, Int::class.java), 44 | *XposedHelpers.findMethodsByExactParameters(clazz.superclass, Void.TYPE, classIMessageEntity, Class::class.java, Int::class.java) 45 | ).forEach { method -> 46 | XLog.d(TAG, "hookGetTokenProcess() called with: method = $method") 47 | 48 | method.hook { 49 | doAfter { 50 | if (args[0].javaClass == classTokenResp) { 51 | mockReceive(thisObject, args[0]) 52 | } 53 | } 54 | } 55 | } 56 | } 57 | 58 | private fun mockReceive(process: Any, response: Any) { 59 | XLog.d(TAG, "mockReceive() called") 60 | 61 | val context: Context = process["context"] 62 | val packageName = process.get("clientIdentity").callMethod("getPackageName") as String 63 | val token: String = response["token"] 64 | val intent = Intent(HMS_CORE_PUSH_ACTION_REGISTRATION) 65 | intent.setPackage(packageName) 66 | intent.putExtra("device_token", token.toByteArray()) 67 | 68 | XLog.d(TAG, "mockReceive() called with: packageName = $packageName") 69 | 70 | context.sendBroadcast(intent) 71 | } 72 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/hms/HookPushNC.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.hms 2 | 3 | import android.app.AndroidAppHelper 4 | import android.app.Notification 5 | import android.app.NotificationChannel 6 | import com.huawei.android.app.NotificationManagerEx 7 | import de.robv.android.xposed.XposedHelpers.ClassNotFoundError 8 | import one.yufz.hmspush.hook.XLog 9 | import one.yufz.xposed.findClass 10 | import one.yufz.xposed.hookMethod 11 | 12 | object HookPushNC { 13 | private const val TAG = "HookPushNC" 14 | 15 | fun canHook(classLoader: ClassLoader): Boolean { 16 | return try { 17 | classLoader.findClass("com.huawei.hsf.notification.HwNotificationManager") 18 | true 19 | } catch (e: ClassNotFoundError) { 20 | false 21 | } 22 | } 23 | 24 | fun hook(classLoader: ClassLoader) { 25 | XLog.d(TAG, "hookPushNC() called with: classLoader = $classLoader") 26 | 27 | FakeHsf.hook(classLoader) 28 | 29 | PushSignWatcher.watch() 30 | 31 | val classHwNotificationManager = classLoader.findClass("com.huawei.hsf.notification.HwNotificationManager") 32 | val classHsfApi = classLoader.findClass("com.huawei.hsf.common.api.HsfApi") 33 | 34 | classHwNotificationManager.hookMethod("isSupportHmsNc", classHsfApi) { 35 | replace { true } 36 | } 37 | 38 | //boolean areNotificationsEnabled(HsfApi hsfApi, String packageName, int userId) 39 | classHwNotificationManager.hookMethod("areNotificationsEnabled", classHsfApi, String::class.java, Int::class.java) { 40 | replace { NotificationManagerEx.areNotificationsEnabled(args[1] as String, args[2] as Int) } 41 | } 42 | 43 | //boolean cancelNotification(HsfApi hsfApi, String packageName, int id, int userId) 44 | classHwNotificationManager.hookMethod("cancelNotification", classHsfApi, String::class.java, Int::class.java, Int::class.java) { 45 | replace { 46 | NotificationManagerEx.cancelNotification(AndroidAppHelper.currentApplication(), args[1] as String, args[2] as Int) 47 | return@replace true 48 | } 49 | 50 | } 51 | //boolean createNotificationChannels(HsfApi hsfApi, String packageName, int userId, List list) 52 | classHwNotificationManager.hookMethod("createNotificationChannels", classHsfApi, String::class.java, Int::class.java, List::class.java) { 53 | replace { 54 | NotificationManagerEx.createNotificationChannels(args[1] as String, args[2] as Int, args[3] as List) 55 | return@replace true 56 | } 57 | } 58 | //boolean deleteNotificationChannel(HsfApi hsfApi, String packageName String channelId) 59 | classHwNotificationManager.hookMethod("deleteNotificationChannel", classHsfApi, String::class.java, String::class.java) { 60 | replace { 61 | NotificationManagerEx.deleteNotificationChannel(args[1] as String, args[2] as String) 62 | return@replace true 63 | } 64 | } 65 | 66 | //NotificationChannel getNotificationChannels(HsfApi hsfApi, String packageName, int userId, String channelId) 67 | classHwNotificationManager.hookMethod("getNotificationChannels", classHsfApi, String::class.java, Int::class.java, String::class.java) { 68 | replace { NotificationManagerEx.getNotificationChannel(args[1] as String, args[2] as Int, args[3] as String, false) } 69 | } 70 | 71 | //boolean notify(HsfApi hsfApi, String packageName, int id, int userId, Notification notification) 72 | classHwNotificationManager.hookMethod("notify", classHsfApi, String::class.java, Int::class.java, Int::class.java, Notification::class.java) { 73 | replace { 74 | NotificationManagerEx.notify(AndroidAppHelper.currentApplication(), args[1] as String, args[2] as Int, args[4] as Notification) 75 | return@replace true 76 | } 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/hms/Prefs.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.hms 2 | 3 | import android.content.Context 4 | import one.yufz.hmspush.common.HMSPUSH_PREF_NAME 5 | import one.yufz.hmspush.common.PREF_KEY_DISABLE_SIGNATURE 6 | import one.yufz.hmspush.common.content.storeToSharedPreference 7 | import one.yufz.hmspush.common.content.toContent 8 | import one.yufz.hmspush.common.model.PrefsModel 9 | 10 | object Prefs { 11 | private val pref = StorageContext.get().getSharedPreferences(HMSPUSH_PREF_NAME, Context.MODE_PRIVATE) 12 | 13 | var prefModel: PrefsModel 14 | private set 15 | 16 | init { 17 | prefModel = pref.toContent() 18 | migrateLegacyPreference() 19 | } 20 | 21 | private fun migrateLegacyPreference() { 22 | if (pref.contains(PREF_KEY_DISABLE_SIGNATURE)) { 23 | updatePreference( 24 | prefModel.copy(disableSignature = pref.getBoolean(PREF_KEY_DISABLE_SIGNATURE, false)) 25 | ) 26 | pref.edit() 27 | .remove(PREF_KEY_DISABLE_SIGNATURE) 28 | .apply() 29 | } 30 | } 31 | 32 | fun updatePreference(prefsModel: PrefsModel) { 33 | this.prefModel = prefsModel.also { model -> 34 | pref.edit() 35 | .also { model.storeToSharedPreference(it) } 36 | .apply() 37 | } 38 | 39 | } 40 | 41 | fun isDisableSignature(): Boolean { 42 | return prefModel.disableSignature 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/hms/PushHistory.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.hms 2 | 3 | import android.app.AndroidAppHelper 4 | import android.content.Context 5 | import one.yufz.hmspush.common.model.PushHistoryModel 6 | 7 | object PushHistory { 8 | private val store by lazy { StorageContext.get().getSharedPreferences("push_history", Context.MODE_PRIVATE) } 9 | 10 | fun record(packageName: String) { 11 | store.edit() 12 | .putLong(packageName, System.currentTimeMillis()) 13 | .apply() 14 | 15 | notifyChange() 16 | } 17 | 18 | fun remove(packageName: String) { 19 | store.edit() 20 | .remove(packageName) 21 | .apply() 22 | 23 | notifyChange() 24 | } 25 | 26 | fun getAll(): List { 27 | return store.all.map { PushHistoryModel(it.key, it.value as Long) } 28 | } 29 | 30 | private fun notifyChange() { 31 | HmsPushService.notifyPushHistoryChanged() 32 | } 33 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/hms/PushSignWatcher.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.hms 2 | 3 | import android.app.AndroidAppHelper 4 | import android.content.Context 5 | import android.content.SharedPreferences 6 | import one.yufz.hmspush.common.BridgeUri 7 | import one.yufz.hmspush.common.model.PushSignModel 8 | import one.yufz.hmspush.hook.XLog 9 | 10 | 11 | object PushSignWatcher : SharedPreferences.OnSharedPreferenceChangeListener { 12 | private const val TAG = "PushSignWatcher" 13 | 14 | private var lastRegistered: Set = emptySet() 15 | 16 | fun watch() { 17 | XLog.d(TAG, "watch() called") 18 | 19 | val pushSignPref = AndroidAppHelper.currentApplication().createDeviceProtectedStorageContext() 20 | .getSharedPreferences("PushSign", Context.MODE_PRIVATE) 21 | 22 | logPushSign(pushSignPref) 23 | 24 | pushSignPref.registerOnSharedPreferenceChangeListener(this) 25 | } 26 | 27 | override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) { 28 | XLog.d(TAG, "onPushSignChanged() called with: key = $key") 29 | logPushSign(sharedPreferences) 30 | } 31 | 32 | private fun logPushSign(pref: SharedPreferences) { 33 | val newList = getAllPackages(pref) 34 | val added = newList - lastRegistered 35 | 36 | if (added.isNotEmpty()) { 37 | XLog.d(TAG, "push registered: $added") 38 | } 39 | 40 | val removed = lastRegistered - newList 41 | 42 | if (removed.isNotEmpty()) { 43 | XLog.d(TAG, "push unregister: $removed") 44 | } 45 | 46 | lastRegistered = newList 47 | 48 | notifyChange() 49 | } 50 | 51 | private fun notifyChange() { 52 | AndroidAppHelper.currentApplication().contentResolver.notifyChange(BridgeUri.PUSH_SIGN.toUri(), null, false) 53 | HmsPushService.notifyPushSignChanged() 54 | } 55 | 56 | private fun getAllPackages(perf: SharedPreferences): Set { 57 | return perf.all.keys.toSet() 58 | } 59 | 60 | fun getRegisterPackages(): List { 61 | return lastRegistered 62 | .map { it.split("/") } 63 | .map { PushSignModel(it[0], it[1].toIntOrNull() ?: 0) } 64 | } 65 | 66 | fun unregisterSign(packageName: String) { 67 | RuntimeKitHook.sendFakePackageRemoveBroadcast(packageName) 68 | } 69 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/hms/RuntimeKitHook.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.hms 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.content.IntentFilter 7 | import android.net.Uri 8 | import android.os.Handler 9 | import one.yufz.hmspush.hook.XLog 10 | import one.yufz.xposed.findClass 11 | import one.yufz.xposed.hookMethod 12 | import java.util.* 13 | 14 | object RuntimeKitHook { 15 | private const val TAG = "RuntimeKitHook" 16 | 17 | private val receivers: MutableMap = WeakHashMap() 18 | 19 | fun hook(classLoader: ClassLoader) { 20 | classLoader.findClass("com.huawei.hms.runtimekit.container.kitsdk.KitContext") 21 | .hookMethod("registerReceiver", BroadcastReceiver::class.java, IntentFilter::class.java, String::class.java, Handler::class.java) { 22 | doAfter { 23 | val receiver = args[0] as BroadcastReceiver 24 | val intentFilter = args[1] as IntentFilter 25 | if (intentFilter.hasAction("android.intent.action.PACKAGE_REMOVED") 26 | && intentFilter.hasAction("android.intent.action.PACKAGE_DATA_CLEARED") 27 | && intentFilter.hasDataScheme("package") 28 | ) { 29 | receivers[receiver] = thisObject as Context 30 | XLog.d(TAG, "receiver added: $receiver") 31 | } 32 | } 33 | } 34 | } 35 | 36 | fun sendFakePackageRemoveBroadcast(packageName: String) { 37 | XLog.d(TAG, "sendFakePackageRemoveBroadcast() called with: packageName = $packageName") 38 | 39 | val intent = Intent("android.intent.action.PACKAGE_REMOVED").apply { 40 | data = Uri.parse("package:${packageName}") 41 | } 42 | 43 | receivers.forEach { (receiver, context) -> 44 | 45 | receiver.onReceive(context, intent) 46 | 47 | XLog.d(TAG, "sendFakePackageRemoveBroadcast() send to: $receiver") 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/hms/StorageContext.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.hms 2 | 3 | import android.app.AndroidAppHelper 4 | import android.content.Context 5 | 6 | object StorageContext { 7 | 8 | fun get(): Context { 9 | return AndroidAppHelper.currentApplication().createDeviceProtectedStorageContext() 10 | } 11 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/hms/dummy/DummyFragment.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.hms.dummy 2 | 3 | import android.app.Fragment 4 | import android.os.Bundle 5 | import android.view.Gravity 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import android.widget.FrameLayout 10 | import android.widget.TextView 11 | import one.yufz.hmspush.common.dp2px 12 | import one.yufz.hmspush.hook.I18n 13 | import one.yufz.xposed.child 14 | 15 | class DummyFragment : Fragment() { 16 | override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? { 17 | val context = requireNotNull(context) 18 | 19 | val root = FrameLayout(context).apply { 20 | layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) 21 | 22 | val dp16 = context.dp2px(16) 23 | 24 | setPadding(dp16, dp16, dp16, dp16) 25 | } 26 | 27 | val p = FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { 28 | gravity = Gravity.CENTER 29 | } 30 | root.child(p) { 31 | gravity = Gravity.CENTER 32 | textSize = 16f 33 | text = I18n.get(context).dummyFragmentDesc 34 | 35 | } 36 | return root 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/hms/dummy/HookDummyActivity.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.hms.dummy 2 | 3 | import android.app.Activity 4 | import android.graphics.Color 5 | import android.os.Bundle 6 | import android.view.View 7 | import android.view.Window 8 | import android.view.WindowManager 9 | import de.robv.android.xposed.XposedHelpers 10 | import one.yufz.hmspush.common.FLAG_HMS_DUMMY_HOOKED 11 | import one.yufz.hmspush.common.HMS_CORE_DUMMY_ACTIVITY 12 | import one.yufz.hmspush.hook.XLog 13 | import one.yufz.xposed.findClass 14 | import one.yufz.xposed.hookMethod 15 | 16 | 17 | object HookDummyActivity { 18 | private const val TAG = "HookDummyActivity" 19 | 20 | private const val KEY_IGNORE_FIRST_FINISH = "ignore_first_finish" 21 | 22 | fun hook(classLoader: ClassLoader) { 23 | XLog.d(TAG, "hook() called") 24 | 25 | HookDummyActivityTask.hook(classLoader) 26 | 27 | val classDummyActivity = classLoader.findClass(HMS_CORE_DUMMY_ACTIVITY) 28 | classDummyActivity.hookMethod("onCreate", Bundle::class.java) { 29 | doBefore { 30 | XLog.d(TAG, "onCreate doBefore() called") 31 | XposedHelpers.setAdditionalInstanceField(thisObject, KEY_IGNORE_FIRST_FINISH, true) 32 | val activity = thisObject as Activity 33 | activity.setTheme(android.R.style.Theme_Material_Light_NoActionBar) 34 | if (args[0] != null) { 35 | args[0] = null 36 | } 37 | } 38 | 39 | doAfter { 40 | val activity = this.thisObject as Activity 41 | val intent = activity.intent 42 | val hooked = intent.getBooleanExtra(FLAG_HMS_DUMMY_HOOKED, false) 43 | 44 | XLog.d(TAG, "onCreate doAfter() called, hooked = $hooked") 45 | 46 | if (hooked) { 47 | makeActivityFullscreen(thisObject as Activity) 48 | 49 | addDummyFragment(activity) 50 | } 51 | } 52 | } 53 | 54 | classDummyActivity.hookMethod("finish") { 55 | doBefore { 56 | val activity = this.thisObject as Activity 57 | val hooked = activity.intent.getBooleanExtra(FLAG_HMS_DUMMY_HOOKED, false) 58 | val ignoreFirstFinish = XposedHelpers.getAdditionalInstanceField(activity, KEY_IGNORE_FIRST_FINISH) != null 59 | 60 | XLog.d(TAG, "finish() called, hooked = $hooked , ignoreFirstFinish = $ignoreFirstFinish") 61 | 62 | if (hooked && ignoreFirstFinish) { 63 | result = null 64 | } 65 | 66 | if (ignoreFirstFinish) { 67 | XposedHelpers.removeAdditionalInstanceField(activity, KEY_IGNORE_FIRST_FINISH) 68 | } 69 | } 70 | } 71 | } 72 | 73 | private fun makeActivityFullscreen(activity: Activity) { 74 | activity.window.apply { 75 | addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) 76 | statusBarColor = Color.TRANSPARENT 77 | decorView.systemUiVisibility = decorView.systemUiVisibility or 78 | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or 79 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE or 80 | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR 81 | } 82 | } 83 | 84 | private fun addDummyFragment(activity: Activity) { 85 | XLog.d(TAG, "addHmsDummyFragment() called") 86 | activity.fragmentManager.beginTransaction() 87 | .add(Window.ID_ANDROID_CONTENT, DummyFragment(), "hms_push_dummy") 88 | .commitNowAllowingStateLoss() 89 | } 90 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/hms/dummy/HookDummyActivityTask.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.hms.dummy 2 | 3 | import android.app.Activity 4 | import android.app.ActivityManager 5 | import android.app.AndroidAppHelper 6 | import android.app.Application 7 | import android.os.Bundle 8 | import one.yufz.hmspush.common.HMS_CORE_DUMMY_ACTIVITY 9 | import one.yufz.hmspush.hook.XLog 10 | import one.yufz.xposed.hookMethod 11 | 12 | 13 | object HookDummyActivityTask { 14 | private const val TAG = "HookDummyActivityTask" 15 | 16 | fun hook(classLoader: ClassLoader) { 17 | Application::class.java.hookMethod("onCreate") { 18 | doAfter { 19 | val application = thisObject as Application 20 | registerActivityLifecycle(application) 21 | } 22 | } 23 | } 24 | 25 | private fun registerActivityLifecycle(application: Application) { 26 | application.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks { 27 | override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { 28 | if (activity.javaClass.name == HMS_CORE_DUMMY_ACTIVITY) { 29 | val activityManager = AndroidAppHelper.currentApplication().getSystemService(ActivityManager::class.java) 30 | activityManager.appTasks.find { it.taskInfo.id == activity.taskId }?.let { 31 | it.setExcludeFromRecents(false) 32 | XLog.d(TAG, "task: ${it.taskInfo}") 33 | } 34 | } 35 | } 36 | 37 | override fun onActivityStarted(activity: Activity) {} 38 | override fun onActivityResumed(activity: Activity) {} 39 | override fun onActivityPaused(activity: Activity) {} 40 | override fun onActivityStopped(activity: Activity) {} 41 | override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} 42 | override fun onActivityDestroyed(activity: Activity) {} 43 | }) 44 | } 45 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/hms/icon/IconManager.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.hms.icon 2 | 3 | import android.content.Context 4 | import android.os.ParcelFileDescriptor 5 | import android.util.LruCache 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.withContext 8 | import one.yufz.hmspush.common.IconData 9 | import one.yufz.hmspush.common.IconData.Companion.scaleForNotification 10 | import one.yufz.hmspush.common.model.IconModel 11 | import one.yufz.hmspush.hook.hms.StorageContext 12 | import java.io.File 13 | 14 | object IconManager { 15 | private val iconDir = File(StorageContext.get().filesDir, "hms_push/icons") 16 | 17 | private val cache = LruCache(20) 18 | 19 | suspend fun saveToLocal(packageName: String, jsonString: String) { 20 | withContext(Dispatchers.IO) { 21 | if (!iconDir.exists()) { 22 | iconDir.mkdirs() 23 | } 24 | 25 | File(iconDir, packageName).apply { 26 | if (exists()) { 27 | delete() 28 | } 29 | createNewFile() 30 | 31 | writeText(jsonString) 32 | } 33 | } 34 | } 35 | 36 | fun getNotificationIconData(context: Context, packageName: String): IconData? { 37 | val cacheIconData = cache.get(packageName) 38 | if (cacheIconData != null) return cacheIconData 39 | 40 | val iconDataFile = File(iconDir, packageName) 41 | 42 | if (!iconDataFile.exists()) return null 43 | 44 | val iconData = IconData.fromJson(iconDataFile.readText()) 45 | .scaleForNotification(context) 46 | 47 | cache.put(packageName, iconData) 48 | 49 | return iconData 50 | } 51 | 52 | suspend fun getAllIconModel(): List { 53 | return withContext(Dispatchers.IO) { 54 | iconDir.listFiles() 55 | ?.map { IconModel(it.name, dataFD = ParcelFileDescriptor.open(it, ParcelFileDescriptor.MODE_READ_ONLY)) } 56 | ?: emptyList() 57 | } 58 | } 59 | 60 | suspend fun deleteIcon(packages: Array?) { 61 | return withContext(Dispatchers.IO) { 62 | val size = iconDir.listFiles()?.size ?: 0 63 | if (packages.isNullOrEmpty()) { 64 | if (size != 0) { 65 | iconDir.deleteRecursively() 66 | cache.evictAll() 67 | } 68 | } else { 69 | packages.onEach { 70 | File(iconDir, it).delete() 71 | cache.remove(it) 72 | } 73 | } 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/hms/nm/INotificationManager.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.hms.nm 2 | 3 | import android.app.Notification 4 | import android.app.NotificationChannel 5 | import android.content.Context 6 | import android.service.notification.StatusBarNotification 7 | 8 | interface INotificationManager { 9 | fun areNotificationsEnabled(packageName: String, userId: Int): Boolean 10 | fun getNotificationChannel(packageName: String, userId: Int, channelId: String, boolean: Boolean): NotificationChannel? 11 | fun notify(context: Context, packageName: String, id: Int, notification: Notification) 12 | fun createNotificationChannels(packageName: String, userId: Int, channels: List) 13 | fun cancelNotification(context: Context, packageName: String, id: Int) 14 | fun deleteNotificationChannel(packageName: String, channelId: String) 15 | fun getActiveNotifications(packageName: String, userId: Int): Array 16 | fun getNotificationChannels(packageName: String, userId: Int): List 17 | fun clearHmsNotificationChannels(packageName: String, userId: Int) 18 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/hms/nm/NotificationManagerEx.kt: -------------------------------------------------------------------------------- 1 | //HMS use reflection to find this class, keep its package 2 | package com.huawei.android.app 3 | 4 | import android.app.Notification 5 | import android.app.NotificationChannel 6 | import android.app.NotificationManager 7 | import android.content.Context 8 | import de.robv.android.xposed.XposedHelpers 9 | import one.yufz.hmspush.hook.XLog 10 | import one.yufz.hmspush.hook.hms.PushHistory 11 | import one.yufz.hmspush.hook.hms.nm.INotificationManager 12 | import one.yufz.hmspush.hook.hms.nm.SelfNotificationManager 13 | import one.yufz.hmspush.hook.hms.nm.SystemNotificationManager 14 | import one.yufz.hmspush.hook.hms.nm.handler.NotificationHandlers 15 | import one.yufz.hmspush.hook.system.HookSystemService 16 | import java.lang.reflect.InvocationTargetException 17 | 18 | object NotificationManagerEx { 19 | private const val TAG = "NotificationManagerEx" 20 | 21 | @JvmStatic 22 | fun getNotificationManager() = this 23 | 24 | private val notificationManager: INotificationManager = createNotificationManager() 25 | 26 | private fun createNotificationManager(): INotificationManager { 27 | return if (HookSystemService.isSystemHookReady) { 28 | XLog.d(TAG, "use SystemNotificationManager") 29 | SystemNotificationManager() 30 | } else { 31 | XLog.d(TAG, "use SelfNotificationManager") 32 | SelfNotificationManager() 33 | } 34 | } 35 | 36 | fun areNotificationsEnabled(packageName: String, userId: Int): Boolean { 37 | XLog.d(TAG, "areNotificationsEnabled() called with: packageName = $packageName, userId = $userId") 38 | return tryInvoke { notificationManager.areNotificationsEnabled(packageName, userId) } 39 | } 40 | 41 | fun getNotificationChannel(packageName: String, userId: Int, channelId: String, boolean: Boolean): NotificationChannel? { 42 | XLog.d(TAG, "getNotificationChannel() called with: packageName = $packageName, userId = $userId, channelId = $channelId, boolean = $boolean") 43 | return tryInvoke { notificationManager.getNotificationChannel(packageName, userId, channelId, boolean) } 44 | } 45 | 46 | fun notify(context: Context, packageName: String, id: Int, notification: Notification) { 47 | XLog.d(TAG, "notify() called with: context = $context, packageName = $packageName, id = $id, notification = $notification") 48 | 49 | tryInvoke { 50 | NotificationHandlers.handle(notificationManager, context, packageName, id, notification) 51 | } 52 | 53 | PushHistory.record(packageName) 54 | } 55 | 56 | fun createNotificationChannels(packageName: String, userId: Int, channels: List) { 57 | channels.forEach { 58 | it.importance = NotificationManager.IMPORTANCE_HIGH 59 | } 60 | XLog.d(TAG, "createNotificationChannels() called with: packageName = $packageName, userId = $userId, channels = $channels") 61 | tryInvoke { notificationManager.createNotificationChannels(packageName, userId, channels) } 62 | } 63 | 64 | fun cancelNotification(context: Context, packageName: String, id: Int) { 65 | XLog.d(TAG, "cancelNotification() called with: context = $context, packageName = $packageName, id = $id") 66 | tryInvoke { notificationManager.cancelNotification(context, packageName, id) } 67 | } 68 | 69 | fun deleteNotificationChannel(packageName: String, channelId: String) { 70 | XLog.d(TAG, "deleteNotificationChannel() called with: packageName = $packageName, channelId = $channelId") 71 | tryInvoke { notificationManager.deleteNotificationChannel(packageName, channelId) } 72 | } 73 | 74 | fun clearHmsNotificationChannels(packageName: String, userId: Int) { 75 | XLog.d(TAG, "clearHmsNotificationChannels() called with: packageName = $packageName, userId = $userId") 76 | tryInvoke { notificationManager.clearHmsNotificationChannels(packageName, userId) } 77 | } 78 | 79 | private inline fun tryInvoke(invoke: () -> R): R { 80 | try { 81 | return invoke() 82 | } catch (e: XposedHelpers.InvocationTargetError) { 83 | XLog.e(TAG, "tryInvoke: ", e) 84 | XLog.e(TAG, "tryInvoke targetException: ", e.cause) 85 | throw e.cause ?: e 86 | } catch (e: InvocationTargetException) { 87 | XLog.e(TAG, "tryInvoke: ", e) 88 | XLog.e(TAG, "tryInvoke targetException: ", e.targetException) 89 | throw e.targetException ?: e 90 | } catch (e: Throwable) { 91 | XLog.e(TAG, "tryInvoke: ", e) 92 | XLog.e(TAG, "tryInvoke cause: ", e.cause) 93 | throw e.cause ?: e 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/hms/nm/SelfNotificationManager.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.hms.nm 2 | 3 | import android.app.AndroidAppHelper 4 | import android.app.Notification 5 | import android.app.NotificationChannel 6 | import android.app.NotificationChannelGroup 7 | import android.app.NotificationManager 8 | import android.content.Context 9 | import android.service.notification.StatusBarNotification 10 | import one.yufz.hmspush.hook.XLog 11 | import one.yufz.xposed.set 12 | 13 | class SelfNotificationManager : INotificationManager { 14 | companion object { 15 | private const val TAG = "SelfNotificationManager" 16 | } 17 | 18 | private val notificationManager = AndroidAppHelper.currentApplication() 19 | .getSystemService(NotificationManager::class.java) 20 | 21 | override fun areNotificationsEnabled(packageName: String, userId: Int): Boolean { 22 | return true 23 | } 24 | 25 | override fun getNotificationChannel(packageName: String, userId: Int, channelId: String, boolean: Boolean): NotificationChannel? { 26 | return notificationManager.getNotificationChannel(channelId) 27 | } 28 | 29 | override fun notify(context: Context, packageName: String, id: Int, notification: Notification) { 30 | notificationManager.notify(id, notification) 31 | } 32 | 33 | override fun createNotificationChannels(packageName: String, userId: Int, channels: List) { 34 | channels.forEach { channel -> 35 | channel["mDesc"] = channel.name 36 | channel.name = getApplicationName(packageName) ?: packageName 37 | } 38 | notificationManager.createNotificationChannels(channels) 39 | } 40 | 41 | private fun getApplicationName(packageName: String): CharSequence? { 42 | try { 43 | val pm = AndroidAppHelper.currentApplication().packageManager 44 | return pm.getApplicationInfo(packageName, 0).loadLabel(pm) 45 | } catch (e: Throwable) { 46 | XLog.e(TAG, "getApplicationName: error", e) 47 | return null 48 | } 49 | } 50 | 51 | override fun cancelNotification(context: Context, packageName: String, id: Int) { 52 | notificationManager.cancel(id) 53 | } 54 | 55 | override fun deleteNotificationChannel(packageName: String, channelId: String) { 56 | notificationManager.deleteNotificationChannel(channelId) 57 | } 58 | 59 | override fun getActiveNotifications(packageName: String, userId: Int): Array { 60 | return notificationManager.activeNotifications 61 | } 62 | 63 | override fun getNotificationChannels(packageName: String, userId: Int): List { 64 | return notificationManager.notificationChannels 65 | } 66 | 67 | override fun clearHmsNotificationChannels(packageName: String, userId: Int) { 68 | val applicationName = getApplicationName(packageName) 69 | getNotificationChannels(packageName, userId).filter { it.name == applicationName }.forEach { 70 | notificationManager.deleteNotificationChannel(it.id) 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/hms/nm/handler/FinalHandler.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.hms.nm.handler 2 | 3 | import android.app.Notification 4 | import android.content.Context 5 | import one.yufz.hmspush.hook.XLog 6 | import one.yufz.hmspush.hook.hms.nm.INotificationManager 7 | 8 | class FinalHandler : NotificationHandler { 9 | companion object { 10 | private const val TAG = "FinalHandler" 11 | } 12 | 13 | override fun careAbout(manager: INotificationManager, context: Context, packageName: String, id: Int, notification: Notification): Boolean { 14 | return true 15 | } 16 | 17 | override fun handle(chain: NotificationHandler.Chain, manager: INotificationManager, context: Context, packageName: String, id: Int, notification: Notification) { 18 | XLog.d(TAG, "handle() called with: packageName = $packageName, id = $id, notification = $notification") 19 | manager.notify(context, packageName, id, notification) 20 | } 21 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/hms/nm/handler/GroupByIdHandler.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.hms.nm.handler 2 | 3 | import android.app.Notification 4 | import android.app.Notification.InboxStyle 5 | import android.content.Context 6 | import one.yufz.hmspush.hook.XLog 7 | import one.yufz.hmspush.hook.hms.Prefs 8 | import one.yufz.hmspush.hook.hms.nm.INotificationManager 9 | import one.yufz.hmspush.hook.util.getInboxLines 10 | import one.yufz.hmspush.hook.util.getSummaryText 11 | import one.yufz.hmspush.hook.util.getText 12 | import one.yufz.hmspush.hook.util.getTitle 13 | import one.yufz.hmspush.hook.util.getUserId 14 | import one.yufz.hmspush.hook.util.newBuilder 15 | 16 | class GroupByIdHandler : NotificationHandler { 17 | companion object { 18 | private const val TAG = "GroupByIdHandler" 19 | } 20 | 21 | override fun careAbout(manager: INotificationManager, context: Context, packageName: String, id: Int, notification: Notification): Boolean { 22 | return Prefs.prefModel.groupMessageById 23 | } 24 | 25 | override fun handle(chain: NotificationHandler.Chain, manager: INotificationManager, context: Context, packageName: String, id: Int, notification: Notification) { 26 | XLog.d(TAG, "handle() called with: packageName = $packageName, id = $id, notification = $notification") 27 | 28 | val activeNotification = manager.getActiveNotifications(packageName, context.getUserId()) 29 | .find { it.id == id } 30 | 31 | if (activeNotification != null) { 32 | val current = activeNotification.notification 33 | 34 | val lines = current.getInboxLines()?.take(25) ?: listOf(current.getText()) 35 | 36 | val inboxStyle = InboxStyle() 37 | .setBigContentTitle(notification.getTitle()) 38 | .setSummaryText(generateSummary(current.getSummaryText())) 39 | .addLine(notification.getText()) 40 | lines.forEach(inboxStyle::addLine) 41 | 42 | val newNotification = notification.newBuilder(context) 43 | .setStyle(inboxStyle) 44 | .build() 45 | 46 | super.handle(chain, manager, context, packageName, id, newNotification) 47 | } else { 48 | super.handle(chain, manager, context, packageName, id, notification) 49 | } 50 | } 51 | 52 | private fun generateSummary(current: CharSequence?): CharSequence { 53 | val currentCount = current?.split(" ")?.firstOrNull()?.toInt() ?: 1 54 | return "${currentCount + 1} 条消息" 55 | } 56 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/hms/nm/handler/GroupNotificationHandler.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.hms.nm.handler 2 | 3 | import android.app.Notification 4 | import android.content.Context 5 | import one.yufz.hmspush.hook.XLog 6 | import one.yufz.hmspush.hook.hms.nm.INotificationManager 7 | import one.yufz.hmspush.hook.hms.nm.SelfNotificationManager 8 | import one.yufz.xposed.set 9 | 10 | class GroupNotificationHandler : NotificationHandler { 11 | companion object { 12 | private const val TAG = "GroupNotificationHandle" 13 | } 14 | 15 | override fun careAbout(manager: INotificationManager, context: Context, packageName: String, id: Int, notification: Notification): Boolean { 16 | return manager is SelfNotificationManager 17 | } 18 | 19 | override fun handle(chain: NotificationHandler.Chain, manager: INotificationManager, context: Context, packageName: String, id: Int, notification: Notification) { 20 | XLog.d(TAG, "handle() called with: packageName = $packageName, id = $id, notification = $notification") 21 | 22 | notification["mGroupKey"] = packageName 23 | 24 | super.handle(chain, manager, context, packageName, id, notification) 25 | 26 | val groupNotification = notification.clone().apply { 27 | this.flags = flags.or(Notification.FLAG_GROUP_SUMMARY) 28 | this["mGroupAlertBehavior"] = Notification.GROUP_ALERT_CHILDREN 29 | } 30 | 31 | manager.notify(context, packageName, packageName.hashCode(), groupNotification) 32 | } 33 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/hms/nm/handler/IconHandler.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.hms.nm.handler 2 | 3 | import android.app.Notification 4 | import android.content.Context 5 | import android.graphics.drawable.Icon 6 | import one.yufz.hmspush.hook.hms.Prefs 7 | import one.yufz.hmspush.hook.hms.icon.IconManager 8 | import one.yufz.hmspush.hook.hms.nm.INotificationManager 9 | import one.yufz.hmspush.hook.util.newBuilder 10 | 11 | class IconHandler : NotificationHandler { 12 | override fun careAbout(manager: INotificationManager, context: Context, packageName: String, id: Int, notification: Notification): Boolean { 13 | return Prefs.prefModel.useCustomIcon 14 | } 15 | 16 | override fun handle(chain: NotificationHandler.Chain, manager: INotificationManager, context: Context, packageName: String, id: Int, notification: Notification) { 17 | var newNotification = notification 18 | val iconData = IconManager.getNotificationIconData(context, packageName) 19 | if (iconData != null) { 20 | val builder = notification.newBuilder(context) 21 | .setSmallIcon(Icon.createWithBitmap(iconData.iconBitmap)) 22 | 23 | if (Prefs.prefModel.tintIconColor) { 24 | iconData.iconColor?.let { 25 | builder.setColor(it) 26 | } 27 | } 28 | 29 | newNotification = builder.build() 30 | } 31 | super.handle(chain, manager, context, packageName, id, newNotification) 32 | } 33 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/hms/nm/handler/LabelHandler.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.hms.nm.handler 2 | 3 | import android.app.Notification 4 | import android.content.Context 5 | import android.graphics.drawable.Icon 6 | import one.yufz.hmspush.hook.XLog 7 | import one.yufz.hmspush.hook.hms.Prefs 8 | import one.yufz.hmspush.hook.hms.icon.IconManager 9 | import one.yufz.hmspush.hook.hms.nm.INotificationManager 10 | import one.yufz.hmspush.hook.hms.nm.SelfNotificationManager 11 | import one.yufz.hmspush.hook.util.newBuilder 12 | 13 | class LabelHandler : NotificationHandler { 14 | companion object { 15 | private const val TAG = "LabelHandler" 16 | } 17 | 18 | override fun careAbout(manager: INotificationManager, context: Context, packageName: String, id: Int, notification: Notification): Boolean { 19 | return manager is SelfNotificationManager 20 | } 21 | 22 | override fun handle(chain: NotificationHandler.Chain, manager: INotificationManager, context: Context, packageName: String, id: Int, notification: Notification) { 23 | val applicationName = getApplicationName(context, packageName) 24 | val n = if (applicationName.isNullOrEmpty()) { 25 | notification 26 | } else { 27 | notification.newBuilder(context) 28 | .setSubText(applicationName) 29 | .build() 30 | } 31 | super.handle(chain, manager, context, packageName, id, n) 32 | } 33 | 34 | private fun getApplicationName(context: Context, packageName: String): CharSequence? { 35 | try { 36 | val pm = context.packageManager 37 | return pm.getApplicationInfo(packageName, 0).loadLabel(pm) 38 | } catch (e: Throwable) { 39 | XLog.e(TAG, "getApplicationName: error", e) 40 | return null 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/hms/nm/handler/NotificationHandler.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.hms.nm.handler 2 | 3 | import android.app.Notification 4 | import android.content.Context 5 | import one.yufz.hmspush.hook.hms.nm.INotificationManager 6 | 7 | interface NotificationHandler { 8 | fun careAbout(manager: INotificationManager, context: Context, packageName: String, id: Int, notification: Notification): Boolean { 9 | return false 10 | } 11 | 12 | fun handle(chain: Chain, manager: INotificationManager, context: Context, packageName: String, id: Int, notification: Notification) { 13 | chain.proceed(manager, context, packageName, id, notification) 14 | } 15 | 16 | interface Chain { 17 | fun proceed(manager: INotificationManager, context: Context, packageName: String, id: Int, notification: Notification) 18 | } 19 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/hms/nm/handler/NotificationHandlers.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.hms.nm.handler 2 | 3 | import android.app.Notification 4 | import android.content.Context 5 | import one.yufz.hmspush.hook.hms.nm.INotificationManager 6 | import java.util.* 7 | 8 | object NotificationHandlers { 9 | private val handlers = listOf( 10 | LabelHandler(), 11 | IconHandler(), 12 | GroupNotificationHandler(), 13 | GroupByIdHandler(), 14 | FinalHandler() 15 | ) 16 | 17 | fun handle(manager: INotificationManager, context: Context, packageName: String, id: Int, notification: Notification) { 18 | HandlerChain(LinkedList(handlers)) 19 | .proceed(manager, context, packageName, id, notification) 20 | } 21 | 22 | class HandlerChain(private val linkList: LinkedList) : NotificationHandler.Chain { 23 | override fun proceed(manager: INotificationManager, context: Context, packageName: String, id: Int, notification: Notification) { 24 | val head = linkList.poll() 25 | 26 | if (head != null) { 27 | if (head.careAbout(manager, context, packageName, id, notification)) { 28 | head.handle(this, manager, context, packageName, id, notification) 29 | } else { 30 | this.proceed(manager, context, packageName, id, notification) 31 | } 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/system/HookSystemService.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.system 2 | 3 | import android.app.AndroidAppHelper 4 | import android.app.NotificationManager 5 | import android.content.Context 6 | import android.os.Binder 7 | import android.os.Build 8 | import de.robv.android.xposed.XposedHelpers 9 | import one.yufz.hmspush.common.ANDROID_PACKAGE_NAME 10 | import one.yufz.hmspush.common.IS_SYSTEM_HOOK_READY 11 | import one.yufz.hmspush.hook.XLog 12 | import one.yufz.xposed.callMethod 13 | import one.yufz.xposed.deoptimizeMethod 14 | import one.yufz.xposed.get 15 | import one.yufz.xposed.hook 16 | import one.yufz.xposed.hookMethod 17 | 18 | class HookSystemService { 19 | companion object { 20 | private const val TAG = "HookSystemService" 21 | 22 | val isSystemHookReady: Boolean by lazy { 23 | try { 24 | val nm = AndroidAppHelper.currentApplication().getSystemService(NotificationManager::class.java) 25 | nm.callMethod("isSystemConditionProviderEnabled", IS_SYSTEM_HOOK_READY) as Boolean 26 | } catch (t: Throwable) { 27 | XLog.e(TAG, "isSystemHookReady error", t) 28 | false 29 | } 30 | } 31 | 32 | } 33 | 34 | fun hook(classLoader: ClassLoader) { 35 | val classNotificationManagerService = XposedHelpers.findClass("com.android.server.notification.NotificationManagerService", classLoader) 36 | 37 | classNotificationManagerService.hookMethod("onStart") { 38 | doAfter { 39 | val context = thisObject.callMethod("getContext") as Context 40 | KeepHmsAlive(context).start() 41 | val stubClass = thisObject.get("mService").javaClass 42 | hookPermission(stubClass) 43 | hookSystemReadyFlag(stubClass) 44 | } 45 | } 46 | 47 | //private boolean isPackageSuspendedForUser(String pkg, int uid) 48 | classNotificationManagerService.hookMethod("isPackageSuspendedForUser", String::class.java, Int::class.java) { 49 | doBefore { 50 | if (Binder.getCallingUid() == 1000) { 51 | //suspend app can not show notification, fake its state 52 | result = false 53 | } 54 | } 55 | } 56 | 57 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 58 | //int resolveNotificationUid(String callingPkg, String targetPkg, int callingUid, int userId) 59 | XposedHelpers.findMethodExact(classNotificationManagerService, "resolveNotificationUid", String::class.java, String::class.java, Int::class.java, Int::class.java) 60 | .deoptimizeMethod() 61 | 62 | //https://cs.android.com/android/platform/superproject/+/android-cts-10.0_r1:frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java;drc=86869c922207a240884697215ba0bf5b89bd0b37;l=1738 63 | // there is a bug from android 10, the enqueueNotificationInternal method 3rd parameter is need a callingUid, in this method, r.sbn.getUid() actually is the targetUid 64 | // when a notification post from HMSPush and snoozed, then the notification will never show again 65 | // this hook temporary fix this issue 66 | try { 67 | classNotificationManagerService.hookMethod("isCallerAndroid", String::class.java, Int::class.java) { 68 | doBefore { 69 | val callingPkg = args[0] as String 70 | if (callingPkg == ANDROID_PACKAGE_NAME) { 71 | result = true 72 | } 73 | } 74 | } 75 | } catch (e: NoSuchMethodError) { 76 | //Samsung One UI 7 delete this method 77 | XLog.d(TAG, "hook isCallerAndroid error, NoSuchMethodError") 78 | } 79 | } 80 | 81 | val classShortcutService = XposedHelpers.findClass("com.android.server.pm.ShortcutService", classLoader) 82 | ShortcutPermissionHooker.hook(classShortcutService) 83 | } 84 | 85 | private fun hookSystemReadyFlag(stubClass: Class) { 86 | stubClass.hookMethod("isSystemConditionProviderEnabled", String::class.java) { 87 | doBefore { 88 | if (args[0] == IS_SYSTEM_HOOK_READY) { 89 | result = true 90 | } 91 | } 92 | } 93 | } 94 | 95 | private fun hookPermission(stubClass: Class) { 96 | NmsPermissionHooker.hook(stubClass) 97 | } 98 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/system/KeepHmsAlive.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.system 2 | 3 | import android.content.ComponentName 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.content.ServiceConnection 7 | import android.os.Handler 8 | import android.os.IBinder 9 | import android.os.Looper 10 | import android.os.Message 11 | import one.yufz.hmspush.hook.XLog 12 | import one.yufz.hmspush.common.HMS_CORE_SERVICE 13 | import one.yufz.hmspush.common.HMS_CORE_SERVICE_ACTION 14 | import one.yufz.hmspush.common.HMS_PACKAGE_NAME 15 | import kotlin.math.min 16 | 17 | class KeepHmsAlive(private val context: Context) { 18 | companion object { 19 | private const val TAG = "KeepHmsAlive" 20 | 21 | private const val MSG_BIND_HMS_SERVICE = 1 22 | 23 | private const val MIN_RETRY_TIMEOUT = 1_000L 24 | private const val MAX_RETRY_TIMEOUT = 30_000L 25 | } 26 | 27 | private val handler = object : Handler(Looper.getMainLooper()) { 28 | override fun handleMessage(msg: Message) { 29 | XLog.d(TAG, "handleMessage() called with: what = ${msg.what}") 30 | 31 | when (msg.what) { 32 | MSG_BIND_HMS_SERVICE -> { 33 | connect() 34 | } 35 | } 36 | } 37 | } 38 | 39 | private var timeout = MIN_RETRY_TIMEOUT 40 | 41 | private var connected: Boolean = false 42 | 43 | private val serviceConnection = object : ServiceConnection { 44 | 45 | override fun onServiceConnected(name: ComponentName, service: IBinder) { 46 | XLog.d(TAG, "onServiceConnected() called with: name = $name, service = $service") 47 | connected = true 48 | timeout = MIN_RETRY_TIMEOUT 49 | } 50 | 51 | override fun onServiceDisconnected(name: ComponentName) { 52 | XLog.d(TAG, "onServiceDisconnected() called with: name = $name") 53 | connected = false 54 | disconnect() 55 | scheduleReconnect() 56 | } 57 | 58 | override fun onBindingDied(name: ComponentName?) { 59 | XLog.d(TAG, "onBindingDied() called with: name = $name") 60 | connected = false 61 | disconnect() 62 | scheduleReconnect() 63 | } 64 | } 65 | 66 | fun start() { 67 | XLog.d(TAG, "start() called") 68 | connect() 69 | } 70 | 71 | private fun connect() { 72 | XLog.d(TAG, "connect() called") 73 | 74 | if (connected) { 75 | XLog.d(TAG, "connect: already connected") 76 | return 77 | } 78 | 79 | val bound = try { 80 | context.bindService(createServiceIntent(), serviceConnection, Context.BIND_AUTO_CREATE or Context.BIND_IMPORTANT) 81 | } catch (t: Throwable) { 82 | XLog.e(TAG, "bindService error", t) 83 | false 84 | } 85 | XLog.d(TAG, "connect() result: $bound") 86 | 87 | if (!bound) { 88 | XLog.d(TAG, "connect() failed, schedule reconnect") 89 | scheduleReconnect() 90 | } 91 | } 92 | 93 | private fun scheduleReconnect() { 94 | XLog.d(TAG, "scheduleReconnect() called") 95 | 96 | if (!handler.hasMessages(MSG_BIND_HMS_SERVICE)) { 97 | timeout = min(MAX_RETRY_TIMEOUT, (timeout * 1.5).toLong()) 98 | 99 | XLog.d(TAG, "scheduleReconnect: scheduling reconnect in $timeout ms") 100 | 101 | handler.sendEmptyMessageDelayed(MSG_BIND_HMS_SERVICE, timeout) 102 | } else { 103 | XLog.d(TAG, "scheduleReconnect() called already has a scheduled reconnect") 104 | } 105 | } 106 | 107 | private fun createServiceIntent(): Intent { 108 | val intent = Intent(HMS_CORE_SERVICE_ACTION).apply { 109 | setClassName(HMS_PACKAGE_NAME, HMS_CORE_SERVICE) 110 | addCategory(Intent.CATEGORY_DEFAULT) 111 | } 112 | return intent 113 | } 114 | 115 | private fun disconnect() { 116 | XLog.d(TAG, "disconnect() called, connected = $connected") 117 | 118 | if (connected) { 119 | context.unbindService(serviceConnection) 120 | } 121 | } 122 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/system/ShortcutPermissionHooker.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.system 2 | 3 | import android.app.AndroidAppHelper 4 | import android.app.Notification 5 | import android.app.NotificationChannelGroup 6 | import android.content.pm.ShortcutInfo 7 | import android.os.Binder 8 | import android.os.Build 9 | import de.robv.android.xposed.XC_MethodHook 10 | import de.robv.android.xposed.XposedHelpers.findClass 11 | import de.robv.android.xposed.XposedHelpers.findMethodExact 12 | import one.yufz.hmspush.common.ANDROID_PACKAGE_NAME 13 | import one.yufz.hmspush.common.HMS_PACKAGE_NAME 14 | import one.yufz.hmspush.hook.XLog 15 | import one.yufz.hmspush.hook.hms.nm.SystemNotificationManager 16 | import one.yufz.xposed.HookCallback 17 | import one.yufz.xposed.hook 18 | 19 | object ShortcutPermissionHooker { 20 | private fun fromHms() = try { 21 | Binder.getCallingUid() == AndroidAppHelper.currentApplication().packageManager.getPackageUid(HMS_PACKAGE_NAME, 0) 22 | } catch (e: Throwable) { 23 | false 24 | } 25 | 26 | private fun tryHookPermission(packageName: String): Boolean { 27 | if (fromHms()) { 28 | Binder.clearCallingIdentity() 29 | return true 30 | } 31 | return false 32 | } 33 | 34 | private fun hookPermission(targetPackageNameParamIndex: Int, hookExtra: (XC_MethodHook.MethodHookParam.() -> Unit)? = null): HookCallback = { 35 | doBefore { 36 | if (tryHookPermission(args[targetPackageNameParamIndex] as String)) { 37 | hookExtra?.invoke(this) 38 | } 39 | } 40 | } 41 | 42 | fun hook(classShortcutService: Class<*>) { 43 | // void pushDynamicShortcut(String packageName, in ShortcutInfo shortcut, int userId); 44 | findMethodExact(classShortcutService, "pushDynamicShortcut", String::class.java, ShortcutInfo::class.java, Int::class.java) 45 | .hook(hookPermission(0)) 46 | 47 | // int getMaxShortcutCountPerActivity(String packageName, int userId); 48 | findMethodExact(classShortcutService, "getMaxShortcutCountPerActivity", String::class.java, Int::class.java) 49 | .hook(hookPermission(0)) 50 | 51 | // void verifyCaller(@NonNull String packageName, @UserIdInt int userId) 52 | findMethodExact(classShortcutService, "verifyCaller", String::class.java, Int::class.java) 53 | .hook(hookPermission(0)) 54 | } 55 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/util/Notification.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.util 2 | 3 | import android.app.Notification 4 | import android.content.Context 5 | import one.yufz.xposed.newInstance 6 | 7 | fun Notification.newBuilder(context: Context): Notification.Builder { 8 | return Notification.Builder::class.java.newInstance(context, this) as Notification.Builder 9 | } 10 | 11 | fun Notification.getText(): String? { 12 | return extras.getString(Notification.EXTRA_TEXT) 13 | } 14 | 15 | fun Notification.getTitle(): String? { 16 | return extras.getString(Notification.EXTRA_TITLE) 17 | } 18 | 19 | fun Notification.getInboxLines(): Array? { 20 | return extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES) 21 | } 22 | 23 | fun Notification.getSummaryText(): CharSequence? { 24 | return extras.getCharSequence(Notification.EXTRA_SUMMARY_TEXT) 25 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/hmspush/hook/util/Util.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.hmspush.hook.util 2 | 3 | import android.content.Context 4 | import one.yufz.xposed.callMethod 5 | 6 | fun Context.getUserId(): Int { 7 | return callMethod("getUserId") as Int? ?: 0 8 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/xposed/Android.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.xposed 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import android.content.ContextWrapper 6 | import android.util.Log 7 | import dalvik.system.BaseDexClassLoader 8 | import de.robv.android.xposed.XC_MethodHook.Unhook 9 | import one.yufz.hmspush.common.doOnce 10 | import one.yufz.hmspush.hook.XLog 11 | 12 | private const val TAG = "AndroidHookUtils" 13 | 14 | fun onApplicationAttachContext(callback: Application.() -> Unit) { 15 | ContextWrapper::class.java.hookMethod("attachBaseContext", Context::class.java) { 16 | doAfter { 17 | if (thisObject is Application) { 18 | unhook() 19 | callback(thisObject as Application) 20 | } 21 | 22 | } 23 | } 24 | } 25 | 26 | fun onDexClassLoaderLoaded(callback: ClassLoader.(unhook: () -> Unit) -> Unit) { 27 | var unhooks: Set? = null 28 | 29 | unhooks = BaseDexClassLoader::class.java.hookAllConstructor { 30 | doAfter { 31 | val hookContext = this@hookAllConstructor 32 | 33 | hookContext.doOnce(thisObject) { 34 | callback(thisObject as ClassLoader) { 35 | unhooks?.forEach { it.unhook() } 36 | } 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/xposed/Layout.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.xposed 2 | 3 | 4 | import android.content.Context 5 | import android.view.View 6 | import android.view.ViewGroup 7 | 8 | inline fun ViewGroup.child(width: Int, height: Int, index: Int = -1, action: T.() -> Unit = {}): T { 9 | val params = ViewGroup.LayoutParams(width, height) 10 | return child(params, index, action) 11 | } 12 | 13 | inline fun ViewGroup.child(params: ViewGroup.LayoutParams? = null, index: Int = -1, action: T.() -> Unit = {}): T { 14 | val child = T::class.java.getConstructor(Context::class.java).newInstance(context) as T 15 | 16 | if (params != null) { 17 | addView(child, index, params) 18 | } else { 19 | addView(child, index) 20 | } 21 | 22 | child.action() 23 | 24 | return child 25 | } -------------------------------------------------------------------------------- /xposed/src/main/java/one/yufz/xposed/XPosedX.kt: -------------------------------------------------------------------------------- 1 | package one.yufz.xposed 2 | 3 | 4 | import de.robv.android.xposed.XC_MethodHook 5 | import de.robv.android.xposed.XposedBridge 6 | import de.robv.android.xposed.XposedHelpers 7 | import java.lang.reflect.Member 8 | import java.lang.reflect.Method 9 | 10 | 11 | fun Any.callMethod(methodName: String, vararg args: Any): Any? = 12 | XposedHelpers.callMethod(this, methodName, *args) 13 | 14 | fun Any.callMethod(methodName: String, parameterTypes: Array>, vararg args: Any): Any? = 15 | XposedHelpers.callMethod(this, methodName, parameterTypes, *args) 16 | 17 | fun Class<*>.callStaticMethod(methodName: String, vararg args: Any): Any? = 18 | XposedHelpers.callStaticMethod(this, methodName, *args) 19 | 20 | fun Class<*>.callStaticMethod( 21 | methodName: String, 22 | parameterTypes: Array>, 23 | vararg args: Any 24 | ): Any? = XposedHelpers.callStaticMethod(this, methodName, parameterTypes, *args) 25 | 26 | typealias HookAction = XC_MethodHook.MethodHookParam.() -> Unit 27 | typealias ReplaceAction = XC_MethodHook.MethodHookParam.() -> Any? 28 | typealias HookCallback = HookContext.() -> Unit 29 | 30 | fun Class<*>.hookMethod(methodName: String, vararg parameterTypes: Class<*>, callback: HookCallback) = 31 | XposedHelpers.findAndHookMethod(this, methodName, *parameterTypes, MethodHook(callback)) 32 | 33 | fun Class<*>.hookConstructor(vararg parameterTypes: Class<*>, callback: HookCallback) = 34 | XposedHelpers.findAndHookConstructor(this, *parameterTypes, MethodHook(callback)) 35 | 36 | fun Class<*>.hookAllConstructor(callback: HookCallback) = 37 | XposedBridge.hookAllConstructors(this, MethodHook(callback)) 38 | 39 | fun hookMethod(className: String, classLoader: ClassLoader, methodName: String, vararg parameterTypes: Class<*>, callback: HookCallback) = 40 | XposedHelpers.findAndHookMethod(className, classLoader, methodName, *parameterTypes, MethodHook(callback)) 41 | 42 | fun hookConstructor(className: String, classLoader: ClassLoader, methodName: String, vararg parameterTypes: Class<*>, callback: HookCallback) = 43 | XposedHelpers.findAndHookConstructor(className, classLoader, methodName, *parameterTypes, MethodHook(callback)) 44 | 45 | fun Method.hook(callback: HookCallback) = XposedBridge.hookMethod(this, MethodHook(callback)) 46 | 47 | fun Class<*>.hookAllMethods(methodName: String, callback: HookCallback) = 48 | XposedBridge.hookAllMethods(this, methodName, MethodHook(callback)) 49 | 50 | class MethodHook(callback: HookCallback) : XC_MethodHook() { 51 | private val context = HookContext(this).apply(callback) 52 | 53 | override fun beforeHookedMethod(param: MethodHookParam) { 54 | super.beforeHookedMethod(param) 55 | 56 | context.replaceAction?.let { 57 | try { 58 | param.result = it.invoke(param) 59 | } catch (t: Throwable) { 60 | param.throwable = t 61 | } 62 | return 63 | } 64 | 65 | context.beforeAction?.invoke(param) 66 | } 67 | 68 | override fun afterHookedMethod(param: MethodHookParam) { 69 | super.afterHookedMethod(param) 70 | context.afterAction?.invoke(param) 71 | } 72 | 73 | } 74 | 75 | class HookContext(private val methodHook: MethodHook) { 76 | internal var beforeAction: HookAction? = null 77 | private set 78 | 79 | internal var afterAction: HookAction? = null 80 | private set 81 | 82 | internal var replaceAction: ReplaceAction? = null 83 | private set 84 | 85 | fun doBefore(action: HookAction) { 86 | this.beforeAction = action 87 | } 88 | 89 | fun doAfter(action: HookAction) { 90 | this.afterAction = action 91 | } 92 | 93 | fun replace(action: ReplaceAction) { 94 | this.replaceAction = action 95 | } 96 | 97 | fun XC_MethodHook.MethodHookParam.unhook() { 98 | XposedBridge.unhookMethod(this.method, methodHook) 99 | } 100 | } 101 | 102 | fun Class<*>.newInstance(vararg args: Any): Any = XposedHelpers.newInstance(this, *args) 103 | 104 | fun Class<*>.newInstance(parameterTypes: Array>, vararg args: Any): Any = 105 | XposedHelpers.newInstance(this, parameterTypes, *args) 106 | 107 | fun ClassLoader.findClass(className: String): Class<*> = XposedHelpers.findClass(className, this) 108 | 109 | inline fun Any.getOrNull(name: String): T? = getField(name, T::class.java) 110 | 111 | inline operator fun Any.get(name: String): T = getField(name, T::class.java)!! 112 | 113 | inline operator fun Any.set(name: String, value: T?) = setField(name, value, T::class.java) 114 | 115 | fun Any.getField(name: String, fieldClazz: Class): T? { 116 | val obj = if (this is Class<*>) null else this 117 | val thisClass = if (this is Class<*>) this else this.javaClass 118 | val field = findField(thisClass, name) 119 | 120 | val value = when (fieldClazz) { 121 | Boolean::class.java -> field.getBoolean(obj) 122 | Byte::class.java -> field.getByte(obj) 123 | Char::class.java -> field.getChar(obj) 124 | Double::class.java -> field.getDouble(obj) 125 | Float::class.java -> field.getFloat(obj) 126 | Int::class.java -> field.getInt(obj) 127 | Long::class.java -> field.getLong(obj) 128 | Short::class.java -> field.getShort(obj) 129 | else -> field.get(obj) 130 | } 131 | return value as? T? 132 | } 133 | 134 | fun Any.setField(name: String, value: T?, fieldClass: Class) { 135 | val obj = if (this is Class<*>) null else this 136 | val thisClass = if (this is Class<*>) this else this.javaClass 137 | 138 | val field = findField(thisClass, name) 139 | 140 | when (fieldClass) { 141 | Boolean::class.java -> field.setBoolean(obj, value as Boolean) 142 | Byte::class.java -> field.setByte(obj, value as Byte) 143 | Char::class.java -> field.setChar(obj, value as Char) 144 | Double::class.java -> field.setDouble(obj, value as Double) 145 | Float::class.java -> field.setFloat(obj, value as Float) 146 | Int::class.java -> field.setInt(obj, value as Int) 147 | Long::class.java -> field.setLong(obj, value as Long) 148 | Short::class.java -> field.setShort(obj, value as Short) 149 | else -> field.set(obj, value as T) 150 | } 151 | } 152 | 153 | private fun findField(clazz: Class<*>, fieldName: String) = XposedHelpers.findField(clazz, fieldName) 154 | 155 | 156 | private val method_deoptimizeMethod = try { 157 | XposedBridge::class.java.getDeclaredMethod("deoptimizeMethod", Member::class.java) 158 | } catch (e: NoSuchMethodException) { 159 | null 160 | } 161 | fun Method.deoptimizeMethod() = method_deoptimizeMethod?.invoke(null, this) --------------------------------------------------------------------------------