Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,6 @@ google-services.json

# Android Profiling
*.hprof

# Local operational artifacts
out/
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@
- ✅ **Kiosk 模式**:单应用锁定(Lock Task)、替换默认桌面、禁止安全模式/恢复出厂

> 详细能力清单见 [ACTIONS.md](./ACTIONS.md)
>
> 实机恢复出厂后的完整落地流程、验收脚本和小米 / MIUI 踩坑记录见 [docs/device-owner/README.md](./docs/device-owner/README.md)。

6. **创建 Telegram 机器人**

Expand Down
10 changes: 10 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ android {
}

buildTypes {
debug {
applicationIdSuffix ".local"
versionNameSuffix "-local"
}
ownerDebug {
initWith(debug)
applicationIdSuffix ""
versionNameSuffix "-owner"
matchingFallbacks = ['debug']
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
Expand Down
27 changes: 27 additions & 0 deletions app/src/debug/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<application>
<activity android:name="com.afwsamples.testdpc.policy.locktask.SetupKioskModeActivity">
<intent-filter tools:node="remove">
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<activity-alias
android:name="com.andforce.andclaw.DebugLauncherActivity"
android:enabled="true"
android:exported="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:targetActivity="com.andforce.andclaw.ChatHistoryActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
</application>

</manifest>
12 changes: 11 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="com.android.alarm.permission.SET_ALARM" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
Expand Down Expand Up @@ -50,6 +52,14 @@
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_config" />
</service>
<receiver
android:name="com.andforce.andclaw.AgentDebugReceiver"
android:exported="true">
<intent-filter>
<action android:name="com.andforce.andclaw.action.START_AGENT" />
<action android:name="com.andforce.andclaw.action.STOP_AGENT" />
</intent-filter>
</receiver>
</application>

</manifest>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class ActionTestActivity : AppCompatActivity() {
companion object {
private const val TAG = "ActionTest"
private const val TEST_DOWNLOAD_URL =
"https://raw.githubusercontent.com/nicehash/NiceHashQuickMiner/main/LICENSE"
"https://www.baidu.com/robots.txt"
}

private lateinit var binding: ActivityActionTestBinding
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import android.util.Log
import android.view.Display
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.view.accessibility.AccessibilityWindowInfo
import java.util.concurrent.Executor

@SuppressLint("AccessibilityPolicy")
Expand All @@ -25,9 +26,20 @@ class AgentAccessibilityService : AccessibilityService() {
override fun onServiceConnected() { instance = this }

fun captureScreenHierarchy(): String {
val root = rootInActiveWindow ?: return "Empty Screen"
val sb = StringBuilder()
parseNode(root, sb)
val roots = getWindowRoots()
if (roots.isEmpty()) return "Empty Screen"
roots.forEachIndexed { index, root ->
val window = root.window
sb.append(
"[window:$index" +
",type=${window?.type}" +
",focused=${window?.isFocused}" +
",active=${window?.isActive}" +
",pkg=${root.packageName}]\n"
)
parseNode(root, sb)
}
return sb.toString()
}

Expand All @@ -41,28 +53,28 @@ class AgentAccessibilityService : AccessibilityService() {
for (i in 0 until node.childCount) parseNode(node.getChild(i), sb)
}

fun click(x: Int, y: Int) {
fun click(x: Int, y: Int): Boolean {
val path = Path().apply { moveTo(x.toFloat(), y.toFloat()) }
val gesture = GestureDescription.Builder()
.addStroke(GestureDescription.StrokeDescription(path, 0, 50)).build()
dispatchGesture(gesture, null, null)
return dispatchGesture(gesture, null, null)
}

fun swipe(startX: Int, startY: Int, endX: Int, endY: Int, durationMs: Long = 300) {
fun swipe(startX: Int, startY: Int, endX: Int, endY: Int, durationMs: Long = 300): Boolean {
val path = Path().apply {
moveTo(startX.toFloat(), startY.toFloat())
lineTo(endX.toFloat(), endY.toFloat())
}
val gesture = GestureDescription.Builder()
.addStroke(GestureDescription.StrokeDescription(path, 0, durationMs)).build()
dispatchGesture(gesture, null, null)
return dispatchGesture(gesture, null, null)
}

fun longPress(x: Int, y: Int, durationMs: Long = 1000) {
fun longPress(x: Int, y: Int, durationMs: Long = 1000): Boolean {
val path = Path().apply { moveTo(x.toFloat(), y.toFloat()) }
val gesture = GestureDescription.Builder()
.addStroke(GestureDescription.StrokeDescription(path, 0, durationMs)).build()
dispatchGesture(gesture, null, null)
return dispatchGesture(gesture, null, null)
}

private val browserPackages = setOf(
Expand Down Expand Up @@ -137,6 +149,73 @@ class AgentAccessibilityService : AccessibilityService() {

fun globalAction(action: Int): Boolean = performGlobalAction(action)

fun clickNodeByText(vararg texts: String): Boolean {
val targets = texts
.map { it.trim() }
.filter { it.isNotEmpty() }
if (targets.isEmpty()) return false

for (root in getWindowRoots()) {
val node = findNodeByText(root, targets) ?: continue
if (node.performAction(AccessibilityNodeInfo.ACTION_CLICK)) {
return true
}

val rect = Rect()
node.getBoundsInScreen(rect)
if (!rect.isEmpty) {
return click(rect.centerX(), rect.centerY())
}
}
return false
}

private fun getWindowRoots(): List<AccessibilityNodeInfo> {
val interactiveRoots = windows
?.mapNotNull(AccessibilityWindowInfo::getRoot)
?.filterNotNull()
?: emptyList()
if (interactiveRoots.isNotEmpty()) return interactiveRoots

val activeRoot = rootInActiveWindow
return if (activeRoot != null) listOf(activeRoot) else emptyList()
}

private fun findNodeByText(
node: AccessibilityNodeInfo,
targets: List<String>,
depth: Int = 0
): AccessibilityNodeInfo? {
if (depth > 25) return null

val text = node.text?.toString()?.trim()
val contentDesc = node.contentDescription?.toString()?.trim()
val matches = targets.any { target ->
text.equals(target, ignoreCase = true) || contentDesc.equals(target, ignoreCase = true)
}
if (matches) {
return findClickableAncestor(node) ?: node
}

for (i in 0 until node.childCount) {
val child = node.getChild(i) ?: continue
val result = findNodeByText(child, targets, depth + 1)
if (result != null) return result
}
return null
}

private fun findClickableAncestor(node: AccessibilityNodeInfo?): AccessibilityNodeInfo? {
var current = node
var hops = 0
while (current != null && hops < 8) {
if (current.isClickable) return current
current = current.parent
hops++
}
return null
}

fun captureScreenshot(callback: (Bitmap?) -> Unit) {
takeScreenshot(
Display.DEFAULT_DISPLAY,
Expand Down
Loading