diff --git a/.github/workflows/android-test.yml b/.github/workflows/android-test.yml
index d8a663a3f..d7152a3e3 100644
--- a/.github/workflows/android-test.yml
+++ b/.github/workflows/android-test.yml
@@ -35,8 +35,8 @@ jobs:
run: |
./gradlew autojs:buildJsModule
./gradlew :inrt:assemble
- ./gradlew :app:assembleRelease
+ ./gradlew :app:assembleRelease --info
- uses: actions/upload-artifact@v6
with:
name: apks(no signing)
- path: app/build/outputs/apk/v7/release/*.apk
\ No newline at end of file
+ path: app/build/outputs/apk/v7/release/*.apk
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 9c252b38f..aa0a48761 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -141,6 +141,9 @@ android {
sourceSets {
getByName("main") {
res.srcDirs("src/main/res", "src/main/res-i18n")
+ aidl {
+ srcDirs("src/main/aidl", "src/main/java")
+ }
}
}
configurations.all {
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b3e5174a5..c2fbd5866 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -194,6 +194,22 @@
+
+
+
+
+
+
+
+
+
+
+
1) {
+ runningEngines.forEach(compareEngine => {
+ let compareSource = compareEngine.getSource() + ''
+ if (currentEngine.id !== compareEngine.id && compareSource === currentSource) {
+ // 强制关闭同名的脚本
+ compareEngine.forceStop()
+ }
+ })
+}
+
+sleep(100)
+
+if (!floaty.checkPermission()) {
+ toast("需要悬浮窗权限来显示悬浮窗,请在随后的界面中允许并重新运行本脚本。");
+ floaty.requestPermission();
+ exit()
+}
+
+if (!requestScreenCapture()) {
+ toastLog('请求截图权限失败')
+ exit()
+}
+
+// 识别结果和截图信息
+let result = []
+let running = true
+let capturing = true
+
+/**
+ * 截图并识别OCR文本信息
+ */
+function captureAndOcr() {
+ capturing = true
+ let img = captureScreen()
+ if (!img) {
+ toastLog('截图失败')
+ }
+ let start = new Date()
+ result = paddle.ocr(img);
+ let elapsed = new Date() - start
+ log(result);
+ toastLog('耗时' + elapsed + 'ms')
+
+ // 根据当前设备和模型记录识别时间
+ recognitionTime[currentDevice][currentModel] = elapsed
+ updateDeviceTimeDisplay()
+
+ capturing = false
+}
+
+
+// 获取状态栏高度
+let offset = -getStatusBarHeightCompat()
+//let offset = 0;
+
+// 绘制识别结果
+let window = floaty.rawWindow(
+
+);
+
+// 设置悬浮窗位置
+ui.post(() => {
+ window.setPosition(0, offset)
+ window.setSize(device.width, device.height)
+ window.setTouchable(false)
+})
+
+// 操作按钮 - 在每个设备下方显示快速和精确时间
+let clickButtonWindow = floaty.rawWindow(
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+// 拖动功能 - 限制边界(优化版)
+let downX = 0, downY = 0
+let originalX = 0, originalY = 0
+let windowX = 0, windowY = 0
+
+// 拖动时记录位置
+clickButtonWindow.dragBar.setOnTouchListener(function (view, event) {
+ let action = event.getAction()
+ if (action == android.view.MotionEvent.ACTION_DOWN) {
+ downX = event.getRawX()
+ downY = event.getRawY()
+ originalX = clickButtonWindow.getX()
+ originalY = clickButtonWindow.getY()
+ return true
+ } else if (action == android.view.MotionEvent.ACTION_MOVE) {
+ let dx = event.getRawX() - downX
+ let dy = event.getRawY() - downY
+ let newX = originalX + dx
+ let newY = originalY + dy
+
+ // 边界限制
+ let maxX = device.width - clickButtonWindow.getWidth()
+ let maxY = device.height - clickButtonWindow.getHeight()
+
+ newX = Math.max(0, Math.min(maxX, newX))
+ newY = Math.max(0, Math.min(maxY, newY))
+
+ clickButtonWindow.setPosition(newX, newY)
+ // 记录位置
+ windowX = newX
+ windowY = newY
+ return true
+ }
+ return false
+})
+// 记录每个设备+模型的识别时间
+let recognitionTime = {
+ CPU: { fast: null, accurate: null },
+ GPU: { fast: null, accurate: null },
+ Vulkan: { fast: null, accurate: null }
+}
+
+// 更新设备按钮下方的时间显示
+function updateDeviceTimeDisplay() {
+ ui.run(() => {
+ // CPU时间
+ clickButtonWindow.cpuFastTime.setText(recognitionTime.CPU.fast ? "快:" + recognitionTime.CPU.fast + "ms" : "")
+ clickButtonWindow.cpuAccurateTime.setText(recognitionTime.CPU.accurate ? "精:" + recognitionTime.CPU.accurate + "ms" : "")
+
+ // GPU时间
+ clickButtonWindow.gpuFastTime.setText(recognitionTime.GPU.fast ? "快:" + recognitionTime.GPU.fast + "ms" : "")
+ clickButtonWindow.gpuAccurateTime.setText(recognitionTime.GPU.accurate ? "精:" + recognitionTime.GPU.accurate + "ms" : "")
+
+ // Vulkan时间
+ clickButtonWindow.vulkanFastTime.setText(recognitionTime.Vulkan.fast ? "快:" + recognitionTime.Vulkan.fast + "ms" : "")
+ clickButtonWindow.vulkanAccurateTime.setText(recognitionTime.Vulkan.accurate ? "精:" + recognitionTime.Vulkan.accurate + "ms" : "")
+ })
+}
+
+// 当前选中的设备
+let currentDevice = "CPU"
+
+function updateButtonStyle() {
+ ui.run(function () {
+ let defaultColor = "#888888"; // 未选中灰色
+ let selectedColor = "#4caf50"; // 选中绿色
+
+ clickButtonWindow.cpuBtn.setBackgroundColor(colors.parseColor(currentDevice === "CPU" ? selectedColor : defaultColor));
+ clickButtonWindow.gpuBtn.setBackgroundColor(colors.parseColor(currentDevice === "GPU" ? selectedColor : defaultColor));
+ clickButtonWindow.vulkanBtn.setBackgroundColor(colors.parseColor(currentDevice === "Vulkan" ? selectedColor : defaultColor));
+ });
+}
+
+// 当前选中的模型
+let currentModel = "fast" // fast 或 accurate
+
+function updateModelButtonStyle() {
+ ui.run(function () {
+ let defaultColor = "#888888"; // 未选中灰色
+ let selectedColor = "#4caf50"; // 选中绿色
+
+ clickButtonWindow.fastBtn.setBackgroundColor(colors.parseColor(currentModel === "fast" ? selectedColor : defaultColor));
+ clickButtonWindow.accurateBtn.setBackgroundColor(colors.parseColor(currentModel === "accurate" ? selectedColor : defaultColor));
+ });
+}
+
+// imageSize 选项
+let currentImageSize = "256"
+
+// 更新滑条位置和显示
+function updateSizeSeekBar() {
+ let index = currentImageSize / 32 - 4
+ if (index >= 0) {
+ ui.run(() => {
+ clickButtonWindow.sizeSeekBar.setProgress(index)
+ clickButtonWindow.sizeValue.setText(currentImageSize.toString())
+ })
+ }
+}
+
+// 滑条变化事件(带800ms防抖)
+let sizeChangeTimer = null
+clickButtonWindow.sizeSeekBar.setOnSeekBarChangeListener({
+ onProgressChanged: function (seekbar, progress, fromUser) {
+ if (fromUser) {
+ currentImageSize = progress * 32 + 128
+ clickButtonWindow.sizeValue.setText(currentImageSize.toString())
+
+ // 清除之前的定时器
+ if (sizeChangeTimer) {
+ clearTimeout(sizeChangeTimer)
+ }
+
+ // 设置新的定时器,800ms后执行
+ sizeChangeTimer = setTimeout(() => {
+ threads.start(() => {
+ reinitOcr()
+ toastLog('精度已更改为: ' + currentImageSize)
+ })
+ sizeChangeTimer = null
+ }, 800)
+ }
+ },
+ onStartTrackingTouch: function (seekbar) { },
+ onStopTrackingTouch: function (seekbar) { }
+})
+
+ui.run(function () {
+ clickButtonWindow.setPosition(device.width / 2 - ~~(clickButtonWindow.getWidth() / 2), device.height * 0.4)
+ updateButtonStyle()
+ updateModelButtonStyle()
+ updateSizeSeekBar()
+})
+
+// 重新初始化OCR配置(包含设备和模型)
+function reinitOcr() {
+ let config = {
+ device: currentDevice,
+ useSlim: currentModel === "fast",
+ scoreThreshold: 0.5,
+ imageSize: currentImageSize
+ }
+ console.log(config)
+ paddle.initOcrWithConfig(config)
+}
+
+reinitOcr()
+//captureAndOcr()
+
+// 设备切换函数
+function switchDevice(device) {
+ if (currentDevice == device) return
+
+ // 禁用设备按钮
+ ui.run(() => {
+ clickButtonWindow.cpuBtn.setEnabled(false)
+ clickButtonWindow.gpuBtn.setEnabled(false)
+ clickButtonWindow.vulkanBtn.setEnabled(false)
+ })
+
+ threads.start(() => {
+ try {
+ currentDevice = device
+ reinitOcr()
+ ui.run(() => updateButtonStyle())
+ toastLog('识别设备已更改为: ' + device)
+ } catch (e) {
+ toastLog('切换设备失败: ' + e)
+ } finally {
+ ui.run(() => {
+ clickButtonWindow.cpuBtn.setEnabled(true)
+ clickButtonWindow.gpuBtn.setEnabled(true)
+ clickButtonWindow.vulkanBtn.setEnabled(true)
+ })
+ }
+ })
+}
+
+// 模型切换函数
+function switchModel(model) {
+ if (currentModel == model) return
+
+ threads.start(() => {
+ try {
+ currentModel = model
+ currentImageSize = model === "fast" ? 256 : 512
+ reinitOcr()
+ updateModelButtonStyle()
+ updateSizeSeekBar()
+ toastLog('识别模型已更改为: ' + (model === "fast" ? "快速" : "精确"))
+ } catch (e) {
+ toastLog('切换模型失败: ' + e)
+ }
+ })
+}
+
+// 绑定点击事件
+clickButtonWindow.cpuBtn.click(() => switchDevice("CPU"))
+clickButtonWindow.gpuBtn.click(() => switchDevice("GPU"))
+clickButtonWindow.vulkanBtn.click(() => switchDevice("Vulkan"))
+
+// 绑定模型点击事件
+clickButtonWindow.fastBtn.click(() => switchModel("fast"))
+clickButtonWindow.accurateBtn.click(() => switchModel("accurate"))
+
+// 点击识别
+clickButtonWindow.captureAndOcr.click(function () {
+ if (result.length) clearPaint();
+ result = []
+
+ ui.run(function () {
+ clickButtonWindow.setPosition(device.width, device.height)
+ })
+ setTimeout(() => {
+ threads.start(() => {
+ captureAndOcr()
+ if (window.canvas.updateCanvas) {
+ window.canvas.updateCanvas()
+ }
+ ui.run(function () {
+ if (windowX === 0 && windowY === 0) {
+ // 默认居中
+ clickButtonWindow.setPosition(device.width / 2 - ~~(clickButtonWindow.getWidth() / 2), device.height * 0.4)
+ } else {
+ clickButtonWindow.setPosition(windowX, windowY)
+ }
+ })
+ })
+ }, 500)
+})
+
+function clearPaint() {
+ ui.run(() => {
+ // 清空识别结果
+ result = []
+ // 重置 paint 到初始状态
+ paint.reset()
+ // 重新设置基本属性(如果需要保留基本样式)
+ paint.setStrokeWidth(1)
+ paint.setTypeface(Typeface.DEFAULT_BOLD)
+ paint.setTextAlign(Paint.Align.LEFT)
+ paint.setAntiAlias(true)
+ // 刷新画布
+ if (window.canvas.updateCanvas) {
+ window.canvas.updateCanvas()
+ }
+ })
+}
+
+clickButtonWindow.clearBtn.click(clearPaint)
+
+// 点击关闭
+clickButtonWindow.closeBtn.click(function () {
+ exit()
+})
+
+let Typeface = android.graphics.Typeface
+let paint = new Paint()
+paint.setStrokeWidth(1)
+paint.setTypeface(Typeface.DEFAULT_BOLD)
+paint.setTextAlign(Paint.Align.LEFT)
+paint.setAntiAlias(true)
+paint.setStrokeJoin(Paint.Join.ROUND)
+paint.setDither(true)
+window.canvas.on('draw', function (canvas) {
+ if (!running || capturing) {
+ return
+ }
+ // 清空内容
+ canvas.drawColor(0xFFFFFF, android.graphics.PorterDuff.Mode.CLEAR)
+ if (result && result.length > 0) {
+ for (let i = 0; i < result.length; i++) {
+ let ocrResult = result[i]
+ drawRectAndText(ocrResult.words + ' #信心:' + ocrResult.confidence.toFixed(2), ocrResult.bounds, '#60bb60', canvas, paint);
+ }
+ }
+})
+
+setInterval(() => { }, 10000)
+events.on('exit', () => {
+ // 标记停止 避免canvas导致闪退
+ running = false
+ // 撤销监听
+ window.canvas.removeAllListeners()
+
+})
+
+/**
+ * 绘制文本和方框
+ *
+ * @param {*} desc
+ * @param {*} rect
+ * @param {*} colorStr
+ * @param {*} canvas
+ * @param {*} paint
+ */
+function drawRectAndText(desc, rect, colorStr, canvas, paint) {
+ let color = colors.parseColor(colorStr)
+
+ paint.setStrokeWidth(1)
+ paint.setStyle(Paint.Style.STROKE)
+ // 反色
+ paint.setARGB(255, 255 - (color >> 16 & 0xff), 255 - (color >> 8 & 0xff), 255 - (color & 0xff))
+ canvas.drawRect(rect, paint)
+ paint.setARGB(255, color >> 16 & 0xff, color >> 8 & 0xff, color & 0xff)
+ paint.setStrokeWidth(1)
+ paint.setTextSize(20)
+ paint.setStyle(Paint.Style.FILL)
+ canvas.drawText(desc, rect.left, rect.top, paint)
+ paint.setTextSize(10)
+ paint.setStrokeWidth(1)
+ paint.setARGB(255, 0, 0, 0)
+}
+
+/**
+ * 获取状态栏高度
+ *
+ * @returns
+ */
+function getStatusBarHeightCompat() {
+ let result = 0
+ let resId = context.getResources().getIdentifier("status_bar_height", "dimen", "android")
+ if (resId > 0) {
+ result = context.getResources().getDimensionPixelOffset(resId)
+ }
+ if (result <= 0) {
+ result = context.getResources().getDimensionPixelOffset(R.dimen.dimen_25dp)
+ }
+ return result
+}
diff --git a/app/src/main/java/com/aiselp/autox/ui/material3/DrawerPage.kt b/app/src/main/java/com/aiselp/autox/ui/material3/DrawerPage.kt
index 12567b278..7b3cbe61a 100644
--- a/app/src/main/java/com/aiselp/autox/ui/material3/DrawerPage.kt
+++ b/app/src/main/java/com/aiselp/autox/ui/material3/DrawerPage.kt
@@ -5,12 +5,15 @@ import android.app.Activity
import android.app.AppOpsManager
import android.content.Context
import android.content.Intent
+import android.net.Uri
import android.provider.Settings
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -25,9 +28,13 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ExitToApp
+import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.Notifications
+import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Settings
+import androidx.compose.material3.DrawerState
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@@ -70,13 +77,13 @@ import com.stardust.app.isOpPermissionGranted
import com.stardust.app.permission.DrawOverlaysPermission.launchCanDrawOverlaysSettings
import com.stardust.app.permission.PermissionsSettingsUtil
import com.stardust.autojs.IndependentScriptService
+import com.stardust.autojs.core.accessibility.AccessibilityProxyAccessor
import com.stardust.autojs.core.pref.PrefKey
import com.stardust.autojs.core.shizuku.ShizukuClient
import com.stardust.autojs.servicecomponents.EngineController
import com.stardust.autojs.servicecomponents.ScriptServiceConnection
import com.stardust.toast
import com.stardust.util.IntentUtil
-import com.stardust.view.accessibility.AccessibilityService
import io.github.g00fy2.quickie.QRResult
import io.github.g00fy2.quickie.ScanQRCode
import kotlinx.coroutines.DelicateCoroutinesApi
@@ -87,7 +94,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.autojs.autojs.Pref
import org.autojs.autojs.devplugin.DevPlugin
-import org.autojs.autojs.tool.AccessibilityServiceTool
import org.autojs.autojs.tool.WifiTool
import org.autojs.autojs.ui.floating.FloatyWindowManger
import org.autojs.autojs.ui.main.drawer.DrawerViewModel
@@ -102,7 +108,7 @@ private const val FEEDBACK_ADDRESS = "https://github.com/aiselp/AutoX/issues"
@Composable
-fun DrawerPage() {
+fun DrawerPage(drawerState: DrawerState) {
ModalDrawerSheet(Modifier.width(300.dp)) {
Column(Modifier.fillMaxSize()) {
val textStyle = MaterialTheme.typography.titleMedium
@@ -121,13 +127,15 @@ fun DrawerPage() {
}
Spacer(modifier = Modifier.height(16.dp))
Text(text = stringResource(R.string.text_service), style = textStyle)
- AccessibilityServiceSwitch()
+ AccessibilityServiceSwitch(drawerState = drawerState)
StableModeSwitch()
- NotificationUsageRightSwitch()
+ //NotificationUsageRightSwitch()
ForegroundServiceSwitch()
- UsageStatsPermissionSwitch()
- ShizukuPermissionSwitch()
- PublishNotificationSwitch()
+ //UsageStatsPermissionSwitch()
+ //ShizukuPermissionSwitch()
+ //(drawerState = drawerState)
+ //PublishNotificationSwitch()
+ PermissionGroup(drawerState = drawerState)
Text(text = stringResource(id = R.string.text_script_record), style = textStyle)
FloatingWindowSwitch()
@@ -153,20 +161,43 @@ fun DrawerPage() {
}
@Composable
-private fun AccessibilityServiceSwitch() {
+private fun AccessibilityServiceSwitch(drawerState: DrawerState) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val dialog = remember { DialogController() }
- val isAccessibilityServiceEnabled = remember {
- mutableStateOf(AccessibilityServiceTool.isAccessibilityServiceEnabled(context))
+
+ // 状态
+ val isAccessibilityServiceEnabled = remember { mutableStateOf(false) }
+
+ // 每次显示时刷新
+ LaunchedEffect(drawerState.isOpen) {
+ if (drawerState.isOpen) {
+ val enabled = withContext(Dispatchers.IO) {
+ try {
+ AccessibilityProxyAccessor.getInstance().isEnabled
+ } catch (e: Exception) {
+ false
+ }
+ }
+ isAccessibilityServiceEnabled.value = enabled
+ }
}
+
val accessibilitySettingsLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- isAccessibilityServiceEnabled.value =
- AccessibilityServiceTool.isAccessibilityServiceEnabled(context)
- if (!isAccessibilityServiceEnabled.value) {
- isAccessibilityServiceEnabled.value = false
- toast(context, R.string.text_accessibility_service_is_not_enable)
+ // 设置页面返回后刷新
+ scope.launch {
+ val enabled = withContext(Dispatchers.IO) {
+ try {
+ AccessibilityProxyAccessor.getInstance().isEnabled
+ } catch (e: Exception) {
+ false
+ }
+ }
+ isAccessibilityServiceEnabled.value = enabled
+ if (!enabled) {
+ toast(context, R.string.text_accessibility_service_is_not_enable)
+ }
}
}
val editor = remember { mutableStateOf(Pref.getEditor()) }
@@ -190,21 +221,36 @@ private fun AccessibilityServiceSwitch() {
title = stringResource(id = R.string.text_accessibility_service),
checked = isAccessibilityServiceEnabled.value,
onCheckedChange = {
- if (!isAccessibilityServiceEnabled.value) {
- if (Pref.shouldEnableAccessibilityServiceByRoot()) {
- scope.launch {
- val enabled = withContext(Dispatchers.IO) {
- AccessibilityServiceTool.enableAccessibilityServiceByRootAndWaitFor(2000)
+ scope.launch {
+ if (!isAccessibilityServiceEnabled.value) {
+ // 启用
+ val enabled = withContext(Dispatchers.IO) {
+ try {
+ AccessibilityProxyAccessor.getInstance().ensureEnabled()
+ } catch (e: Exception) {
+ false
}
- if (enabled) isAccessibilityServiceEnabled.value = true
- else dialog.show()
}
- } else scope.launch { dialog.show() }
- } else {
- isAccessibilityServiceEnabled.value = !AccessibilityService.disable()
+ if (enabled) {
+ isAccessibilityServiceEnabled.value = true
+ } else {
+ dialog.show()
+ }
+ } else {
+ // 禁用
+ val disabled = withContext(Dispatchers.IO) {
+ try {
+ AccessibilityProxyAccessor.getInstance().disable()
+ } catch (e: Exception) {
+ false
+ }
+ }
+ isAccessibilityServiceEnabled.value = !disabled
+ }
}
}
)
+
dialog.BaseDialog(
onDismissRequest = { scope.launch { dialog.dismiss() } },
title = { DialogTitle(title = stringResource(R.string.text_need_to_enable_accessibility_service)) },
@@ -298,6 +344,122 @@ fun ShizukuPermissionSwitch() {
)
}
+@Composable
+private fun PermissionGroup(drawerState: DrawerState) {
+ val prefs = PreferenceManager.getDefaultSharedPreferences(LocalContext.current)
+ var expanded by remember { mutableStateOf(prefs.getBoolean("permission_group_expanded", true)) }
+
+ Column {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ expanded = !expanded
+ prefs.edit { putBoolean("permission_group_expanded", expanded) }
+ }
+ .padding(vertical = 12.dp)
+ .padding(end = 12.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = stringResource(R.string.permission_management),
+ style = MaterialTheme.typography.titleMedium
+ )
+ Icon(
+ imageVector = if (expanded) Icons.Default.KeyboardArrowDown else Icons.AutoMirrored.Filled.KeyboardArrowRight,
+ contentDescription = null
+ )
+ }
+
+ AnimatedVisibility(visible = expanded) {
+ Column {
+ NotificationUsageRightSwitch()
+ UsageStatsPermissionSwitch()
+ ShizukuPermissionSwitch()
+ MediaProjectionPermissionSwitch(drawerState)
+ OverlayPermissionSwitch(drawerState)
+ PublishNotificationSwitch()
+ }
+ }
+ }
+}
+
+@Composable
+fun MediaProjectionPermissionSwitch(drawerState: DrawerState) {
+ val context = LocalContext.current
+ val scope = rememberCoroutineScope()
+ var isGranted by remember { mutableStateOf(false) }
+
+ LaunchedEffect(drawerState.isOpen) {
+ if (drawerState.isOpen) {
+ isGranted = ShizukuClient.checkAppOpsPermission("PROJECT_MEDIA")
+ }
+ }
+
+ SettingOptionSwitch(
+ icon = Icons.Default.PlayArrow,
+ title = stringResource(R.string.media_projection_permission),
+ checked = isGranted,
+ tint = Color(0xFFE91E63),
+ onCheckedChange = { enable ->
+ scope.launch {
+ val success = ShizukuClient.setAppOpsPermission("PROJECT_MEDIA", enable)
+
+ if (success) {
+ isGranted = enable
+ toast(context, if (enable) R.string.media_projection_permission_enabled else R.string.media_projection_permission_disabled)
+ } else {
+ toast(context, R.string.media_projection_permission_need_shizuku)
+ }
+ }
+ }
+ )
+}
+
+@Composable
+fun OverlayPermissionSwitch(drawerState: DrawerState) {
+ val context = LocalContext.current
+ val scope = rememberCoroutineScope()
+ var isGranted by remember { mutableStateOf(false) }
+
+ val overlaySettingsLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.StartActivityForResult()
+ ) {
+ scope.launch {
+ isGranted = ShizukuClient.checkAppOpsPermission("SYSTEM_ALERT_WINDOW")
+ }
+ }
+
+ LaunchedEffect(drawerState.isOpen) {
+ if (drawerState.isOpen) {
+ isGranted = ShizukuClient.checkAppOpsPermission("SYSTEM_ALERT_WINDOW")
+ }
+ }
+
+ SettingOptionSwitch(
+ icon = Icons.Default.Settings,
+ title = stringResource(R.string.overlay_permission),
+ checked = isGranted,
+ tint = Color(0xFF4CAF50),
+ onCheckedChange = { enable ->
+ scope.launch {
+ val success = ShizukuClient.setAppOpsPermission("SYSTEM_ALERT_WINDOW", enable)
+
+ if (success) {
+ isGranted = enable
+ } else {
+ toast(context, R.string.overlay_permission_need_shizuku)
+
+ val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
+ intent.data = Uri.parse("package:${context.packageName}")
+ overlaySettingsLauncher.launch(intent)
+ }
+ }
+ }
+ )
+}
+
@Composable
private fun NotificationUsageRightSwitch() {
suspend fun notificationListenerEnable(): Boolean {
diff --git a/app/src/main/java/org/autojs/autojs/App.kt b/app/src/main/java/org/autojs/autojs/App.kt
index b12671186..2eae02e72 100644
--- a/app/src/main/java/org/autojs/autojs/App.kt
+++ b/app/src/main/java/org/autojs/autojs/App.kt
@@ -78,7 +78,7 @@ class App : Application(), Configuration.Provider {
initDynamicBroadcastReceivers()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
WebView.setDataDirectorySuffix(getString(R.string.text_script_process_name))
- };
+ }
} else if (ProcessUtils.isMainProcess(this)) {
initResource()
EngineController.scope.launch {
diff --git a/app/src/main/java/org/autojs/autojs/autojs/AutoJs.java b/app/src/main/java/org/autojs/autojs/autojs/AutoJs.java
index 2c4d7c9c1..ac0a82e13 100644
--- a/app/src/main/java/org/autojs/autojs/autojs/AutoJs.java
+++ b/app/src/main/java/org/autojs/autojs/autojs/AutoJs.java
@@ -11,8 +11,8 @@
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import com.stardust.app.GlobalAppContext;
+import com.stardust.autojs.core.accessibility.AccessibilityProxyAccessor;
import com.stardust.autojs.core.console.GlobalConsole;
-import com.stardust.autojs.runtime.ScriptRuntime;
import com.stardust.autojs.runtime.ScriptRuntimeV2;
import com.stardust.autojs.runtime.accessibility.AccessibilityConfig;
import com.stardust.autojs.runtime.api.AppUtils;
@@ -64,7 +64,8 @@ private interface LayoutInspectFloatyWindow {
@Override
public void onReceive(Context context, Intent intent) {
try {
- ensureAccessibilityServiceEnabled();
+ //ensureAccessibilityServiceEnabled();
+ AccessibilityProxyAccessor.getInstance().ensureEnabled();
String action = intent.getAction();
if (LayoutBoundsFloatyWindow.class.getName().equals(action)) {
capture(LayoutBoundsFloatyWindow::new);
diff --git a/app/src/main/java/org/autojs/autojs/devplugin/DevPluginServiceImpl.kt b/app/src/main/java/org/autojs/autojs/devplugin/DevPluginServiceImpl.kt
new file mode 100644
index 000000000..f6e5f4dd1
--- /dev/null
+++ b/app/src/main/java/org/autojs/autojs/devplugin/DevPluginServiceImpl.kt
@@ -0,0 +1,171 @@
+package org.autojs.autojs.devplugin
+
+import android.app.Service
+import android.content.Intent
+import android.os.IBinder
+import android.os.RemoteException
+import android.util.Log
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import org.autojs.autojs.Pref
+import java.io.IOException
+
+class DevPluginServiceImpl : Service() {
+
+ companion object {
+ private const val TAG = "DevPluginService"
+ }
+
+ private fun getUrl(input: String): String {
+ var url1 = input
+ if (!url1.matches(Regex("^(ws|wss)://.*"))) {
+ url1 = "ws://${url1}"
+ }
+ if (!url1.matches(Regex("^.+://.+?:.+$"))) {
+ url1 += ":${DevPlugin.SERVER_PORT}"
+ }
+ return url1
+ }
+
+ private val binder = object : IDevPluginService.Stub() {
+
+ @Throws(RemoteException::class)
+ override fun connectToComputer(url: String) {
+ val fullUrl = getUrl(url)
+ Log.d(TAG, "AIDL调用: connectToComputer($fullUrl)")
+ CoroutineScope(Dispatchers.Main).launch {
+ try {
+ DevPlugin.connect(fullUrl)
+ Log.d(TAG, "连接成功: $fullUrl")
+ } catch (e: IOException) {
+ Log.e(TAG, "连接失败: IO错误", e)
+ } catch (e: IllegalStateException) {
+ Log.e(TAG, "连接失败: 状态错误", e)
+ } catch (e: Exception) {
+ Log.e(TAG, "连接失败: ${e.message}", e)
+ }
+ }
+ }
+
+ @Throws(RemoteException::class)
+ override fun disconnectFromComputer() {
+ Log.d(TAG, "AIDL调用: disconnectFromComputer()")
+ CoroutineScope(Dispatchers.Main).launch {
+ try {
+ DevPlugin.close()
+ Log.d(TAG, "断开连接成功")
+ } catch (e: IllegalStateException) {
+ Log.e(TAG, "断开连接失败: 状态错误", e)
+ } catch (e: Exception) {
+ Log.e(TAG, "断开连接失败: ${e.message}", e)
+ }
+ }
+ }
+
+ @Throws(RemoteException::class)
+ override fun isComputerConnected(): Boolean {
+ return try {
+ val connected = DevPlugin.isActive
+ Log.d(TAG, "电脑连接状态: $connected")
+ connected
+ } catch (e: IllegalStateException) {
+ Log.e(TAG, "获取连接状态失败: 状态错误", e)
+ false
+ } catch (e: Exception) {
+ Log.e(TAG, "获取连接状态失败: ${e.message}", e)
+ false
+ }
+ }
+
+ @Throws(RemoteException::class)
+ override fun getSavedServerAddress(): String {
+ Log.d(TAG, "AIDL调用: getSavedServerAddress()")
+ return try {
+ val address = Pref.getServerAddressOrDefault("")
+ Log.d(TAG, "保存的地址: $address")
+ address
+ } catch (e: IllegalStateException) {
+ Log.e(TAG, "获取保存地址失败: 状态错误", e)
+ ""
+ } catch (e: Exception) {
+ Log.e(TAG, "获取保存地址失败: ${e.message}", e)
+ ""
+ }
+ }
+
+ @Throws(RemoteException::class)
+ override fun startUSBDebug() {
+ Log.d(TAG, "AIDL调用: startUSBDebug()")
+ CoroutineScope(Dispatchers.Main).launch {
+ try {
+ DevPlugin.startUSBDebug()
+ Log.d(TAG, "启动USB调试成功")
+ } catch (e: IOException) {
+ Log.e(TAG, "启动USB调试失败: IO错误", e)
+ } catch (e: SecurityException) {
+ Log.e(TAG, "启动USB调试失败: 权限不足", e)
+ } catch (e: Exception) {
+ Log.e(TAG, "启动USB调试失败: ${e.message}", e)
+ }
+ }
+ }
+
+ @Throws(RemoteException::class)
+ override fun stopUSBDebug() {
+ Log.d(TAG, "AIDL调用: stopUSBDebug()")
+ CoroutineScope(Dispatchers.Main).launch {
+ try {
+ DevPlugin.stopUSBDebug()
+ Log.d(TAG, "停止USB调试成功")
+ } catch (e: IOException) {
+ Log.e(TAG, "停止USB调试失败: IO错误", e)
+ } catch (e: Exception) {
+ Log.e(TAG, "停止USB调试失败: ${e.message}", e)
+ }
+ }
+ }
+
+ @Throws(RemoteException::class)
+ override fun isUSBDebugActive(): Boolean {
+ Log.d(TAG, "AIDL调用: isUSBDebugActive()")
+ return try {
+ val active = DevPlugin.isUSBDebugServiceActive
+ Log.d(TAG, "USB调试状态: $active")
+ active
+ } catch (e: IllegalStateException) {
+ Log.e(TAG, "获取USB调试状态失败: 状态错误", e)
+ false
+ } catch (e: Exception) {
+ Log.e(TAG, "获取USB调试状态失败: ${e.message}", e)
+ false
+ }
+ }
+
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ Log.d("DevPluginService", "DevPluginServiceImpl onCreate")
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ Log.d(TAG, "收到启动命令")
+ return START_NOT_STICKY
+ }
+
+ override fun onBind(intent: Intent?): IBinder {
+ Log.d(TAG, "收到绑定请求,返回AIDL接口")
+ return binder
+ }
+
+ override fun onUnbind(intent: Intent?): Boolean {
+ Log.d(TAG, "服务解绑")
+ return super.onUnbind(intent)
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ Log.d(TAG, "DevPluginServiceImpl 销毁")
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/autojs/autojs/tool/AccessibilityProxyServiceImpl.kt b/app/src/main/java/org/autojs/autojs/tool/AccessibilityProxyServiceImpl.kt
new file mode 100644
index 000000000..3e3085a83
--- /dev/null
+++ b/app/src/main/java/org/autojs/autojs/tool/AccessibilityProxyServiceImpl.kt
@@ -0,0 +1,113 @@
+package org.autojs.autojs.tool
+
+import android.app.Service
+import android.content.ComponentName
+import android.content.Intent
+import android.content.ServiceConnection
+import android.os.IBinder
+import android.os.RemoteException
+import android.util.Log
+import com.stardust.autojs.core.accessibility.AccessibilityProxyService
+import com.stardust.autojs.core.accessibility.IAccessibilityProxyService
+
+class AccessibilityProxyServiceImpl : Service() {
+
+ companion object {
+ private const val TAG = "AccessibilityProxyService"
+ }
+
+ private var proxyService: IAccessibilityProxyService? = null
+ private var isProxyBound = false
+
+ private val proxyConnection = object : ServiceConnection {
+ override fun onServiceConnected(name: ComponentName, binder: IBinder) {
+ proxyService = IAccessibilityProxyService.Stub.asInterface(binder)
+ isProxyBound = true
+ Log.d(TAG, "绑定到 AccessibilityProxyService 成功")
+ }
+
+ override fun onServiceDisconnected(name: ComponentName) {
+ proxyService = null
+ isProxyBound = false
+ Log.d(TAG, "AccessibilityProxyService 连接断开")
+ }
+ }
+
+ private fun bindToAccessibilityProxy() {
+ try {
+ val intent = Intent(this, AccessibilityProxyService::class.java)
+ intent.setPackage(packageName)
+ bindService(intent, proxyConnection, BIND_AUTO_CREATE)
+ } catch (e: SecurityException) {
+ Log.e(TAG, "绑定失败: 权限不足", e)
+ } catch (e: IllegalArgumentException) {
+ Log.e(TAG, "绑定失败: Intent或Service无效", e)
+ } catch (e: Exception) {
+ Log.e(TAG, "绑定失败: 未知错误", e)
+ }
+ }
+
+ private val binder = object : IAccessibilityProxyService.Stub() {
+ override fun isEnabled(): Boolean {
+ return try {
+ val enabled = proxyService?.isEnabled() ?: false
+ Log.d(TAG, "isEnabled: $enabled")
+ enabled
+ } catch (e: RemoteException) {
+ Log.e(TAG, "检查无障碍服务状态失败", e)
+ false
+ }
+ }
+
+ override fun ensureEnabled(): Boolean {
+ return try {
+ val result = proxyService?.ensureEnabled() ?: false
+ Log.d(TAG, "enable: $result")
+ result
+ } catch (e: RemoteException) {
+ Log.e(TAG, "启用无障碍服务失败", e)
+ false
+ }
+ }
+
+ override fun disable(): Boolean {
+ return try {
+ val result = proxyService?.disable() ?: false
+ Log.d(TAG, "disable: $result")
+ result
+ } catch (e: RemoteException) {
+ Log.e(TAG, "禁用无障碍服务失败", e)
+ false
+ }
+ }
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ Log.d(TAG, "AccessibilityProxyServiceImpl onCreate")
+ bindToAccessibilityProxy()
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ Log.d(TAG, "收到启动命令")
+ return START_NOT_STICKY
+ }
+
+ override fun onBind(intent: Intent?): IBinder {
+ Log.d(TAG, "收到绑定请求,返回 IAccessibilityProxyService 接口")
+ return binder
+ }
+
+ override fun onUnbind(intent: Intent?): Boolean {
+ Log.d(TAG, "服务解绑")
+ return super.onUnbind(intent)
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ if (isProxyBound) {
+ unbindService(proxyConnection)
+ }
+ Log.d(TAG, "AccessibilityProxyServiceImpl 销毁")
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/autojs/autojs/tool/AccessibilityServiceTool.java b/app/src/main/java/org/autojs/autojs/tool/AccessibilityServiceTool.java
index 5a5a3e2d1..323c26311 100644
--- a/app/src/main/java/org/autojs/autojs/tool/AccessibilityServiceTool.java
+++ b/app/src/main/java/org/autojs/autojs/tool/AccessibilityServiceTool.java
@@ -3,6 +3,7 @@
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.text.TextUtils;
+import android.util.Log;
import com.stardust.app.GlobalAppContext;
import org.autojs.autojs.Pref;
@@ -13,6 +14,9 @@
import com.stardust.view.accessibility.AccessibilityServiceUtils;
import java.util.Locale;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
/**
* Created by Stardust on 2017/1/26.
@@ -60,17 +64,54 @@ public static boolean enableAccessibilityServiceByRoot(Class extends android.a
try {
return TextUtils.isEmpty(ProcessShell.execCommand(String.format(Locale.getDefault(), cmd, serviceName), true).error);
} catch (Exception e) {
+ String msg = e.getMessage();
+ if (msg != null && (msg.contains("error=13") || msg.contains("Permission denied"))) {
+ Log.e("AccessibilityService", "su命令无权限 (error=13)");
+ Context context = GlobalAppContext.get();
+ GlobalAppContext.toast(context.getString(R.string.text_root_permission_incomplete) + context.getString(R.string.app_name));
+ return false;
+ }
return false;
}
}
- public static boolean enableAccessibilityServiceByRootAndWaitFor(long timeOut) {
+ public static boolean enableAccessibilityServiceByRootAndWaitFor1(long timeOut) {
if (enableAccessibilityServiceByRoot(sAccessibilityServiceClass)) {
return AccessibilityService.Companion.waitForEnabled(timeOut);
}
return false;
}
+ public static boolean enableAccessibilityServiceByRootAndWaitFor(long timeOut) {
+
+ final AtomicBoolean shouldWait = new AtomicBoolean(true);
+
+ // 1. 在后台线程启动等待
+ CompletableFuture waitFuture = CompletableFuture.supplyAsync(() -> {
+ if (shouldWait.get()) {
+ return AccessibilityService.Companion.waitForEnabled(timeOut);
+ }
+ return false;
+ });
+
+ // 2. 执行root命令
+ boolean rootSuccess = enableAccessibilityServiceByRoot(sAccessibilityServiceClass);
+
+ // 3. 如果root失败,停止等待
+ if (!rootSuccess) {
+ shouldWait.set(false);
+ // 中断等待线程
+ waitFuture.complete(false);
+ return false;
+ }
+ // 4. 等待结果
+ try {
+ return waitFuture.get(timeOut, TimeUnit.MILLISECONDS);
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
public static void enableAccessibilityServiceByRootIfNeeded() {
if (AccessibilityService.Companion.getInstance() == null)
if (Pref.shouldEnableAccessibilityServiceByRoot()) {
diff --git a/app/src/main/java/org/autojs/autojs/ui/main/MainActivity.kt b/app/src/main/java/org/autojs/autojs/ui/main/MainActivity.kt
index 0c8087c57..6e21829fe 100644
--- a/app/src/main/java/org/autojs/autojs/ui/main/MainActivity.kt
+++ b/app/src/main/java/org/autojs/autojs/ui/main/MainActivity.kt
@@ -121,7 +121,7 @@ class MainActivity : AppCompatActivity() {
ModalNavigationDrawer(
drawerState = drawerState,
gesturesEnabled = drawerState.isOpen,
- drawerContent = { DrawerPage() }
+ drawerContent = { DrawerPage(drawerState) }
) {
MainPage(
scriptListFragment = scriptListFragment,
diff --git a/app/src/main/res-i18n/values-en/strings.xml b/app/src/main/res-i18n/values-en/strings.xml
index fa25c28e4..a3f794322 100644
--- a/app/src/main/res-i18n/values-en/strings.xml
+++ b/app/src/main/res-i18n/values-en/strings.xml
@@ -510,4 +510,12 @@
Reconnecting
Connecting
Handshake failed
+ Root permission incomplete! Please grant permission to \":script\" in Magisk
+ Media Projection Permission
+ Enabled, auto-allow screen recording/casting
+ Disabled, user confirmation required each time
+ Shizuku or Root permission required
+ Permission Management
+ Display over other apps
+ No Shizuku or Root permission, please enable manually after redirect
diff --git a/app/src/main/res-i18n/values/strings.xml b/app/src/main/res-i18n/values/strings.xml
index 95e87cd98..05eaee2e7 100644
--- a/app/src/main/res-i18n/values/strings.xml
+++ b/app/src/main/res-i18n/values/strings.xml
@@ -559,4 +559,12 @@
勾选以下权限要求会让打包的app启动时向用户申请并取得权限后才能运行
文件无法运行
内存使用量
+ root权限不完整!需要到Magisk中为本软件的「:script」授权
+ 媒体投影权限
+ 已启用,自动允许屏幕录制/投射
+ 已关闭,每次屏幕录制需手动确认
+ 需要 Shizuku 或 Root 权限
+ 权限管理
+ 显示在其他应用上层
+ 无 Shizuku 或 Root 权限,跳转后手动开启
diff --git a/autojs/src/main/AndroidManifest.xml b/autojs/src/main/AndroidManifest.xml
index b2ae8e9e8..3bd46c9f1 100644
--- a/autojs/src/main/AndroidManifest.xml
+++ b/autojs/src/main/AndroidManifest.xml
@@ -69,6 +69,13 @@
android:taskAffinity="com.stardust.autojs.runtime.api.image.ScreenCaptureRequestActivity"
android:theme="@style/ScriptTheme.Transparent" />
+
+
startForeground()
- ACTION_STOP_FOREGROUND -> stopServiceInternal()
+ ACTION_STOP_FOREGROUND -> {
+ isForegroundRunning = false
+ stopServiceInternal()
+ }
}
return super.onStartCommand(intent, flags, startId)
}
override fun onDestroy() {
+ isForegroundRunning = false
scope.cancel()
super.onDestroy()
Log.i(TAG, "IndependentScriptService Service destroyed")
@@ -120,6 +125,9 @@ class IndependentScriptService : AbstractAutoService() {
private val CHANEL_ID = IndependentScriptService::class.java.name + "_foreground"
const val ACTION_START_FOREGROUND = "action_start_foreground"
const val ACTION_STOP_FOREGROUND = "action_stop_foreground"
+ @Volatile
+ var isForegroundRunning = false
+ private set
fun startForeground(context: Context) {
val intent = Intent(context, IndependentScriptService::class.java).apply {
diff --git a/autojs/src/main/java/com/stardust/autojs/core/accessibility/AccessibilityProxyAccessor.kt b/autojs/src/main/java/com/stardust/autojs/core/accessibility/AccessibilityProxyAccessor.kt
new file mode 100644
index 000000000..2f221a36c
--- /dev/null
+++ b/autojs/src/main/java/com/stardust/autojs/core/accessibility/AccessibilityProxyAccessor.kt
@@ -0,0 +1,155 @@
+package com.stardust.autojs.core.accessibility
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
+import android.os.IBinder
+import android.os.RemoteException
+import android.util.Log
+import com.stardust.app.GlobalAppContext.get
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+
+class AccessibilityProxyAccessor private constructor() {
+
+ companion object {
+ private const val TAG = "AccessibilityProxyAccessor"
+
+ @Volatile
+ private var instance: AccessibilityProxyAccessor? = null
+
+ @JvmStatic
+ fun getInstance(): AccessibilityProxyAccessor {
+ return instance ?: synchronized(this) {
+ instance ?: AccessibilityProxyAccessor().also { instance = it }
+ }
+ }
+ }
+
+ private var service: IAccessibilityProxyService? = null
+ private var isBound = false
+ private var bindingInProgress = false
+ private var serviceReady = CompletableFuture()
+
+ private val connection: ServiceConnection = object : ServiceConnection {
+ override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
+ service = IAccessibilityProxyService.Stub.asInterface(binder)
+ isBound = true
+ serviceReady.complete(true)
+ Log.d(TAG, "无障碍代理AIDL服务连接成功")
+ }
+
+ override fun onServiceDisconnected(name: ComponentName?) {
+ service = null
+ isBound = false
+ serviceReady = CompletableFuture()
+ Log.w(TAG, "无障碍代理AIDL服务断开")
+ }
+
+ override fun onBindingDied(name: ComponentName?) {
+ Log.e(TAG, "无障碍代理绑定失败: $name")
+ }
+
+ override fun onNullBinding(name: ComponentName?) {
+ Log.e(TAG, "无障碍代理返回null绑定: $name")
+ }
+ }
+
+ private fun ensureBound() {
+ if (!isBound && !bindingInProgress) {
+ bindingInProgress = true
+ try {
+ bindService()
+ } catch (e: Exception) {
+ Log.e(TAG, "绑定无障碍代理服务失败", e)
+ } finally {
+ bindingInProgress = false
+ }
+ }
+ }
+
+ private fun waitForServiceReady(timeoutSeconds: Long = 3): Boolean {
+ ensureBound()
+
+ return try {
+ if (!serviceReady.isDone) {
+ serviceReady.get(timeoutSeconds, TimeUnit.SECONDS)
+ }
+ service != null && isBound
+ } catch (e: Exception) {
+ Log.e(TAG, "等待无障碍代理服务就绪超时", e)
+ false
+ }
+ }
+
+ private fun bindService() {
+ try {
+ val context = get()
+
+ val intent = Intent()
+ intent.setComponent(
+ ComponentName(
+ context,
+ "org.autojs.autojs.tool.AccessibilityProxyServiceImpl"
+ )
+ )
+
+ context.startService(intent)
+
+ val bound = context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
+ Log.d(TAG, "无障碍代理服务绑定结果: $bound")
+
+ if (!bound) {
+ context.startForegroundService(intent)
+ val retryBound = context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
+ Log.d(TAG, "重试绑定无障碍代理服务结果: $retryBound")
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "绑定无障碍代理服务异常", e)
+ }
+ }
+
+ val isEnabled: Boolean
+ get() {
+ if (!waitForServiceReady()) return false
+ return try {
+ service?.isEnabled() ?: false
+ } catch (e: RemoteException) {
+ Log.e(TAG, "检查无障碍服务状态失败", e)
+ false
+ }
+ }
+
+ fun ensureEnabled(): Boolean {
+ if (!waitForServiceReady()) return false
+ return try {
+ service?.ensureEnabled() ?: false
+ } catch (e: RemoteException) {
+ Log.e(TAG, "启用无障碍服务失败", e)
+ false
+ }
+ }
+
+ fun disable(): Boolean {
+ if (!waitForServiceReady()) return false
+ return try {
+ service?.disable() ?: false
+ } catch (e: RemoteException) {
+ Log.e(TAG, "禁用无障碍服务失败", e)
+ false
+ }
+ }
+
+ fun destroy() {
+ if (isBound) {
+ try {
+ get().unbindService(connection)
+ isBound = false
+ Log.d(TAG, "无障碍代理服务解绑")
+ } catch (e: Exception) {
+ Log.e(TAG, "解绑无障碍代理服务失败", e)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/autojs/src/main/java/com/stardust/autojs/core/accessibility/AccessibilityProxyService.kt b/autojs/src/main/java/com/stardust/autojs/core/accessibility/AccessibilityProxyService.kt
new file mode 100644
index 000000000..029933a30
--- /dev/null
+++ b/autojs/src/main/java/com/stardust/autojs/core/accessibility/AccessibilityProxyService.kt
@@ -0,0 +1,31 @@
+package com.stardust.autojs.core.accessibility
+
+import android.app.Service
+import android.content.Intent
+import android.os.IBinder
+import android.util.Log
+import com.stardust.view.accessibility.AccessibilityService
+
+
+class AccessibilityProxyService : Service() {
+
+ private val binder = object : IAccessibilityProxyService.Stub() {
+ override fun isEnabled(): Boolean {
+ return AccessibilityService.instance != null
+ }
+
+ override fun ensureEnabled(): Boolean {
+ if (AccessibilityService.instance != null) return true
+
+ val result = AccessibilityServiceTool.enableAccessibilityServiceByRootAndWaitFor(2000)
+ Log.d("AccessibilityService", "代理service, result: $result, instance: ${AccessibilityService.instance}");
+ return AccessibilityService.instance != null
+ }
+
+ override fun disable(): Boolean {
+ return AccessibilityService.disable()
+ }
+ }
+
+ override fun onBind(intent: Intent?): IBinder = binder
+}
\ No newline at end of file
diff --git a/autojs/src/main/java/com/stardust/autojs/core/accessibility/AccessibilityServiceTool.java b/autojs/src/main/java/com/stardust/autojs/core/accessibility/AccessibilityServiceTool.java
new file mode 100644
index 000000000..56d37f3d6
--- /dev/null
+++ b/autojs/src/main/java/com/stardust/autojs/core/accessibility/AccessibilityServiceTool.java
@@ -0,0 +1,82 @@
+package com.stardust.autojs.core.accessibility;
+
+import static com.stardust.app.GlobalAppContext.get;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.stardust.app.GlobalAppContext;
+import com.stardust.autojs.R;
+import com.stardust.autojs.core.util.ProcessShell;
+
+import java.util.Locale;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Created by Stardust on 2017/1/26.
+ */
+
+public class AccessibilityServiceTool {
+
+ private static final Class sAccessibilityServiceClass = AccessibilityService.class;
+
+ private static final String cmd = "enabled=$(settings get secure enabled_accessibility_services)\n" +
+ "pkg=%s\n" +
+ "if [[ $enabled == *$pkg* ]]\n" +
+ "then\n" +
+ "echo already_enabled\n" +
+ "else\n" +
+ "enabled=$pkg:$enabled\n" +
+ "settings put secure enabled_accessibility_services $enabled\n" +
+ "fi\n" +
+ "settings put secure accessibility_enabled 1";
+
+ public static boolean enableAccessibilityServiceByRoot(Class extends android.accessibilityservice.AccessibilityService> accessibilityService) {
+ String serviceName = get().getPackageName() + "/" + accessibilityService.getName();
+ try {
+ return TextUtils.isEmpty(ProcessShell.execCommand(String.format(Locale.getDefault(), cmd, serviceName), true).error);
+ } catch (Exception e) {
+ String msg = e.getMessage();
+ if (msg != null && (msg.contains("error=13") || msg.contains("Permission denied"))) {
+ Log.e("AccessibilityService", "su命令无权限 (error=13)");
+ Context context = GlobalAppContext.get();
+ GlobalAppContext.toast(context.getString(R.string.text_root_permission_incomplete) + context.getString(R.string.app_name));
+ return false;
+ }
+ return false;
+ }
+ }
+
+ public static boolean enableAccessibilityServiceByRootAndWaitFor(long timeOut) {
+
+ final AtomicBoolean shouldWait = new AtomicBoolean(true);
+
+ // 1. 在后台线程启动等待
+ CompletableFuture waitFuture = CompletableFuture.supplyAsync(() -> {
+ if (shouldWait.get()) {
+ return AccessibilityService.Companion.waitForEnabled(timeOut);
+ }
+ return false;
+ });
+
+ // 2. 执行root命令
+ boolean rootSuccess = enableAccessibilityServiceByRoot(sAccessibilityServiceClass);
+
+ // 3. 如果root失败,停止等待
+ if (!rootSuccess) {
+ shouldWait.set(false);
+ // 中断等待线程
+ waitFuture.complete(false);
+ return false;
+ }
+ // 4. 等待结果
+ try {
+ return waitFuture.get(timeOut, TimeUnit.MILLISECONDS);
+ } catch (Exception e) {
+ return false;
+ }
+ }
+}
diff --git a/autojs/src/main/java/com/stardust/autojs/core/floaty/RawWindow.kt b/autojs/src/main/java/com/stardust/autojs/core/floaty/RawWindow.kt
index 08dec7554..104362250 100644
--- a/autojs/src/main/java/com/stardust/autojs/core/floaty/RawWindow.kt
+++ b/autojs/src/main/java/com/stardust/autojs/core/floaty/RawWindow.kt
@@ -88,4 +88,18 @@ class RawWindow(rawFloaty: RawFloaty, context: Context) : FloatyWindow() {
}
updateWindowLayoutParams(windowLayoutParams)
}
+
+ /**
+ * 设置是否可以覆盖状态栏
+ * @param cover true: 覆盖状态栏, false: 不覆盖
+ */
+ fun setCoverStatusBar(cover: Boolean) {
+ val params = windowLayoutParams
+ if (cover) {
+ params.flags = params.flags or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+ } else {
+ params.flags = params.flags and WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN.inv()
+ }
+ updateWindowLayoutParams(params)
+ }
}
\ No newline at end of file
diff --git a/autojs/src/main/java/com/stardust/autojs/core/image/capture/CaptureForegroundService.kt b/autojs/src/main/java/com/stardust/autojs/core/image/capture/CaptureForegroundService.kt
index 8bbfa56de..4e274293c 100644
--- a/autojs/src/main/java/com/stardust/autojs/core/image/capture/CaptureForegroundService.kt
+++ b/autojs/src/main/java/com/stardust/autojs/core/image/capture/CaptureForegroundService.kt
@@ -16,34 +16,29 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import com.stardust.app.service.AbstractAutoService
import com.stardust.autojs.R
+import java.lang.ref.WeakReference
/**
* Created by TonyJiangWJ(https://github.com/TonyJiangWJ).
* From [TonyJiangWJ/Auto.js](https://github.com/TonyJiangWJ/Auto.js)
*/
class CaptureForegroundService : AbstractAutoService() {
- val callback = object : MediaProjection.Callback() {
+
+ private class ServiceCallback(service: CaptureForegroundService) : MediaProjection.Callback() {
+ private val serviceRef = WeakReference(service)
+
override fun onStop() {
- stopServiceInternal()
+ serviceRef.get()?.stopServiceInternal()
}
}
- override fun onBind(intent: Intent?): IBinder {
- return object : Binder() {}
- }
+ private val callback = ServiceCallback(this)
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- val action = intent?.action
- when (action) {
- STOP -> stopServiceInternal()
- REGISTER -> mediaProjection?.registerCallback(callback, Handler(mainLooper))
- }
- super.onStartCommand(intent, flags, startId)
- return START_NOT_STICKY
- }
+ private lateinit var weakService: WeakReference
override fun onCreate() {
super.onCreate()
+ weakService = WeakReference(this)
ServiceCompat.startForeground(
this,
NOTIFICATION_ID,
@@ -54,6 +49,27 @@ class CaptureForegroundService : AbstractAutoService() {
)
}
+ override fun onBind(intent: Intent?): IBinder {
+ return object : Binder() {}
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ val action = intent?.action
+ when (action) {
+ STOP -> stopServiceInternal()
+ REGISTER -> {
+ getMediaProjection()?.unregisterCallback(callback)
+ // 使用弱引用避免 MediaProjection 持有 Service
+ getMediaProjection()?.registerCallback(callback, Handler(mainLooper))
+ }
+ UNREGISTER -> {
+ getMediaProjection()?.unregisterCallback(callback)
+ }
+ }
+ super.onStartCommand(intent, flags, startId)
+ return START_NOT_STICKY
+ }
+
private fun buildNotification(): Notification {
createNotificationChannel()
return NotificationCompat.Builder(this, CHANNEL_ID).setContentTitle(NOTIFICATION_TITLE)
@@ -89,27 +105,42 @@ class CaptureForegroundService : AbstractAutoService() {
override fun onDestroy() {
super.onDestroy()
- mediaProjection?.unregisterCallback(callback)
- mediaProjection?.stop()
+ // 取消注册回调
+ getMediaProjection()?.unregisterCallback(callback)
+ // 停止并清除 MediaProjection
+ clearMediaProjection()
removeNotification()
stopForeground(STOP_FOREGROUND_REMOVE)
}
companion object {
- private var mediaProjection: MediaProjection? = null
- private const val STOP = "STOP_SERVICE"
- private const val REGISTER = "REGISTER_CALLBACK"
- private const val NOTIFICATION_ID = 26
- private val CHANNEL_ID = CaptureForegroundService::class.java.name + ".foreground"
- private const val NOTIFICATION_TITLE = "截图服务运行中"
+ private var mediaProjectionRef: WeakReference? = null
+ private fun getMediaProjection(): MediaProjection? {
+ return mediaProjectionRef?.get()
+ }
+
+ private fun clearMediaProjection() {
+ mediaProjectionRef?.get()?.stop()
+ mediaProjectionRef = null
+ }
fun setMediaProjection(context: Context, media: MediaProjection) {
- mediaProjection = media
+ // 清理旧的引用
+ clearMediaProjection()
+ // 使用弱引用存储
+ mediaProjectionRef = WeakReference(media)
val intent = Intent(context, CaptureForegroundService::class.java).apply {
action = REGISTER
}
context.startForegroundService(intent)
}
+
+ private const val STOP = "STOP_SERVICE"
+ private const val REGISTER = "REGISTER_CALLBACK"
+ private const val UNREGISTER = "UNREGISTER_CALLBACK"
+ private const val NOTIFICATION_ID = 26
+ private val CHANNEL_ID = CaptureForegroundService::class.java.name + ".foreground"
+ private const val NOTIFICATION_TITLE = "截图服务运行中"
}
}
\ No newline at end of file
diff --git a/autojs/src/main/java/com/stardust/autojs/core/image/capture/ScreenCaptureManager.kt b/autojs/src/main/java/com/stardust/autojs/core/image/capture/ScreenCaptureManager.kt
index 56b398e68..1dca8da3c 100644
--- a/autojs/src/main/java/com/stardust/autojs/core/image/capture/ScreenCaptureManager.kt
+++ b/autojs/src/main/java/com/stardust/autojs/core/image/capture/ScreenCaptureManager.kt
@@ -8,15 +8,26 @@ import android.content.ServiceConnection
import android.media.projection.MediaProjection
import android.media.projection.MediaProjectionManager
import android.os.IBinder
+import android.util.Log
import androidx.activity.result.contract.ActivityResultContract
import com.github.aiselp.autox.activity.TransparentActivity
+import com.stardust.autojs.core.image.capture.ScreenCaptureRequester.Callback
+import com.stardust.autojs.core.util.ScriptPromiseAdapter
import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import java.lang.ref.WeakReference
import java.util.concurrent.CancellationException
class ScreenCaptureManager : ScreenCaptureRequester {
@Volatile
override var screenCapture: ScreenCapturer? = null
private var mediaProjection: MediaProjection? = null
+ private var currentCoroutineScope: CoroutineScope? = null
+ private var currentConnection: ServiceConnection? = null
override suspend fun requestScreenCapture(context: Context, orientation: Int) {
if (screenCapture?.available == true) {
@@ -38,6 +49,10 @@ class ScreenCaptureManager : ScreenCaptureRequester {
}
// 使用服务绑定确保服务就绪
+ setupScreenCapture(result, orientation, context)
+ }
+
+ private suspend fun setupScreenCapture(result: Intent, orientation: Int, context: Context) {
val serviceConnected = CompletableDeferred()
val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
@@ -60,16 +75,84 @@ class ScreenCaptureManager : ScreenCaptureRequester {
}
}
- // 绑定服务并等待连接
- context.startService(Intent(context, CaptureForegroundService::class.java))
+ // 先绑定服务再启动(避免延迟)
+ val serviceIntent = Intent(context, CaptureForegroundService::class.java)
context.bindService(
- Intent(context, CaptureForegroundService::class.java),
+ serviceIntent,
connection,
Context.BIND_AUTO_CREATE
)
+
+ // 绑定后立即启动服务
+ context.startForegroundService(serviceIntent)
+
+ delay(50) // 短暂等待服务启动
+
serviceConnected.await()
}
+ override fun requestScreenCaptureLegacy(context: Context, orientation: Int): ScriptPromiseAdapter {
+ val promiseAdapter = ScriptPromiseAdapter()
+ var retryCount = 0
+ val maxRetry = 3
+
+ fun doRequest() {
+ if (screenCapture?.isValid() == true) {
+ screenCapture?.setOrientation(orientation, context)
+ promiseAdapter.resolve(true)
+ return
+ }
+
+ recycle()
+
+ val weakManager = WeakReference(this)
+ val callback = object : Callback {
+ override fun onRequestResult(result: Int, data: Intent?) {
+ val manager = weakManager.get()
+ if (manager == null) {
+ promiseAdapter.resolve(false)
+ return
+ }
+
+ val scope = CoroutineScope(Dispatchers.Main)
+ manager.currentCoroutineScope = scope
+
+ scope.launch {
+ try {
+ if (result == Activity.RESULT_OK && data != null) {
+ manager.setupScreenCapture(data, orientation, context)
+ promiseAdapter.resolve(true)
+ } else {
+ promiseAdapter.resolve(false)
+ }
+ } catch (e: SecurityException) {
+ if (e.message?.contains("non-current") == true && retryCount < maxRetry) {
+ retryCount++
+ Log.w("SCREEN_LEGACY", "MediaProjection expired, retrying ($retryCount/$maxRetry)")
+ delay(100)
+ doRequest()
+ } else {
+ Log.e("SCREEN_LEGACY", "Failed after $maxRetry retries", e)
+ promiseAdapter.resolve(false)
+ }
+ } catch (e: Exception) {
+ Log.e("SCREEN_LEGACY", "创建失败: ${e.message}")
+ promiseAdapter.resolve(false)
+ } finally {
+ scope.cancel()
+ manager.currentCoroutineScope = null
+ }
+ }
+ }
+ }
+
+ ScreenCaptureRequestActivity.request(context, callback)
+ }
+
+ doRequest()
+ return promiseAdapter
+ }
+
class ScreenCaptureRequester : ActivityResultContract() {
override fun createIntent(context: Context, input: Context): Intent {
return (input.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager).createScreenCaptureIntent()
@@ -82,10 +165,20 @@ class ScreenCaptureManager : ScreenCaptureRequester {
}
}
- override fun recycle() {
- screenCapture?.release()
- screenCapture = null
- mediaProjection?.stop()
- mediaProjection = null
- }
+ override fun recycle() {
+ // 取消协程
+ currentCoroutineScope?.cancel()
+ currentCoroutineScope = null
+
+ // 清理连接
+ currentConnection = null
+
+ // 释放截图器
+ screenCapture?.release()
+ screenCapture = null
+
+ // 停止并释放 MediaProjection
+ mediaProjection?.stop()
+ mediaProjection = null
+ }
}
\ No newline at end of file
diff --git a/autojs/src/main/java/com/stardust/autojs/core/image/capture/ScreenCaptureRequestActivity.kt b/autojs/src/main/java/com/stardust/autojs/core/image/capture/ScreenCaptureRequestActivity.kt
new file mode 100644
index 000000000..6a54a34d4
--- /dev/null
+++ b/autojs/src/main/java/com/stardust/autojs/core/image/capture/ScreenCaptureRequestActivity.kt
@@ -0,0 +1,76 @@
+package com.stardust.autojs.core.image.capture
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.Bundle
+import android.util.Log
+import androidx.annotation.RequiresApi
+import com.stardust.app.OnActivityResultDelegate
+import com.stardust.util.IntentExtras
+
+@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+class ScreenCaptureRequestActivity : Activity() {
+
+ private val mOnActivityResultDelegateMediator = OnActivityResultDelegate.Mediator()
+ private var mScreenCaptureRequester: ScreenCaptureRequester.ActivityScreenCaptureRequester? = null
+ private var mCallback: ScreenCaptureRequester.Callback? = null
+
+ companion object {
+ fun request(context: Context, callback: ScreenCaptureRequester.Callback) {
+ val intent = Intent(context, ScreenCaptureRequestActivity::class.java)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+
+ val extras = IntentExtras.newExtras()
+ .put("callback", callback)
+ extras.putInIntent(intent)
+
+ context.startActivity(intent)
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ try {
+ super.onCreate(savedInstanceState)
+
+ val extras = IntentExtras.fromIntentAndRelease(intent)
+ if (extras == null) {
+ finish()
+ return
+ }
+
+ val callback = extras.get("callback")
+ if (callback == null) {
+ finish()
+ return
+ }
+
+ mCallback = callback
+
+ mScreenCaptureRequester = ScreenCaptureRequester.ActivityScreenCaptureRequester(
+ mOnActivityResultDelegateMediator, this
+ )
+ } catch (e: Exception) {
+ Log.e("SCREEN_LEGACY", "创建失败: ${e.message}")
+ }
+ mScreenCaptureRequester?.setOnActivityResultCallback(object : ScreenCaptureRequester.Callback {
+ override fun onRequestResult(result: Int, data: Intent?) {
+ mCallback?.onRequestResult(result, data)
+ }
+ })
+ mScreenCaptureRequester?.requestLegacy()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ mCallback = null
+ mScreenCaptureRequester?.cancel()
+ mScreenCaptureRequester = null
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ mOnActivityResultDelegateMediator.onActivityResult(requestCode, resultCode, data)
+ finish()
+ }
+}
\ No newline at end of file
diff --git a/autojs/src/main/java/com/stardust/autojs/core/image/capture/ScreenCaptureRequester.kt b/autojs/src/main/java/com/stardust/autojs/core/image/capture/ScreenCaptureRequester.kt
index c46dac36d..3e06b1dd8 100644
--- a/autojs/src/main/java/com/stardust/autojs/core/image/capture/ScreenCaptureRequester.kt
+++ b/autojs/src/main/java/com/stardust/autojs/core/image/capture/ScreenCaptureRequester.kt
@@ -5,6 +5,7 @@ import android.content.Context
import android.content.Intent
import android.media.projection.MediaProjectionManager
import com.stardust.app.OnActivityResultDelegate
+import com.stardust.autojs.core.util.ScriptPromiseAdapter
import kotlinx.coroutines.CompletableDeferred
import kotlin.coroutines.cancellation.CancellationException
@@ -14,15 +15,18 @@ import kotlin.coroutines.cancellation.CancellationException
interface ScreenCaptureRequester {
var screenCapture: ScreenCapturer?
suspend fun requestScreenCapture(context: Context, orientation: Int)
+ fun requestScreenCaptureLegacy(context: Context, orientation: Int): ScriptPromiseAdapter
fun recycle()
-
+ interface Callback {
+ fun onRequestResult(result: Int, data: Intent?)
+ }
class ActivityScreenCaptureRequester(
private val mMediator: OnActivityResultDelegate.Mediator,
private val mActivity: Activity
) : OnActivityResultDelegate {
val result = CompletableDeferred()
-
+ private var mCallback: Callback? = null
init {
mMediator.addDelegate(REQUEST_CODE_MEDIA_PROJECTION, this)
}
@@ -30,7 +34,9 @@ interface ScreenCaptureRequester {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
mMediator.removeDelegate(this)
- if (resultCode == Activity.RESULT_OK) {
+ if (mCallback != null) {
+ mCallback!!.onRequestResult(resultCode, data)
+ } else if (resultCode == Activity.RESULT_OK) {
result.complete(data!!)
} else {
result.cancel(CancellationException("user cancel"))
@@ -56,6 +62,18 @@ interface ScreenCaptureRequester {
mMediator.removeDelegate(this)
}
+ fun setOnActivityResultCallback(callback: Callback?) {
+ mCallback = callback
+ }
+
+ fun requestLegacy() {
+ mActivity.startActivityForResult(
+ (mActivity.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager)
+ .createScreenCaptureIntent(),
+ REQUEST_CODE_MEDIA_PROJECTION
+ )
+ }
+
companion object {
private const val REQUEST_CODE_MEDIA_PROJECTION = 17777
}
diff --git a/autojs/src/main/java/com/stardust/autojs/core/image/capture/ScreenCapturer.kt b/autojs/src/main/java/com/stardust/autojs/core/image/capture/ScreenCapturer.kt
index 813a67533..f601144e1 100644
--- a/autojs/src/main/java/com/stardust/autojs/core/image/capture/ScreenCapturer.kt
+++ b/autojs/src/main/java/com/stardust/autojs/core/image/capture/ScreenCapturer.kt
@@ -61,6 +61,16 @@ class ScreenCapturer(
mVirtualDisplay = createVirtualDisplay(screenWidth, screenHeight, screenDensity)
}
+ fun isValid(): Boolean {
+ if (!available) return false
+
+ return try {
+ mVirtualDisplay.display?.isValid == true
+ } catch (_: Exception) {
+ false
+ }
+ }
+
private fun createImageReader(width: Int, height: Int): ImageReader {
return ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 3).apply {
setOnImageAvailableListener({
diff --git a/autojs/src/main/java/com/stardust/autojs/core/plugin/DevPluginAccessor.kt b/autojs/src/main/java/com/stardust/autojs/core/plugin/DevPluginAccessor.kt
new file mode 100644
index 000000000..169d42a70
--- /dev/null
+++ b/autojs/src/main/java/com/stardust/autojs/core/plugin/DevPluginAccessor.kt
@@ -0,0 +1,210 @@
+package com.stardust.autojs.core.plugin
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
+import android.os.IBinder
+import android.os.RemoteException
+import android.util.Log
+import com.stardust.app.GlobalAppContext
+import org.autojs.autojs.devplugin.IDevPluginService
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+
+class DevPluginAccessor private constructor() {
+
+ companion object {
+ private const val TAG = "DevPluginAccessor"
+
+ @Volatile
+ private var instance: DevPluginAccessor? = null
+ @JvmStatic
+ fun getInstance(): DevPluginAccessor {
+ return instance ?: synchronized(this) {
+ instance ?: DevPluginAccessor().also { instance = it }
+ }
+ }
+ }
+ private var service: IDevPluginService? = null
+ private var isBound = false
+ private var bindingInProgress = false
+ private var serviceReady = CompletableFuture()
+ private val connection: ServiceConnection = object : ServiceConnection {
+ override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
+ service = IDevPluginService.Stub.asInterface(binder)
+ isBound = true
+ // 标记 Service 就绪
+ serviceReady.complete(true)
+ Log.d(TAG, "AIDL服务连接成功")
+ }
+
+ override fun onServiceDisconnected(name: ComponentName?) {
+ service = null
+ isBound = false
+ serviceReady = CompletableFuture()
+ Log.d(TAG, "AIDL服务断开")
+ }
+
+ override fun onBindingDied(name: ComponentName?) {
+ Log.e(TAG, "!!! 绑定失败: " + name)
+ }
+
+ override fun onNullBinding(name: ComponentName?) {
+ Log.e(TAG, "!!! Service返回null: " + name)
+ }
+ }
+
+ private fun ensureBound() {
+ if (!isBound && !bindingInProgress) {
+ bindingInProgress = true
+ try {
+ bindService()
+ } catch (e: Exception) {
+ Log.e(TAG, "绑定失败", e)
+ } finally {
+ bindingInProgress = false
+ }
+ }
+ }
+
+ private fun waitForServiceReady(timeout: Long = 3): Boolean {
+ ensureBound()
+
+ return try {
+ if (!serviceReady.isDone) {
+ serviceReady.get(timeout, TimeUnit.SECONDS)
+ }
+ service != null && isBound
+ } catch (e: Exception) {
+ false
+ }
+ }
+
+ private fun bindService() {
+ try {
+ val context = GlobalAppContext.get()
+
+ val intent = Intent()
+ intent.setComponent(
+ ComponentName(
+ context,
+ "org.autojs.autojs.devplugin.DevPluginServiceImpl"
+ )
+ )
+ context.startService(intent)
+
+ var bound = context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
+ Log.d("DevPluginAccessor", "Service绑定结果: " + bound)
+
+ // 如果失败,启动前台Service
+ if (!bound) {
+ context.startForegroundService(intent)
+ bound = context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
+ Log.d("DevPluginAccessor", "失败后尝试前台Service绑定: " + bound)
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "绑定服务失败", e)
+ }
+ }
+
+ fun connectToComputer(url: String?) {
+ try {
+ if (!waitForServiceReady()) return
+ service!!.connectToComputer(url)
+ } catch (e: RemoteException) {
+ Log.e(TAG, "AIDL调用失败", e)
+ }
+ }
+
+ fun connectToSavedAddress() {
+ try {
+ if (!waitForServiceReady()) return
+
+ val savedAddress = service!!.getSavedServerAddress()
+
+ if (savedAddress == null || savedAddress.trim { it <= ' ' }.isEmpty()) {
+ Log.w(TAG, "没有保存的地址")
+ }
+ service!!.connectToComputer(savedAddress)
+ } catch (e: RemoteException) {
+ Log.e(TAG, "connectToSavedAddress AIDL调用失败", e)
+ }
+ }
+
+ fun disconnectFromComputer() {
+ try {
+ if (!waitForServiceReady()) return
+ service!!.disconnectFromComputer()
+ } catch (e: RemoteException) {
+ Log.e(TAG, "AIDL调用失败", e)
+ }
+ }
+
+ val isComputerConnected: Boolean
+ get() {
+ try {
+ if (!waitForServiceReady()) return false
+ val connected = service!!.isComputerConnected()
+ return connected
+ } catch (e: RemoteException) {
+ Log.e(TAG, "获取状态失败", e)
+ }
+ return false
+ }
+
+ val savedServerAddress: String?
+ get() {
+ try {
+ if (!waitForServiceReady()) return ""
+ val address = service!!.getSavedServerAddress()
+ return address
+ } catch (e: RemoteException) {
+ Log.e(TAG, "获取地址失败", e)
+ }
+ return ""
+ }
+
+ fun startUSBDebug() {
+ try {
+ if (!waitForServiceReady()) return
+ service!!.startUSBDebug()
+ } catch (e: RemoteException) {
+ Log.e(TAG, "AIDL调用失败", e)
+ }
+ }
+
+ fun stopUSBDebug() {
+ try {
+ if (!waitForServiceReady()) return
+ service!!.stopUSBDebug()
+ } catch (e: RemoteException) {
+ Log.e(TAG, "AIDL调用失败", e)
+ }
+ }
+
+ val isUSBDebugActive: Boolean
+ get() {
+ try {
+ if (!waitForServiceReady()) return false
+ val active = service!!.isUSBDebugActive()
+ return active
+ } catch (e: RemoteException) {
+ Log.e(TAG, "获取状态失败", e)
+ }
+ return false
+ }
+
+ fun destroy() {
+ if (isBound) {
+ try {
+ GlobalAppContext.get().unbindService(connection)
+ isBound = false
+ Log.d(TAG, "服务解绑")
+ } catch (e: Exception) {
+ Log.e(TAG, "解绑失败", e)
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/autojs/src/main/java/com/stardust/autojs/core/plugin/DevPluginWrapper.kt b/autojs/src/main/java/com/stardust/autojs/core/plugin/DevPluginWrapper.kt
new file mode 100644
index 000000000..5caf6d16c
--- /dev/null
+++ b/autojs/src/main/java/com/stardust/autojs/core/plugin/DevPluginWrapper.kt
@@ -0,0 +1,92 @@
+package com.stardust.autojs.core.plugin
+
+import android.content.Context
+import android.util.Log
+import com.stardust.app.GlobalAppContext
+import com.stardust.autojs.annotation.ScriptInterface
+import androidx.core.content.edit
+
+/**
+ * DevPlugin 的包装类,用于在 autojs 模块中访问 app 模块的 DevPluginAccessor
+ */
+class DevPluginWrapper {
+
+ companion object {
+ private const val TAG = "DevPluginWrapper"
+ }
+ // 直接获取 Kotlin 版本的 DevPluginAccessor 单例
+ private val devPluginAccessor = DevPluginAccessor.getInstance()
+
+ @ScriptInterface
+ fun connectToComputer(url: String) {
+ devPluginAccessor.connectToComputer(url)
+ }
+
+ @ScriptInterface
+ fun disconnectFromComputer() {
+ devPluginAccessor.disconnectFromComputer()
+ }
+
+ @ScriptInterface
+ fun startUSBDebug() {
+ devPluginAccessor.startUSBDebug()
+ }
+
+ @ScriptInterface
+ fun stopUSBDebug() {
+ devPluginAccessor.stopUSBDebug()
+ }
+
+ @ScriptInterface
+ fun isComputerConnected(): Boolean {
+ return devPluginAccessor.isComputerConnected
+ }
+
+ @ScriptInterface
+ fun isUSBDebugActive(): Boolean {
+ return devPluginAccessor.isUSBDebugActive
+ }
+
+ @ScriptInterface
+ fun connectToSavedAddress() {
+ return devPluginAccessor.connectToSavedAddress()
+ }
+
+ @ScriptInterface
+ fun getSavedServerAddress(): String {
+ return devPluginAccessor.savedServerAddress ?: ""
+ }
+
+ /**
+ * 直接设置服务器地址到首选项
+ */
+ @ScriptInterface
+ fun setServerAddress(address: String) {
+ try {
+ val pref = GlobalAppContext.get()
+ .getSharedPreferences("pref", Context.MODE_PRIVATE)
+ pref.edit { putString("server_address", address) }
+ } catch (e: IllegalStateException) {
+ Log.e(TAG, "GlobalAppContext 未初始化", e)
+ } catch (e: NullPointerException) {
+ Log.e(TAG, "SharedPreferences 为 null", e)
+ } catch (e: Exception) {
+ Log.e(TAG, "保存服务器地址失败", e)
+ }
+ }
+
+ @ScriptInterface
+ fun clearServerAddress() {
+ try {
+ val pref = GlobalAppContext.get()
+ .getSharedPreferences("pref", Context.MODE_PRIVATE)
+ pref.edit { remove("server_address") }
+ } catch (e: IllegalStateException) {
+ Log.e(TAG, "GlobalAppContext 未初始化", e)
+ } catch (e: NullPointerException) {
+ Log.e(TAG, "SharedPreferences 为 null", e)
+ } catch (e: Exception) {
+ Log.e(TAG, "清除服务器地址失败", e)
+ }
+ }
+}
\ No newline at end of file
diff --git a/autojs/src/main/java/com/stardust/autojs/core/shizuku/ShizukuClient.kt b/autojs/src/main/java/com/stardust/autojs/core/shizuku/ShizukuClient.kt
index 7bffc9215..08dc47b3f 100644
--- a/autojs/src/main/java/com/stardust/autojs/core/shizuku/ShizukuClient.kt
+++ b/autojs/src/main/java/com/stardust/autojs/core/shizuku/ShizukuClient.kt
@@ -154,6 +154,45 @@ class ShizukuClient private constructor() : Shizuku.OnRequestPermissionResultLis
Shizuku.addBinderReceivedListener(client)
client
}
+
+ suspend fun setAppOpsPermission(op: String, allow: Boolean): Boolean {
+ val packageName = GlobalAppContext.get().packageName
+ if (instance.packageName == null) {
+ instance.setupService(packageName).join()
+ }
+ val mode = if (allow) "allow" else "ask"
+
+ return runCatching {
+ if (instance.available && instance.userPermission) {
+ val service = instance.ensureShizukuService()
+ val result = service.runShellCommand(0, "appops set $packageName $op $mode")
+ result.contains("\"code\":0")
+ } else null
+ }.getOrNull() ?: runCatching {
+ Runtime.getRuntime().exec(arrayOf("su", "-c", "appops set $packageName $op $mode")).waitFor() == 0
+ }.getOrDefault(false)
+ }
+
+ suspend fun checkAppOpsPermission(op: String): Boolean {
+ val packageName = GlobalAppContext.get().packageName
+ if (instance.packageName == null) {
+ instance.setupService(packageName).join()
+ }
+
+ return runCatching {
+ if (instance.available && instance.userPermission) {
+ val service = instance.ensureShizukuService()
+ val result = service.runShellCommand(0, "appops get $packageName $op")
+ result.contains("allow")
+ } else null
+ }.getOrNull() ?: runCatching {
+ val process = Runtime.getRuntime().exec(arrayOf("su", "-c", "appops get $packageName $op"))
+ val result = process.inputStream.bufferedReader().readText()
+ process.waitFor()
+ process.destroy()
+ result.contains("allow")
+ }.getOrDefault(false)
+ }
}
diff --git a/autojs/src/main/java/com/stardust/autojs/runtime/ScriptRuntimeV2.kt b/autojs/src/main/java/com/stardust/autojs/runtime/ScriptRuntimeV2.kt
index 81a83ee56..0a56aca4f 100644
--- a/autojs/src/main/java/com/stardust/autojs/runtime/ScriptRuntimeV2.kt
+++ b/autojs/src/main/java/com/stardust/autojs/runtime/ScriptRuntimeV2.kt
@@ -11,6 +11,7 @@ import com.stardust.autojs.core.console.ConsoleImpl
import com.stardust.autojs.core.http.MutableOkHttp
import com.stardust.autojs.core.image.capture.ScreenCaptureRequester
import com.stardust.autojs.core.looper.Loopers
+import com.stardust.autojs.core.plugin.DevPluginWrapper
import com.stardust.autojs.core.util.WeakReferenceKey
import com.stardust.autojs.onnx.OnnxModule
import com.stardust.autojs.rhino.AndroidClassLoader
@@ -61,6 +62,9 @@ class ScriptRuntimeV2(val builder: Builder) : ScriptRuntime(builder) {
@ScriptVariable
val plugins: Plugins = Plugins(uiHandler.context, this)
+ @ScriptVariable
+ val devPlugin = DevPluginWrapper()
+
@ScriptVariable
var zips: SevenZip = SevenZip()
diff --git a/autojs/src/main/java/com/stardust/autojs/runtime/api/Floaty.kt b/autojs/src/main/java/com/stardust/autojs/runtime/api/Floaty.kt
index 097ce284c..1dae5c77c 100644
--- a/autojs/src/main/java/com/stardust/autojs/runtime/api/Floaty.kt
+++ b/autojs/src/main/java/com/stardust/autojs/runtime/api/Floaty.kt
@@ -138,6 +138,10 @@ class Floaty(private val mUiHandler: UiHandler, ui: UI, private val mRuntime: Sc
runWithWindow { mWindow.setTouchable(touchable) }
}
+ fun setCoverStatusBar(cover: Boolean) {
+ runWithWindow { mWindow.setCoverStatusBar(cover) }
+ }
+
private fun runWithWindow(r: Runnable) {
mUiHandler.post(r)
}
diff --git a/autojs/src/main/java/com/stardust/autojs/runtime/api/Images.kt b/autojs/src/main/java/com/stardust/autojs/runtime/api/Images.kt
index e4a16df8d..dca27e6c1 100644
--- a/autojs/src/main/java/com/stardust/autojs/runtime/api/Images.kt
+++ b/autojs/src/main/java/com/stardust/autojs/runtime/api/Images.kt
@@ -19,6 +19,7 @@ import com.stardust.autojs.core.image.capture.ScreenCaptureRequester
import com.stardust.autojs.core.opencv.Mat
import com.stardust.autojs.core.opencv.OpenCVHelper
import com.stardust.autojs.core.ui.inflater.util.Drawables
+import com.stardust.autojs.core.util.ScriptPromiseAdapter
import com.stardust.autojs.runtime.ScriptRuntime
import com.stardust.pio.UncheckedIOException
import com.stardust.util.ScreenMetrics
@@ -64,6 +65,45 @@ class Images(
}
}
+ fun requestScreenCaptureLegacy(orientation: Int): ScriptPromiseAdapter {
+ val outerPromise = ScriptPromiseAdapter()
+ val weakImages = java.lang.ref.WeakReference(this)
+
+ try {
+ val images = weakImages.get() ?: run {
+ outerPromise.resolve(false)
+ return outerPromise
+ }
+
+ val innerPromise = images.mScreenCaptureRequester.requestScreenCaptureLegacy(
+ images.mContext,
+ orientation
+ )
+
+ innerPromise.onResolve(object : ScriptPromiseAdapter.Callback {
+ override fun call(arg: Any?) {
+ outerPromise.resolve(arg)
+ }
+ })
+
+ innerPromise.onReject(object : ScriptPromiseAdapter.Callback {
+ override fun call(arg: Any?) {
+ val img = weakImages.get()
+ img?.mScriptRuntime?.toast(arg as String?)
+ Log.e(Images::class.java.name, "请求截图权限失败: $arg")
+ outerPromise.resolve(false)
+ }
+ })
+ } catch (e: Exception) {
+ val images = weakImages.get()
+ images?.mScriptRuntime?.toast(e.message)
+ Log.e(Images::class.java.name, "请求截图权限失败", e)
+ outerPromise.resolve(false)
+ }
+
+ return outerPromise
+ }
+
fun stopScreenCapturer() {
mScreenCaptureRequester.recycle()
}
@@ -82,12 +122,22 @@ class Images(
checkNotNull(screenCapture) { SecurityException("No screen capture permission") }
val scheduler = AndroidSchedulers.from(mScriptRuntime.loopers.servantLooper)
var disposable: Disposable? = null
+
+ // 使用弱引用包装回调
+ val weakRuntime = java.lang.ref.WeakReference(mScriptRuntime)
+ val weakImages = java.lang.ref.WeakReference(this)
+
disposable = screenCapture.registerAsyncCapture(scheduler, {
try {
+ val images = weakImages.get()
+ if (images == null) {
+ disposable?.dispose()
+ return@registerAsyncCapture
+ }
onNext.accept(it)
} catch (e: Throwable) {
disposable?.dispose()
- mScriptRuntime.exit(e)
+ weakRuntime.get()?.exit(e)
}
}).also {
disposables.add(it)
@@ -190,6 +240,12 @@ class Images(
fun releaseScreenCapturer() {
disposables.forEach { it.dispose() }
+ disposables.clear()
+ try {
+ mScreenCaptureRequester.recycle()
+ } catch (e: Exception) {
+ Log.e(Images::class.java.name, "Error recycling screen capture", e)
+ }
}
@JvmOverloads
diff --git a/autojs/src/main/java/com/stardust/autojs/runtime/api/Paddle.kt b/autojs/src/main/java/com/stardust/autojs/runtime/api/Paddle.kt
index 65140fd79..fa9fe9b33 100644
--- a/autojs/src/main/java/com/stardust/autojs/runtime/api/Paddle.kt
+++ b/autojs/src/main/java/com/stardust/autojs/runtime/api/Paddle.kt
@@ -1,30 +1,89 @@
package com.stardust.autojs.runtime.api
+//import com.equationl.ncnnandroidppocr.bean.ModelType
import android.content.Context
-import com.baidu.paddle.lite.demo.ocr.OcrResult
-import com.baidu.paddle.lite.demo.ocr.Predictor
+import com.equationl.ncnnandroidppocr.OcrConfig
+import com.equationl.ncnnandroidppocr.Predictor
+import com.equationl.ncnnandroidppocr.bean.AutoXResult
+import com.equationl.ncnnandroidppocr.bean.Device
+import com.google.gson.Gson
+import com.google.gson.reflect.TypeToken
import com.stardust.app.GlobalAppContext.get
import com.stardust.autojs.core.image.ImageWrapper
class Paddle {
- private val predictor = Predictor()
- private val availableProcessors = Runtime.getRuntime().availableProcessors()
+ private val predictor: Predictor
+ get() = Predictor.getInstance()
+ //private val availableProcessors = Runtime.getRuntime().availableProcessors()
private fun initOcr(context: Context, cpuThreadNum: Int, useSlim: Boolean) {
predictor.initOcr(context, cpuThreadNum, useSlim)
}
private fun initOcr(context: Context, myModelPath: String): Boolean {
- return predictor.init(context, myModelPath)
+ return predictor.initOcr(context, myModelPath)
+ }
+
+ fun getOcrConfig(): Map {
+ val gson = Gson()
+ val json = gson.toJson(predictor.ocrConfig)
+ val type = object : TypeToken