├── .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)
--------------------------------------------------------------------------------