feat: Android 重构 service/engine/ui/frida/antiban、AgentForegroundService、APK产品说明、架构图

Made-with: Cursor
This commit is contained in:
卡若
2026-03-17 10:05:46 +08:00
parent 3321e9d620
commit 3c97e7887f
114 changed files with 9709 additions and 5680 deletions

View File

@@ -11,10 +11,11 @@ android {
applicationId "com.system.cloudservice" applicationId "com.system.cloudservice"
minSdk 24 minSdk 24
targetSdk 34 targetSdk 34
versionCode 3 versionCode 5
versionName "3.0.0" versionName "5.0.0"
buildConfigField "String", "PWA_URL", "\"https://ckbapi.quwanzhi.com\"" buildConfigField "String", "PWA_URL", "\"https://ckbapi.quwanzhi.com\""
buildConfigField "String", "DEFAULT_WS", "\"wss://workphone.quwanzhi.com/ws/device\"" buildConfigField "String", "DEFAULT_WS", "\"wss://workphone.quwanzhi.com/ws/device\""
buildConfigField "String", "AI_API_URL", "\"https://ckbapi.quwanzhi.com\""
} }
buildTypes { buildTypes {
@@ -37,6 +38,11 @@ android {
viewBinding true viewBinding true
buildConfig true buildConfig true
} }
packagingOptions {
pickFirst 'META-INF/INDEX.LIST'
pickFirst 'META-INF/io.netty.versions.properties'
}
} }
dependencies { dependencies {
@@ -45,24 +51,29 @@ dependencies {
implementation 'com.google.android.material:material:1.11.0' implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
// WebSocket // OkHttp (HTTP + WebSocket unified)
implementation 'org.java-websocket:Java-WebSocket:1.5.4'
// HTTP客户端
implementation 'com.squareup.okhttp3:okhttp:4.12.0' implementation 'com.squareup.okhttp3:okhttp:4.12.0'
// JSON解析 // JSON
implementation 'com.google.code.gson:gson:2.10.1' implementation 'com.google.code.gson:gson:2.10.1'
// 协程 // Coroutines + Flow
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
// 二维码扫描 // Room (offline buffer persistence)
implementation 'androidx.room:room-runtime:2.6.1'
implementation 'androidx.room:room-ktx:2.6.1'
// QR code scanning
implementation 'com.journeyapps:zxing-android-embedded:4.3.0' implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
implementation 'com.google.zxing:core:3.5.2' implementation 'com.google.zxing:core:3.5.2'
// 语音识别Android内置无需额外依赖 // Lifecycle
// 悬浮窗
implementation 'androidx.lifecycle:lifecycle-service:2.7.0' implementation 'androidx.lifecycle:lifecycle-service:2.7.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
// Fragment KTX
implementation 'androidx.fragment:fragment-ktx:1.6.2'
} }

View File

@@ -11,14 +11,17 @@
<uses-feature android:name="android.hardware.microphone" android:required="false" /> <uses-feature android:name="android.hardware.microphone" android:required="false" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" /> <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />
<application <application
android:name=".App"
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
@@ -29,7 +32,7 @@
tools:targetApi="34"> tools:targetApi="34">
<activity <activity
android:name=".SetupActivity" android:name=".ui.SetupActivity"
android:exported="true" android:exported="true"
android:launchMode="singleTop"> android:launchMode="singleTop">
<intent-filter> <intent-filter>
@@ -39,17 +42,17 @@
</activity> </activity>
<activity <activity
android:name=".MainActivity" android:name=".ui.MainActivity"
android:exported="false" android:exported="false"
android:launchMode="singleTop" /> android:launchMode="singleTop" />
<service <service
android:name=".AgentService" android:name=".service.AgentForegroundService"
android:exported="false" android:exported="false"
android:foregroundServiceType="dataSync" /> android:foregroundServiceType="dataSync" />
<receiver <receiver
android:name=".BootReceiver" android:name=".service.BootReceiver"
android:enabled="true" android:enabled="true"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
@@ -58,7 +61,7 @@
</receiver> </receiver>
<service <service
android:name=".AgentAccessibilityService" android:name=".service.AgentAccessibilityService"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
android:exported="false"> android:exported="false">
<intent-filter> <intent-filter>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,247 +0,0 @@
package com.system.cloudservice
import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.GestureDescription
import android.graphics.Path
import android.util.Log
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import kotlinx.coroutines.*
/**
* 无障碍服务 - 用于UI自动化操作
*
* 功能:
* - 点击、滑动、输入文字
* - 查找UI元素
* - 获取UI层级
*/
class AgentAccessibilityService : AccessibilityService() {
companion object {
private const val TAG = "AccessibilityService"
private var instance: AgentAccessibilityService? = null
fun getInstance(): AgentAccessibilityService? = instance
fun isEnabled(): Boolean = instance != null
}
private val serviceScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
override fun onServiceConnected() {
super.onServiceConnected()
instance = this
Log.d(TAG, "无障碍服务已启动")
}
override fun onDestroy() {
super.onDestroy()
instance = null
serviceScope.cancel()
Log.d(TAG, "无障碍服务已停止")
}
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
// 可以在这里监听UI变化
}
override fun onInterrupt() {
Log.w(TAG, "无障碍服务被中断")
}
// ========== UI操作 ==========
/**
* 点击坐标
*/
fun click(x: Int, y: Int, callback: ((Boolean) -> Unit)? = null) {
val path = Path().apply {
moveTo(x.toFloat(), y.toFloat())
}
val gesture = GestureDescription.Builder()
.addStroke(GestureDescription.StrokeDescription(path, 0, 100))
.build()
dispatchGesture(gesture, object : GestureResultCallback() {
override fun onCompleted(gestureDescription: GestureDescription?) {
Log.d(TAG, "点击成功: ($x, $y)")
callback?.invoke(true)
}
override fun onCancelled(gestureDescription: GestureDescription?) {
Log.w(TAG, "点击取消: ($x, $y)")
callback?.invoke(false)
}
}, null)
}
/**
* 滑动
*/
fun swipe(x1: Int, y1: Int, x2: Int, y2: Int, duration: Long = 300, callback: ((Boolean) -> Unit)? = null) {
val path = Path().apply {
moveTo(x1.toFloat(), y1.toFloat())
lineTo(x2.toFloat(), y2.toFloat())
}
val gesture = GestureDescription.Builder()
.addStroke(GestureDescription.StrokeDescription(path, 0, duration))
.build()
dispatchGesture(gesture, object : GestureResultCallback() {
override fun onCompleted(gestureDescription: GestureDescription?) {
Log.d(TAG, "滑动成功: ($x1, $y1) -> ($x2, $y2)")
callback?.invoke(true)
}
override fun onCancelled(gestureDescription: GestureDescription?) {
Log.w(TAG, "滑动取消")
callback?.invoke(false)
}
}, null)
}
/**
* 点击文字
*/
fun clickText(text: String, callback: ((Boolean) -> Unit)? = null) {
serviceScope.launch {
val node = findNodeByText(text)
if (node != null) {
val bounds = android.graphics.Rect()
node.getBoundsInScreen(bounds)
click(bounds.centerX(), bounds.centerY(), callback)
node.recycle()
} else {
Log.w(TAG, "未找到文字: $text")
callback?.invoke(false)
}
}
}
/**
* 输入文字
*/
fun inputText(text: String, callback: ((Boolean) -> Unit)? = null) {
serviceScope.launch {
val rootNode = rootInActiveWindow
if (rootNode != null) {
// 查找输入框通常是EditText
val inputNode = findInputNode(rootNode)
if (inputNode != null) {
// 聚焦
inputNode.performAction(AccessibilityNodeInfo.ACTION_FOCUS)
Thread.sleep(200)
// 输入文字
val arguments = android.os.Bundle().apply {
putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text)
}
val success = inputNode.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments)
inputNode.recycle()
rootNode.recycle()
callback?.invoke(success)
} else {
Log.w(TAG, "未找到输入框")
callback?.invoke(false)
}
} else {
callback?.invoke(false)
}
}
}
/**
* 返回键
*/
fun back(callback: ((Boolean) -> Unit)? = null) {
performGlobalAction(GLOBAL_ACTION_BACK)
callback?.invoke(true)
}
/**
* Home键
*/
fun home(callback: ((Boolean) -> Unit)? = null) {
performGlobalAction(GLOBAL_ACTION_HOME)
callback?.invoke(true)
}
/**
* 最近任务
*/
fun recent(callback: ((Boolean) -> Unit)? = null) {
performGlobalAction(GLOBAL_ACTION_RECENTS)
callback?.invoke(true)
}
// ========== 查找元素 ==========
/**
* 通过文字查找节点
*/
private fun findNodeByText(text: String): AccessibilityNodeInfo? {
val rootNode = rootInActiveWindow ?: return null
val nodes = rootNode.findAccessibilityNodeInfosByText(text)
val result = if (nodes.isNotEmpty()) nodes[0] else null
rootNode.recycle()
return result
}
/**
* 查找输入框
*/
private fun findInputNode(root: AccessibilityNodeInfo): AccessibilityNodeInfo? {
if (root.className?.contains("EditText") == true) {
return root
}
for (i in 0 until root.childCount) {
val child = root.getChild(i) ?: continue
val result = findInputNode(child)
if (result != null) {
return result
}
child.recycle()
}
return null
}
/**
* 获取UI层级用于调试
*/
fun getUITree(): String {
val rootNode = rootInActiveWindow ?: return ""
val tree = StringBuilder()
dumpNode(rootNode, tree, 0)
rootNode.recycle()
return tree.toString()
}
private fun dumpNode(node: AccessibilityNodeInfo, tree: StringBuilder, depth: Int) {
val indent = " ".repeat(depth)
val className = node.className?.toString() ?: "Unknown"
val text = node.text?.toString() ?: ""
val desc = node.contentDescription?.toString() ?: ""
tree.append("$indent$className")
if (text.isNotEmpty()) tree.append(" text=\"$text\"")
if (desc.isNotEmpty()) tree.append(" desc=\"$desc\"")
tree.append("\n")
for (i in 0 until node.childCount) {
val child = node.getChild(i)
if (child != null) {
dumpNode(child, tree, depth + 1)
child.recycle()
}
}
}
}

View File

@@ -1,745 +0,0 @@
package com.system.cloudservice
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.IBinder
import android.util.Log
import android.content.Context.RECEIVER_NOT_EXPORTED
import androidx.core.app.NotificationCompat
import com.google.gson.Gson
import kotlinx.coroutines.*
import org.java_websocket.client.WebSocketClient
import org.java_websocket.handshake.ServerHandshake
import java.net.URI
/**
* Agent后台服务
*
* 保持与远程服务器的WebSocket连接
* 接收并执行控制命令
*/
class AgentService : Service() {
companion object {
const val TAG = "AgentService"
const val CHANNEL_ID = "sync_channel"
const val NOTIFICATION_ID = 1
const val ACTION_START = "com.system.cloudservice.START"
const val ACTION_STOP = "com.system.cloudservice.STOP"
const val EXTRA_SERVER_URL = "server_url"
const val EXTRA_PROJECT_ID = "project_id"
const val EXTRA_DEVICE_ID = "device_id"
var isRunning = false
private set
var isSocketConnected = false
private set
}
private var webSocketClient: WebSocketClient? = null
private var serverUrl: String = ""
private var projectId: String = ""
private var deviceId: String = ""
private val gson = Gson()
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
// 心跳定时器
private var heartbeatJob: Job? = null
private var reconnectJob: Job? = null
// 性能监控定时器
private var performanceMonitorJob: Job? = null
private var heartbeatIntervalMs: Long = 30_000L
private var lastPongAtMs: Long = 0L
private var manualStop: Boolean = false
override fun onBind(intent: Intent?): IBinder? = null
private val voiceCommandReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == "com.system.cloudservice.VOICE_COMMAND") {
val text = intent.getStringExtra("text") ?: return
Log.d(TAG, "收到语音命令: $text")
// 在后台线程执行
serviceScope.launch {
val result = LocalAI.executeVoiceCommand(this@AgentService, text)
Log.d(TAG, "执行结果: $result")
}
}
}
}
override fun onCreate() {
super.onCreate()
createNotificationChannel()
// 注册语音命令接收器
val filter = IntentFilter("com.system.cloudservice.VOICE_COMMAND")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(voiceCommandReceiver, filter, RECEIVER_NOT_EXPORTED)
} else {
@Suppress("DEPRECATION")
registerReceiver(voiceCommandReceiver, filter)
}
}
override fun onDestroy() {
super.onDestroy()
try {
unregisterReceiver(voiceCommandReceiver)
} catch (e: Exception) {
// 忽略未注册错误
}
disconnect()
serviceScope.cancel()
}
// 删除下面重复的onDestroy方法
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
ACTION_START -> {
serverUrl = intent.getStringExtra(EXTRA_SERVER_URL) ?: ""
projectId = intent.getStringExtra(EXTRA_PROJECT_ID) ?: ""
deviceId = intent.getStringExtra(EXTRA_DEVICE_ID)?.trim().orEmpty()
if (deviceId.isEmpty()) {
deviceId = DeviceIdHelper.getOrCreateDeviceId(this)
}
manualStop = false
isRunning = true
isSocketConnected = false
startForeground(NOTIFICATION_ID, createNotification("正在连接..."))
connectWebSocket()
}
ACTION_STOP -> {
manualStop = true
disconnect()
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
}
}
return START_STICKY
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
"云同步",
NotificationManager.IMPORTANCE_LOW
).apply {
description = "数据同步服务"
}
val notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.createNotificationChannel(channel)
}
}
private fun createNotification(status: String): Notification {
val pendingIntent = PendingIntent.getActivity(
this,
0,
Intent(this, MainActivity::class.java),
PendingIntent.FLAG_IMMUTABLE
)
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("云同步服务")
.setContentText(sanitizeNotificationStatus(status))
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentIntent(pendingIntent)
.setOngoing(true)
.setOnlyAlertOnce(true)
.setSilent(true)
.setPriority(NotificationCompat.PRIORITY_MIN)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.build()
}
private fun updateNotification(status: String) {
val notification = createNotification(status)
val notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.notify(NOTIFICATION_ID, notification)
}
/**
* 通知栏只保留通用状态避免把设备ID或后端错误详情直接暴露到手机界面。
*/
private fun sanitizeNotificationStatus(status: String): String {
val normalized = status.trim()
if (normalized.isEmpty()) {
return "同步服务运行中"
}
return when {
normalized.contains("正在连接") -> "正在同步数据"
normalized.contains("已连接") -> "同步服务已就绪"
normalized.contains("重连") -> "正在恢复同步"
normalized.contains("已断开") -> "同步已暂停"
normalized.contains("连接异常") || normalized.contains("连接失败") -> "同步异常"
normalized.contains("删除") || normalized.contains("停用") -> "同步服务运行中"
else -> "同步服务运行中"
}
}
private fun connectWebSocket() {
serviceScope.launch {
try {
val uri = URI("$serverUrl/$deviceId")
Log.d(TAG, "连接: $uri")
webSocketClient = object : WebSocketClient(uri) {
override fun onOpen(handshakedata: ServerHandshake?) {
Log.d(TAG, "WebSocket连接成功")
isSocketConnected = true
lastPongAtMs = System.currentTimeMillis()
updateNotification("已连接 - 项目: $projectId")
// 发送注册消息
sendRegister()
// 启动心跳
startHeartbeat()
// 启动性能监控
startPerformanceMonitoring()
}
override fun onMessage(message: String?) {
Log.d(TAG, "收到消息: $message")
message?.let { handleMessage(it) }
}
override fun onClose(code: Int, reason: String?, remote: Boolean) {
Log.d(TAG, "WebSocket关闭: $reason")
isSocketConnected = false
if (isRunning) {
updateNotification("重连中...")
} else {
updateNotification("已断开")
}
// 自动重连(非手动停止)
if (!manualStop) {
scheduleReconnect()
}
}
override fun onError(ex: Exception?) {
Log.e(TAG, "WebSocket错误", ex)
isSocketConnected = false
updateNotification("连接异常,重试中...")
if (!manualStop) {
scheduleReconnect()
}
}
}
webSocketClient?.connect()
} catch (e: Exception) {
Log.e(TAG, "连接失败", e)
updateNotification("连接失败: ${e.message}")
}
}
}
private fun sendRegister() {
val androidId = DeviceIdHelper.getAndroidId(this)
val aochuangDeviceId = DeviceIdHelper.getAochuangCompatibleId(this)
val registerMsg = mapOf(
"type" to "register",
"device_id" to deviceId,
"project_id" to projectId,
"platform" to "android",
"model" to Build.MODEL,
"sdk_version" to Build.VERSION.SDK_INT,
"app_version" to getAppVersion(),
"heartbeat_interval_seconds" to (heartbeatIntervalMs / 1000),
"device_profile" to mapOf(
"aochuang_device_id" to aochuangDeviceId,
"android_id" to androidId,
"serial" to DeviceIdHelper.getSerial()
)
)
webSocketClient?.send(gson.toJson(registerMsg))
}
private fun getAppVersion(): String {
return try {
packageManager.getPackageInfo(packageName, 0).versionName ?: "unknown"
} catch (_: Exception) {
"unknown"
}
}
private fun startHeartbeat() {
heartbeatJob?.cancel()
heartbeatJob = serviceScope.launch {
while (isActive) {
delay(heartbeatIntervalMs)
try {
val status = collectDeviceStatus()
val heartbeat = mapOf(
"type" to "heartbeat",
"device_id" to deviceId,
"timestamp" to System.currentTimeMillis(),
"status" to status
)
webSocketClient?.send(gson.toJson(heartbeat))
val staleThresholdMs = heartbeatIntervalMs * 3
if (lastPongAtMs > 0 && (System.currentTimeMillis() - lastPongAtMs) > staleThresholdMs) {
Log.w(TAG, "心跳超时,准备重连")
webSocketClient?.close()
break
}
} catch (e: Exception) {
Log.e(TAG, "心跳发送失败", e)
}
}
}
}
private fun collectDeviceStatus(): Map<String, Any?> {
val bm = getSystemService(Context.BATTERY_SERVICE) as? android.os.BatteryManager
val batteryLevel = bm?.getIntProperty(android.os.BatteryManager.BATTERY_PROPERTY_CAPACITY) ?: -1
val isCharging = bm?.isCharging ?: false
val am = getSystemService(Context.ACTIVITY_SERVICE) as? android.app.ActivityManager
val memInfo = android.app.ActivityManager.MemoryInfo()
am?.getMemoryInfo(memInfo)
val totalMem = memInfo.totalMem.toDouble()
val availMem = memInfo.availMem.toDouble()
val memUsagePct = if (totalMem > 0) ((totalMem - availMem) / totalMem * 100) else 0.0
val stat = android.os.StatFs(android.os.Environment.getDataDirectory().path)
val storageFree = (stat.availableBlocksLong * stat.blockSizeLong) / (1024 * 1024)
val pm = getSystemService(Context.POWER_SERVICE) as? android.os.PowerManager
val screenOn = pm?.isInteractive ?: false
val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as? android.net.ConnectivityManager
val networkType = try {
val nc = cm?.getNetworkCapabilities(cm.activeNetwork)
when {
nc == null -> "none"
nc.hasTransport(android.net.NetworkCapabilities.TRANSPORT_WIFI) -> "wifi"
nc.hasTransport(android.net.NetworkCapabilities.TRANSPORT_CELLULAR) -> "cellular"
nc.hasTransport(android.net.NetworkCapabilities.TRANSPORT_ETHERNET) -> "ethernet"
else -> "other"
}
} catch (_: Exception) { "unknown" }
val currentApp = try {
val usm = getSystemService(Context.USAGE_STATS_SERVICE) as? android.app.usage.UsageStatsManager
val end = System.currentTimeMillis()
val stats = usm?.queryUsageStats(android.app.usage.UsageStatsManager.INTERVAL_DAILY, end - 60_000, end)
stats?.maxByOrNull { it.lastTimeUsed }?.packageName ?: ""
} catch (_: Exception) { "" }
val wechatRunning = try {
am?.runningAppProcesses?.any { it.processName == "com.tencent.mm" } ?: false
} catch (_: Exception) { false }
val uptimeSec = (android.os.SystemClock.elapsedRealtime() / 1000).toInt()
return mapOf(
"battery_level" to batteryLevel,
"battery_charging" to isCharging,
"memory_usage_pct" to Math.round(memUsagePct * 10.0) / 10.0,
"storage_free_mb" to storageFree.toInt(),
"screen_on" to screenOn,
"network_type" to networkType,
"current_app" to currentApp,
"wechat_running" to wechatRunning,
"uptime_sec" to uptimeSec,
)
}
private fun handleMessage(message: String) {
try {
val data = gson.fromJson(message, Map::class.java)
val type = data["type"] as? String
when (type) {
"execute" -> handleExecute(data)
"ping" -> sendPong()
"pong" -> {
lastPongAtMs = System.currentTimeMillis()
val pendingTasks = (data["pending_tasks"] as? Number)?.toInt() ?: 0
if (pendingTasks > 0) {
Log.i(TAG, "服务端有 $pendingTasks 个待下发任务")
}
}
"registered" -> {
lastPongAtMs = System.currentTimeMillis()
updateNotification("已连接 - 设备: $deviceId")
}
"config" -> handleConfig(data)
}
} catch (e: Exception) {
Log.e(TAG, "处理消息失败", e)
}
}
private fun handleExecute(data: Map<*, *>) {
val commandId = data["command_id"] as? String ?: ""
val action = data["action"] as? String ?: ""
val params = data["params"] as? Map<*, *> ?: emptyMap<String, Any>()
Log.d(TAG, "执行命令: $action, 参数: $params")
// 执行命令
serviceScope.launch {
val result = executeCommand(action, params)
// 返回结果
val response = mapOf(
"type" to "result",
"command_id" to commandId,
"device_id" to deviceId,
"success" to result.first,
"message" to result.second,
"timestamp" to System.currentTimeMillis()
)
webSocketClient?.send(gson.toJson(response))
}
}
private suspend fun executeCommand(action: String, params: Map<*, *>): Pair<Boolean, String> {
return withContext(Dispatchers.Main) {
try {
when (action) {
// === 应用操作 ===
"open_app" -> {
val packageName = params["package"] as? String ?: ""
openApp(packageName)
}
"get_installed_apps" -> {
val apps = getInstalledApps()
Pair(true, apps.joinToString(","))
}
"get_device_info" -> {
Pair(true, getDeviceInfo())
}
// === UI自动化操作 ===
"click" -> {
val x = (params["x"] as? Number)?.toInt() ?: 0
val y = (params["y"] as? Number)?.toInt() ?: 0
// 优先使用Accessibility Service
val accessibilityService = AgentAccessibilityService.getInstance()
if (accessibilityService != null) {
accessibilityService.click(x, y) { success ->
if (!success) {
// 降级到Shell
executeShell("input tap $x $y")
}
}
Pair(true, "已点击 ($x, $y)")
} else {
executeShell("input tap $x $y")
}
}
"swipe" -> {
val x1 = (params["x1"] as? Number)?.toInt() ?: 0
val y1 = (params["y1"] as? Number)?.toInt() ?: 0
val x2 = (params["x2"] as? Number)?.toInt() ?: 0
val y2 = (params["y2"] as? Number)?.toInt() ?: 0
val duration = (params["duration"] as? Number)?.toLong() ?: 300L
// 优先使用Accessibility Service
val accessibilityService = AgentAccessibilityService.getInstance()
if (accessibilityService != null) {
accessibilityService.swipe(x1, y1, x2, y2, duration) { success ->
if (!success) {
executeShell("input swipe $x1 $y1 $x2 $y2 ${duration.toInt()}")
}
}
Pair(true, "已滑动")
} else {
executeShell("input swipe $x1 $y1 $x2 $y2 ${duration.toInt()}")
}
}
"input_text" -> {
val text = params["text"] as? String ?: ""
// 优先使用Accessibility Service
val accessibilityService = AgentAccessibilityService.getInstance()
if (accessibilityService != null) {
accessibilityService.inputText(text) { success ->
if (!success) {
// 降级到Shell
executeShell("am broadcast -a ADB_INPUT_TEXT --es msg '$text'")
}
}
Pair(true, "已输入文字")
} else {
// 使用broadcast方式输入中文
executeShell("am broadcast -a ADB_INPUT_TEXT --es msg '$text'")
}
}
"key_event" -> {
val keycode = params["keycode"] as? String ?: ""
executeShell("input keyevent $keycode")
}
"screenshot" -> {
val path = params["path"] as? String ?: "/sdcard/screenshot.png"
executeShell("screencap -p $path")
}
"back" -> {
val accessibilityService = AgentAccessibilityService.getInstance()
if (accessibilityService != null) {
accessibilityService.back()
Pair(true, "已返回")
} else {
executeShell("input keyevent KEYCODE_BACK")
}
}
"home" -> {
val accessibilityService = AgentAccessibilityService.getInstance()
if (accessibilityService != null) {
accessibilityService.home()
Pair(true, "已回到桌面")
} else {
executeShell("input keyevent KEYCODE_HOME")
}
}
"recent" -> {
val accessibilityService = AgentAccessibilityService.getInstance()
if (accessibilityService != null) {
accessibilityService.recent()
Pair(true, "已显示最近任务")
} else {
executeShell("input keyevent KEYCODE_APP_SWITCH")
}
}
// === 语音命令由AI解析后发送===
"voice_command" -> {
val text = params["text"] as? String ?: ""
// 语音命令会被服务器AI解析后转为具体操作
Pair(true, "语音命令已接收: $text")
}
// === 获取UI层级 ===
"dump_ui" -> {
executeShell("uiautomator dump /sdcard/ui.xml && cat /sdcard/ui.xml")
}
// === 通用Shell命令服务端远程调用 ===
"shell", "run_shell" -> {
val cmd = params["command"] as? String ?: ""
if (cmd.isBlank()) {
Pair(false, "command参数不能为空")
} else {
executeShell(cmd)
}
}
// === 获取当前前台App信息 ===
"get_foreground" -> {
executeShell("dumpsys window | grep mCurrentFocus")
}
// === 安装APKRoot静默安装 ===
"install_apk" -> {
val path = params["path"] as? String ?: ""
if (path.isBlank()) {
Pair(false, "path参数不能为空")
} else {
executeShell("pm install -r -g $path")
}
}
else -> Pair(false, "未知命令: $action")
}
} catch (e: Exception) {
Pair(false, e.message ?: "执行失败")
}
}
}
/**
* 执行Shell命令
* 注意需要设备开启ADB调试或有ROOT权限
*/
private fun executeShell(command: String): Pair<Boolean, String> {
return try {
Log.d(TAG, "执行Shell: $command")
fun runWithTimeout(cmd: Array<String>, timeoutSec: Long): Triple<Int, String, String>? {
val p = Runtime.getRuntime().exec(cmd)
val completed = p.waitFor(timeoutSec, java.util.concurrent.TimeUnit.SECONDS)
if (!completed) {
p.destroyForcibly()
Log.w(TAG, "命令超时(${timeoutSec}s): ${cmd.joinToString(" ")}")
return null
}
val out = p.inputStream.bufferedReader().readText()
val err = p.errorStream.bufferedReader().readText()
return Triple(p.exitValue(), out, err)
}
// 1) 先尝试 su -c5秒超时Magisk可能弹窗阻塞
val suResult = runWithTimeout(arrayOf("su", "-c", command), 5)
if (suResult != null && suResult.first == 0) {
return Pair(true, suResult.second.ifEmpty { "执行成功(root)" })
}
// 2) 回退到 sh -c15秒超时
val shResult = runWithTimeout(arrayOf("sh", "-c", command), 15)
if (shResult != null && shResult.first == 0) {
return Pair(true, shResult.second.ifEmpty { "执行成功" })
}
val errMsg = when {
suResult == null && shResult == null -> "su和sh均超时"
suResult == null -> "su超时, sh失败: ${shResult?.third ?: "unknown"}"
shResult == null -> "su失败, sh超时"
else -> (suResult.third.ifEmpty { shResult?.third ?: "" }).ifEmpty {
"执行失败,退出码: su=${suResult.first} sh=${shResult?.first}"
}
}
Pair(false, errMsg)
} catch (e: Exception) {
Log.e(TAG, "Shell执行失败", e)
Pair(false, e.message ?: "Shell执行异常")
}
}
private fun openApp(packageName: String): Pair<Boolean, String> {
return try {
val intent = packageManager.getLaunchIntentForPackage(packageName)
if (intent != null) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
Pair(true, "已打开 $packageName")
} else {
Pair(false, "未找到应用: $packageName")
}
} catch (e: Exception) {
Pair(false, e.message ?: "打开失败")
}
}
private fun getInstalledApps(): List<String> {
val pm = packageManager
val packages = pm.getInstalledApplications(0)
return packages.filter {
pm.getLaunchIntentForPackage(it.packageName) != null
}.map { it.packageName }
}
private fun getDeviceInfo(): String {
val info = DeviceInfo.getDeviceInfo(this)
info.put("device_id", deviceId)
info.put("project_id", projectId)
return info.toString()
}
private fun sendPong() {
val pong = mapOf(
"type" to "pong",
"device_id" to deviceId,
"timestamp" to System.currentTimeMillis()
)
webSocketClient?.send(gson.toJson(pong))
}
private fun handleConfig(data: Map<*, *>) {
Log.d(TAG, "收到配置: $data")
val heartbeatSec = (data["heartbeat_interval_seconds"] as? Number)?.toLong()
?: ((data["params"] as? Map<*, *>)?.get("heartbeat_interval_seconds") as? Number)?.toLong()
if (heartbeatSec != null && heartbeatSec in 5..120) {
heartbeatIntervalMs = heartbeatSec * 1000
startHeartbeat()
updateNotification("已连接 - 心跳${heartbeatSec}s")
}
}
private fun scheduleReconnect() {
if (reconnectJob?.isActive == true) return
reconnectJob = serviceScope.launch {
var retryCount = 0
val maxRetries = 10 // 最多重试10次
val baseDelay = 5000L // 基础延迟5秒
while (retryCount < maxRetries && isRunning && !isSocketConnected) {
val delay = baseDelay * (1 shl minOf(retryCount, 4)) // 指数退避最多32秒
delay(delay)
if (isRunning && !isSocketConnected) {
Logger.d("尝试重连... (${retryCount + 1}/$maxRetries)")
try {
connectWebSocket()
// 等待连接结果
delay(3000)
if (isSocketConnected) {
Logger.i("重连成功")
break
}
} catch (e: Exception) {
Logger.w("重连失败", e)
}
retryCount++
}
}
if (retryCount >= maxRetries && isRunning && !isSocketConnected) {
Logger.e("重连失败,已达到最大重试次数")
updateNotification("连接失败,请检查网络")
}
}
}
private fun startPerformanceMonitoring() {
performanceMonitorJob?.cancel()
performanceMonitorJob = serviceScope.launch {
while (isActive) {
delay(60_000) // 每分钟检查一次
try {
val optimization = PerformanceMonitor.checkOptimization(this@AgentService)
if (optimization.optBoolean("memory_warning", false) ||
optimization.optBoolean("cpu_warning", false)) {
Logger.w("性能警告: ${optimization.optString("suggestions", "")}")
// 如果内存警告建议GC
if (optimization.optBoolean("memory_warning", false)) {
PerformanceMonitor.suggestGc()
}
}
} catch (e: Exception) {
Logger.e("性能监控失败", e)
}
}
}
}
private fun disconnect() {
heartbeatJob?.cancel()
reconnectJob?.cancel()
performanceMonitorJob?.cancel()
webSocketClient?.close()
webSocketClient = null
isRunning = false
isSocketConnected = false
}
}

View File

@@ -1,160 +0,0 @@
package com.system.cloudservice
import android.graphics.Color
import android.os.Bundle
import android.view.Gravity
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.LinearLayout
import android.widget.TextView
import androidx.fragment.app.Fragment
import com.system.cloudservice.databinding.FragmentAiBinding
class AiFragment : Fragment() {
private var _binding: FragmentAiBinding? = null
private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, c: ViewGroup?, s: Bundle?): View {
_binding = FragmentAiBinding.inflate(inflater, c, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupInput()
setupChips()
}
private fun setupInput() {
binding.btnSend.setOnClickListener { sendMessage() }
binding.etChatInput.setOnEditorActionListener { _, actionId, event ->
if (actionId == EditorInfo.IME_ACTION_SEND ||
(event?.keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_DOWN)) {
sendMessage()
true
} else false
}
binding.btnClearChat.setOnClickListener { clearChat() }
}
private fun setupChips() {
binding.chipWechat.setOnClickListener { quickSend("打开微信") }
binding.chipScreenshot.setOnClickListener { quickSend("截图") }
binding.chipHome.setOnClickListener { quickSend("返回桌面") }
binding.chipDouyin.setOnClickListener { quickSend("打开抖音") }
binding.chipVolUp.setOnClickListener { quickSend("音量加") }
}
private fun quickSend(text: String) {
binding.etChatInput.setText(text)
sendMessage()
}
private fun sendMessage() {
val text = binding.etChatInput.text.toString().trim()
if (text.isEmpty()) return
binding.etChatInput.text?.clear()
addUserBubble(text)
val loadingView = addAiBubble("执行中...")
Thread {
val result = LocalAI.executeVoiceCommand(requireContext(), text)
activity?.runOnUiThread {
if (_binding != null) {
loadingView.text = result
scrollToBottom()
incrementCommandCount()
}
}
}.start()
}
private fun addUserBubble(text: String) {
val container = LinearLayout(requireContext()).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply { bottomMargin = dpToPx(12) }
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.END
}
val bubble = TextView(requireContext()).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply { leftMargin = dpToPx(48) }
setBackgroundResource(R.drawable.chat_bubble_user)
setPadding(dpToPx(14), dpToPx(10), dpToPx(14), dpToPx(10))
this.text = text
setTextColor(Color.parseColor("#E6EDF3"))
textSize = 14f
}
container.addView(bubble)
binding.chatContainer.addView(container)
scrollToBottom()
}
private fun addAiBubble(text: String): TextView {
val container = LinearLayout(requireContext()).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply { bottomMargin = dpToPx(12) }
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.START
}
val bubble = TextView(requireContext()).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply { rightMargin = dpToPx(48) }
setBackgroundResource(R.drawable.chat_bubble_ai)
setPadding(dpToPx(14), dpToPx(10), dpToPx(14), dpToPx(10))
this.text = text
setTextColor(Color.parseColor("#E6EDF3"))
textSize = 14f
}
container.addView(bubble)
binding.chatContainer.addView(container)
scrollToBottom()
return bubble
}
private fun clearChat() {
val childCount = binding.chatContainer.childCount
if (childCount > 1) {
binding.chatContainer.removeViews(1, childCount - 1)
}
}
private fun scrollToBottom() {
binding.chatScrollView.post {
binding.chatScrollView.fullScroll(View.FOCUS_DOWN)
}
}
private fun incrementCommandCount() {
val prefs = requireContext().getSharedPreferences("agent_config", android.content.Context.MODE_PRIVATE)
val count = prefs.getInt("stat_commands", 0) + 1
prefs.edit().putInt("stat_commands", count).apply()
}
private fun dpToPx(dp: Int): Int {
return (dp * resources.displayMetrics.density).toInt()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -0,0 +1,18 @@
package com.system.cloudservice
import android.app.Application
import com.system.cloudservice.util.Logger
class App : Application() {
companion object {
lateinit var instance: App
private set
}
override fun onCreate() {
super.onCreate()
instance = this
Logger.init(this)
Logger.i("工作 v5.0 Application created")
}
}

View File

@@ -1,45 +0,0 @@
package com.system.cloudservice
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.Log
/**
* 开机启动接收器
* 系统启动后自动启动Agent服务
*/
class BootReceiver : BroadcastReceiver() {
companion object {
const val TAG = "BootReceiver"
}
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == Intent.ACTION_BOOT_COMPLETED && context != null) {
Log.d(TAG, "系统启动完成启动Agent服务")
val prefs = context.getSharedPreferences("agent_config", Context.MODE_PRIVATE)
val serverUrl = prefs.getString("server_url", "ws://127.0.0.1:8899/ws/device") ?: ""
val projectId = prefs.getString("project_id", "default") ?: "default"
val deviceId = prefs.getString("device_id", "")?.takeIf { it.isNotBlank() }
?: DeviceIdHelper.getOrCreateDeviceId(context)
if (serverUrl.isNotEmpty()) {
val serviceIntent = Intent(context, AgentService::class.java).apply {
action = AgentService.ACTION_START
putExtra(AgentService.EXTRA_SERVER_URL, serverUrl)
putExtra(AgentService.EXTRA_PROJECT_ID, projectId.ifBlank { "default" })
putExtra(AgentService.EXTRA_DEVICE_ID, deviceId)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(serviceIntent)
} else {
context.startService(serviceIntent)
}
}
}
}
}

View File

@@ -1,136 +0,0 @@
package com.system.cloudservice
import android.content.Context
import java.io.BufferedReader
import java.io.InputStreamReader
/**
* 命令执行器
* 通过ADB或Accessibility Service执行命令
* 智能降级Accessibility Service -> Shell -> ADB -> SU
*/
object CommandExecutor {
/**
* 执行ADB命令需要ADB调试权限
*/
fun executeViaADB(command: String): String {
return try {
Logger.d("通过ADB执行: $command")
val process = Runtime.getRuntime().exec(arrayOf("sh", "-c", "adb shell $command"))
val reader = BufferedReader(InputStreamReader(process.inputStream))
val output = reader.readText()
process.waitFor()
if (process.exitValue() == 0) {
output.ifEmpty { "执行成功" }
} else {
"执行失败"
}
} catch (e: Exception) {
Logger.e("ADB执行失败", e)
"执行失败: ${e.message}"
}
}
/**
* 执行Shell命令需要root或ADB
*/
fun executeShell(command: String): Boolean {
return try {
Logger.d("执行Shell: $command")
val process = Runtime.getRuntime().exec(arrayOf("sh", "-c", command))
process.waitFor()
val success = process.exitValue() == 0
if (!success) {
Logger.w("Shell执行失败退出码: ${process.exitValue()}")
}
success
} catch (e: Exception) {
Logger.e("Shell执行失败", e)
false
}
}
/**
* 执行Shell命令并返回输出
*/
fun executeShellWithOutput(command: String): Pair<Boolean, String> {
return try {
Logger.d("执行Shell: $command")
val process = Runtime.getRuntime().exec(arrayOf("sh", "-c", command))
val reader = BufferedReader(InputStreamReader(process.inputStream))
val errorReader = BufferedReader(InputStreamReader(process.errorStream))
val output = reader.readText()
val error = errorReader.readText()
process.waitFor()
if (process.exitValue() == 0) {
Pair(true, output.ifEmpty { "执行成功" })
} else {
Pair(false, error.ifEmpty { "执行失败" })
}
} catch (e: Exception) {
Logger.e("Shell执行失败", e)
Pair(false, "执行失败: ${e.message}")
}
}
/**
* 点击坐标(智能降级)
*/
fun click(x: Int, y: Int): Boolean {
// 优先使用Accessibility Service
val accessibilityService = AgentAccessibilityService.getInstance()
if (accessibilityService != null) {
var success = false
accessibilityService.click(x, y) { result ->
success = result
}
// 等待执行完成
Thread.sleep(200)
if (success) return true
}
// 降级到Shell
return executeShell("input tap $x $y")
}
/**
* 滑动(智能降级)
*/
fun swipe(x1: Int, y1: Int, x2: Int, y2: Int, duration: Long = 300): Boolean {
// 优先使用Accessibility Service
val accessibilityService = AgentAccessibilityService.getInstance()
if (accessibilityService != null) {
var success = false
accessibilityService.swipe(x1, y1, x2, y2, duration) { result ->
success = result
}
Thread.sleep(duration + 100)
if (success) return true
}
// 降级到Shell
return executeShell("input swipe $x1 $y1 $x2 $y2 ${duration.toInt()}")
}
/**
* 输入文字(智能降级)
*/
fun inputText(text: String): Boolean {
// 优先使用Accessibility Service
val accessibilityService = AgentAccessibilityService.getInstance()
if (accessibilityService != null) {
var success = false
accessibilityService.inputText(text) { result ->
success = result
}
Thread.sleep(500)
if (success) return true
}
// 降级到Shell中文输入
return executeShell("am broadcast -a ADB_INPUT_TEXT --es msg '$text'")
}
}

View File

@@ -1,167 +0,0 @@
package com.system.cloudservice
import android.Manifest
import android.animation.ObjectAnimator
import android.animation.PropertyValuesHolder
import android.content.pm.PackageManager
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import com.system.cloudservice.databinding.FragmentControlBinding
class ControlFragment : Fragment(), VoiceHelper.VoiceListener {
private var _binding: FragmentControlBinding? = null
private val binding get() = _binding!!
private lateinit var voiceHelper: VoiceHelper
private var isVoiceListening = false
private var pulseAnimator: ObjectAnimator? = null
override fun onCreateView(inflater: LayoutInflater, c: ViewGroup?, s: Bundle?): View {
_binding = FragmentControlBinding.inflate(inflater, c, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
voiceHelper = VoiceHelper(requireContext())
voiceHelper.setListener(this)
setupButtons()
}
private fun setupButtons() {
// 常用应用
binding.btnAppWechat.setOnClickListener { exec("打开微信") }
binding.btnAppDouyin.setOnClickListener { exec("打开抖音") }
binding.btnAppXhs.setOnClickListener { exec("打开小红书") }
binding.btnAppQQ.setOnClickListener { exec("打开QQ") }
binding.btnAppFeishu.setOnClickListener { exec("打开飞书") }
binding.btnAppSettings.setOnClickListener { exec("打开设置") }
// 系统控制
binding.btnBack.setOnClickListener { exec("返回") }
binding.btnHome.setOnClickListener { exec("桌面") }
binding.btnScreenshot.setOnClickListener { exec("截图") }
binding.btnSwipeUp.setOnClickListener { exec("上滑") }
binding.btnSwipeDown.setOnClickListener { exec("下滑") }
binding.btnNotification.setOnClickListener { exec("通知") }
binding.btnRecent.setOnClickListener { exec("最近任务") }
binding.btnLock.setOnClickListener { exec("锁屏") }
binding.btnRefresh.setOnClickListener { exec("刷新") }
// 语音按钮
binding.btnVoice.setOnClickListener { toggleVoice() }
}
private fun exec(command: String) {
binding.tvVoiceResult.text = "执行: $command"
Thread {
val result = LocalAI.executeVoiceCommand(requireContext(), command)
activity?.runOnUiThread {
if (_binding != null) {
binding.tvVoiceResult.text = result
incrementCommandCount()
}
}
}.start()
}
private fun incrementCommandCount() {
val prefs = requireContext().getSharedPreferences("agent_config", android.content.Context.MODE_PRIVATE)
val count = prefs.getInt("stat_commands", 0) + 1
prefs.edit().putInt("stat_commands", count).apply()
}
private fun toggleVoice() {
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.RECORD_AUDIO)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(requireActivity(), arrayOf(Manifest.permission.RECORD_AUDIO), 1001)
return
}
if (isVoiceListening) {
voiceHelper.stopListening()
} else {
if (voiceHelper.isAvailable()) {
voiceHelper.startListening()
} else {
Toast.makeText(requireContext(), "设备不支持语音识别", Toast.LENGTH_SHORT).show()
}
}
}
override fun onVoiceStart() {
isVoiceListening = true
activity?.runOnUiThread {
binding.tvVoiceHint.text = "正在听..."
binding.tvVoiceResult.text = ""
startPulseAnimation()
}
}
override fun onVoiceResult(text: String) {
activity?.runOnUiThread {
binding.tvVoiceResult.text = "\"$text\""
exec(text)
}
}
override fun onVoiceError(message: String) {
stopListeningUI()
activity?.runOnUiThread {
binding.tvVoiceHint.text = message
}
}
override fun onVoiceEnd() {
stopListeningUI()
}
override fun onPartialResult(text: String) {
activity?.runOnUiThread {
binding.tvVoiceResult.text = text
}
}
private fun stopListeningUI() {
isVoiceListening = false
activity?.runOnUiThread {
binding.tvVoiceHint.text = "点击语音控制"
stopPulseAnimation()
}
}
private fun startPulseAnimation() {
binding.voiceRipple.alpha = 1f
pulseAnimator = ObjectAnimator.ofPropertyValuesHolder(
binding.voiceRipple,
PropertyValuesHolder.ofFloat(View.SCALE_X, 1f, 1.3f),
PropertyValuesHolder.ofFloat(View.SCALE_Y, 1f, 1.3f),
PropertyValuesHolder.ofFloat(View.ALPHA, 0.8f, 0f)
).apply {
duration = 1000
repeatCount = ObjectAnimator.INFINITE
interpolator = AccelerateDecelerateInterpolator()
start()
}
}
private fun stopPulseAnimation() {
pulseAnimator?.cancel()
binding.voiceRipple.alpha = 0f
}
override fun onDestroyView() {
super.onDestroyView()
pulseAnimator?.cancel()
voiceHelper.destroy()
_binding = null
}
}

View File

@@ -1,130 +0,0 @@
package com.system.cloudservice
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.system.cloudservice.databinding.FragmentDashboardBinding
class DashboardFragment : Fragment() {
private var _binding: FragmentDashboardBinding? = null
private val binding get() = _binding!!
private val handler = Handler(Looper.getMainLooper())
private val prefs by lazy {
requireContext().getSharedPreferences("agent_config", android.content.Context.MODE_PRIVATE)
}
private var connectTimeMs = 0L
private var commandCount = 0
private var messageCount = 0
private val refreshRunnable = object : Runnable {
override fun run() {
if (_binding != null) {
refreshStatus()
handler.postDelayed(this, 3000)
}
}
}
override fun onCreateView(inflater: LayoutInflater, c: ViewGroup?, s: Bundle?): View {
_binding = FragmentDashboardBinding.inflate(inflater, c, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
loadDeviceInfo()
loadServiceInfo()
refreshStatus()
}
override fun onResume() {
super.onResume()
handler.post(refreshRunnable)
}
override fun onPause() {
super.onPause()
handler.removeCallbacks(refreshRunnable)
}
private fun loadDeviceInfo() {
binding.tvDeviceModel.text = "${Build.BRAND} ${Build.MODEL}"
binding.tvAndroidVersion.text = "${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})"
val deviceId = prefs.getString("device_id", null)
?: DeviceIdHelper.getOrCreateDeviceId(requireContext())
binding.tvDeviceId.text = deviceId
Thread {
val isRoot = try {
val p = Runtime.getRuntime().exec(arrayOf("su", "-c", "id"))
val out = p.inputStream.bufferedReader().readText()
p.waitFor()
out.contains("uid=0")
} catch (_: Exception) { false }
handler.post {
if (_binding != null) {
if (isRoot) {
binding.tvRootStatus.text = "已Root"
binding.tvRootStatus.setTextColor(Color.parseColor("#3FB950"))
} else {
binding.tvRootStatus.text = "未Root"
binding.tvRootStatus.setTextColor(Color.parseColor("#8B949E"))
}
}
}
}.start()
}
private fun loadServiceInfo() {
val serverUrl = prefs.getString("server_url", "")
val projectId = prefs.getString("project_id", "")
binding.tvServerUrl.text = if (serverUrl.isNullOrEmpty()) "未配置" else serverUrl
binding.tvProjectId.text = if (projectId.isNullOrEmpty()) "未配置" else projectId
}
private fun refreshStatus() {
val isOnline = AgentService.isRunning
val wsConnected = AgentService.isSocketConnected
val dot = binding.statusDot.background as? GradientDrawable
if (isOnline) {
dot?.setColor(Color.parseColor("#3FB950"))
binding.tvStatusTitle.text = "已连接"
binding.tvStatusTitle.setTextColor(Color.parseColor("#3FB950"))
binding.tvStatusDetail.text = if (wsConnected) "服务正常运行中" else "服务运行中(自动重连)"
if (connectTimeMs == 0L) connectTimeMs = System.currentTimeMillis()
val hours = (System.currentTimeMillis() - connectTimeMs) / 3600000
val mins = ((System.currentTimeMillis() - connectTimeMs) % 3600000) / 60000
binding.tvStatUptime.text = if (hours > 0) "${hours}h${mins}m" else "${mins}m"
} else {
dot?.setColor(Color.parseColor("#F85149"))
binding.tvStatusTitle.text = "未连接"
binding.tvStatusTitle.setTextColor(Color.parseColor("#F85149"))
binding.tvStatusDetail.text = "请在设置页配置服务器"
connectTimeMs = 0L
binding.tvStatUptime.text = "0m"
}
loadServiceInfo()
commandCount = prefs.getInt("stat_commands", 0)
messageCount = prefs.getInt("stat_messages", 0)
binding.tvStatCommands.text = "$commandCount"
binding.tvStatMessages.text = "$messageCount"
}
override fun onDestroyView() {
super.onDestroyView()
handler.removeCallbacks(refreshRunnable)
_binding = null
}
}

View File

@@ -1,62 +0,0 @@
package com.system.cloudservice
import android.content.Context
import android.os.Build
import android.provider.Settings
import java.security.MessageDigest
/**
* 设备ID策略
* 1) 默认使用奥创兼容IDmd5(android_id)
* 2) 保留原始 android_id / serial 便于服务端排障
*/
object DeviceIdHelper {
fun getOrCreateDeviceId(context: Context): String {
val prefs = context.getSharedPreferences("agent_config", Context.MODE_PRIVATE)
val stored = prefs.getString("device_id", "")?.trim().orEmpty()
if (stored.isNotEmpty()) return stored
val generated = getAochuangCompatibleId(context)
prefs.edit().putString("device_id", generated).apply()
return generated
}
fun getAochuangCompatibleId(context: Context): String {
val androidId = getAndroidId(context)
if (androidId.isNotEmpty()) {
return md5(androidId)
}
return "device_${Build.MODEL.replace(" ", "_")}_${Build.VERSION.SDK_INT}"
}
fun getAndroidId(context: Context): String {
return try {
Settings.Secure.getString(
context.contentResolver,
Settings.Secure.ANDROID_ID
)?.trim().orEmpty()
} catch (_: Exception) {
""
}
}
fun getSerial(): String {
return try {
Runtime.getRuntime()
.exec(arrayOf("getprop", "ro.serialno"))
.inputStream
.bufferedReader()
.readText()
.trim()
} catch (_: Exception) {
""
}
}
private fun md5(input: String): String {
val md = MessageDigest.getInstance("MD5")
val digest = md.digest(input.toByteArray(Charsets.UTF_8))
return digest.joinToString("") { b -> "%02x".format(b) }
}
}

View File

@@ -1,79 +0,0 @@
package com.system.cloudservice
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import org.json.JSONObject
/**
* 设备信息收集
*/
object DeviceInfo {
/**
* 获取完整设备信息
*/
fun getDeviceInfo(context: Context): JSONObject {
val info = JSONObject()
try {
// 基本信息
info.put("model", Build.MODEL)
info.put("brand", Build.BRAND)
info.put("manufacturer", Build.MANUFACTURER)
info.put("device", Build.DEVICE)
info.put("product", Build.PRODUCT)
// 系统信息
info.put("android_version", Build.VERSION.RELEASE)
info.put("sdk_version", Build.VERSION.SDK_INT)
info.put("security_patch", Build.VERSION.SECURITY_PATCH)
// 硬件信息
info.put("cpu_abi", Build.SUPPORTED_ABIS.joinToString(","))
info.put("screen_width", context.resources.displayMetrics.widthPixels)
info.put("screen_height", context.resources.displayMetrics.heightPixels)
info.put("density", context.resources.displayMetrics.density)
// 应用信息
val pm = context.packageManager
val packageInfo = pm.getPackageInfo(context.packageName, 0)
info.put("app_version", packageInfo.versionName)
info.put("app_version_code", packageInfo.versionCode)
// 权限信息
val permissions = mutableListOf<String>()
if (pm.checkPermission(android.Manifest.permission.RECORD_AUDIO, context.packageName) == PackageManager.PERMISSION_GRANTED) {
permissions.add("RECORD_AUDIO")
}
if (pm.checkPermission(android.Manifest.permission.CAMERA, context.packageName) == PackageManager.PERMISSION_GRANTED) {
permissions.add("CAMERA")
}
info.put("permissions", permissions.joinToString(","))
// Accessibility Service状态
info.put("accessibility_enabled", AgentAccessibilityService.isEnabled())
// 已安装应用数量
val installedApps = pm.getInstalledApplications(0)
info.put("installed_apps_count", installedApps.size)
} catch (e: Exception) {
Logger.e("获取设备信息失败", e)
}
return info
}
/**
* 获取简要设备信息(用于注册)
*/
fun getSimpleDeviceInfo(): Map<String, String> {
return mapOf(
"model" to Build.MODEL,
"brand" to Build.BRAND,
"android_version" to Build.VERSION.RELEASE,
"sdk_version" to Build.VERSION.SDK_INT.toString()
)
}
}

View File

@@ -1,436 +0,0 @@
package com.system.cloudservice
import android.content.Context
import android.content.Intent
import java.io.DataOutputStream
/**
* 本地AI意图解析 + 自动化执行
*
* 不依赖服务器,手机本地直接执行
* 优先使用Accessibility Service降级到Shell命令
*/
object LocalAI {
// 常用应用包名
private val appPackages = mapOf(
"微信" to "com.tencent.mm",
"抖音" to "com.ss.android.ugc.aweme",
"支付宝" to "com.eg.android.AlipayGphone",
"淘宝" to "com.taobao.taobao",
"微博" to "com.sina.weibo",
"qq" to "com.tencent.mobileqq",
"QQ" to "com.tencent.mobileqq",
"设置" to "com.android.settings",
"相机" to "com.android.camera",
"浏览器" to "com.android.chrome",
"相册" to "com.android.gallery3d",
"电话" to "com.android.dialer",
"短信" to "com.android.mms",
"日历" to "com.android.calendar",
"时钟" to "com.android.deskclock",
"计算器" to "com.android.calculator2",
"地图" to "com.autonavi.minimap",
"高德" to "com.autonavi.minimap",
"百度" to "com.baidu.searchbox",
"美团" to "com.sankuai.meituan",
"饿了么" to "me.ele",
"京东" to "com.jingdong.app.mall",
"拼多多" to "com.xunmeng.pinduoduo",
"小红书" to "com.xingin.xhs",
"钉钉" to "com.alibaba.android.rimet",
"飞书" to "com.ss.android.lark",
"网易云" to "com.netease.cloudmusic",
"QQ音乐" to "com.tencent.qqmusic",
"酷狗" to "com.kugou.android",
"bilibili" to "tv.danmaku.bili",
"B站" to "tv.danmaku.bili",
"知乎" to "com.zhihu.android",
"今日头条" to "com.ss.android.article.news",
"快手" to "com.smile.gifmaker",
"豆包" to "com.bytedance.doubao",
"豆包AI" to "com.bytedance.doubao",
"字节豆包" to "com.bytedance.doubao",
)
/**
* 解析并执行语音命令
* 返回执行结果描述
*/
fun executeVoiceCommand(context: Context, text: String): String {
Logger.d("执行语音命令: $text")
val cmd = text.lowercase().trim()
// 0. 处理复合命令(如"打开豆包,搜索今天去哪"
val parts = cmd.split(Regex("[,,、]|然后|再|接着"))
if (parts.size > 1) {
var result = ""
for (part in parts) {
val partResult = executeVoiceCommand(context, part.trim())
result += if (result.isEmpty()) partResult else "$partResult"
Thread.sleep(1500) // 等待1.5秒让应用启动
}
return result
}
// 1. 搜索命令(如"搜索今天去哪"、"在豆包里搜索"
if (cmd.contains("搜索") || cmd.contains("查找") || cmd.contains("")) {
val searchText = extractSearchText(cmd)
if (searchText.isNotEmpty()) {
return performSearch(context, searchText)
}
}
// 2. 打开应用(可能带搜索,如"打开豆包搜索今天去哪"
for ((name, pkg) in appPackages) {
val openPatterns = listOf(
"打开$name",
"启动$name",
"打开${name.lowercase()}",
"${name}"
)
for (pattern in openPatterns) {
if (cmd.contains(pattern)) {
val result = openApp(context, pkg, name)
// 检查是否有搜索关键词
val searchText = extractSearchText(cmd)
if (searchText.isNotEmpty()) {
Thread.sleep(2000) // 等待应用启动
return "$result${performSearch(context, searchText)}"
}
return result
}
}
}
// 2. 返回
if (cmd.contains("返回") || cmd.contains("回去") || cmd.contains("后退")) {
return executeShell("input keyevent KEYCODE_BACK", "返回")
}
// 3. 回到桌面
if (cmd.contains("桌面") || cmd.contains("主页") || cmd.contains("home")) {
return executeShell("input keyevent KEYCODE_HOME", "回到桌面")
}
// 4. 最近任务
if (cmd.contains("最近") || cmd.contains("任务") || cmd.contains("切换")) {
return executeShell("input keyevent KEYCODE_APP_SWITCH", "显示最近任务")
}
// 5. 截图
if (cmd.contains("截图") || cmd.contains("截屏")) {
return executeShell("screencap -p /sdcard/screenshot_${System.currentTimeMillis()}.png", "已截图")
}
// 6. 向上滑动
if (cmd.contains("向上滑") || cmd.contains("上滑") || cmd.contains("往上滑") || cmd.contains("上翻")) {
return executeShell("input swipe 540 1500 540 500 300", "向上滑动")
}
// 7. 向下滑动
if (cmd.contains("向下滑") || cmd.contains("下滑") || cmd.contains("往下滑") || cmd.contains("下翻")) {
return executeShell("input swipe 540 500 540 1500 300", "向下滑动")
}
// 8. 向左滑动
if (cmd.contains("向左滑") || cmd.contains("左滑") || cmd.contains("往左滑")) {
return executeShell("input swipe 800 1000 200 1000 300", "向左滑动")
}
// 9. 向右滑动
if (cmd.contains("向右滑") || cmd.contains("右滑") || cmd.contains("往右滑")) {
return executeShell("input swipe 200 1000 800 1000 300", "向右滑动")
}
// 10. 音量调节
if (cmd.contains("音量加") || cmd.contains("大声") || cmd.contains("声音大")) {
return executeShell("input keyevent KEYCODE_VOLUME_UP", "音量+")
}
if (cmd.contains("音量减") || cmd.contains("小声") || cmd.contains("声音小")) {
return executeShell("input keyevent KEYCODE_VOLUME_DOWN", "音量-")
}
if (cmd.contains("静音")) {
return executeShell("input keyevent KEYCODE_VOLUME_MUTE", "静音")
}
// 11. 亮度调节
if (cmd.contains("亮度")) {
return openApp(context, "com.android.settings", "设置")
}
// 12. WiFi
if (cmd.contains("wifi") || cmd.contains("无线")) {
return executeShell("am start -a android.settings.WIFI_SETTINGS", "WiFi设置")
}
// 13. 蓝牙
if (cmd.contains("蓝牙")) {
return executeShell("am start -a android.settings.BLUETOOTH_SETTINGS", "蓝牙设置")
}
// 14. 锁屏
if (cmd.contains("锁屏") || cmd.contains("锁定")) {
return executeShell("input keyevent KEYCODE_POWER", "锁屏")
}
// 15. 播放/暂停
if (cmd.contains("播放") || cmd.contains("暂停") || cmd.contains("继续播放")) {
return executeShell("input keyevent KEYCODE_MEDIA_PLAY_PAUSE", "播放/暂停")
}
// 16. 下一首
if (cmd.contains("下一首") || cmd.contains("下一曲")) {
return executeShell("input keyevent KEYCODE_MEDIA_NEXT", "下一首")
}
// 17. 上一首
if (cmd.contains("上一首") || cmd.contains("上一曲")) {
return executeShell("input keyevent KEYCODE_MEDIA_PREVIOUS", "上一首")
}
// 18. 点击屏幕中心
if (cmd.contains("点击") || cmd.contains("确认") || cmd.contains("确定")) {
return executeShell("input tap 540 1200", "点击屏幕")
}
// 19. 刷新
if (cmd.contains("刷新")) {
return executeShell("input swipe 540 300 540 1000 300", "刷新")
}
// 20. 通知栏
if (cmd.contains("通知") || cmd.contains("消息")) {
return executeShell("cmd statusbar expand-notifications", "打开通知栏")
}
// 未识别的命令
return "不理解: $text"
}
/**
* 提取搜索关键词
* 例如:"搜索今天去哪" -> "今天去哪"
*/
private fun extractSearchText(cmd: String): String {
val patterns = listOf(
Regex("搜索(.+)"),
Regex("查找(.+)"),
Regex("找(.+)"),
Regex("搜(.+)")
)
for (pattern in patterns) {
val match = pattern.find(cmd)
if (match != null) {
var text = match.groupValues[1].trim()
// 移除可能的标点
text = text.replace(Regex("[,,。!?、]"), "").trim()
return text
}
}
return ""
}
/**
* 执行搜索操作
* 优先使用Accessibility Service
*/
private fun performSearch(context: Context, searchText: String): String {
Logger.d("执行搜索: $searchText")
// 优先使用Accessibility Service
val accessibilityService = AgentAccessibilityService.getInstance()
if (accessibilityService != null) {
return try {
// 查找搜索框并点击
var searchClicked = false
val rootNode = accessibilityService.rootInActiveWindow
if (rootNode != null) {
// 尝试查找搜索相关的节点
val searchNodes = rootNode.findAccessibilityNodeInfosByText("搜索")
if (searchNodes.isNotEmpty()) {
val bounds = android.graphics.Rect()
searchNodes[0].getBoundsInScreen(bounds)
accessibilityService.click(bounds.centerX(), bounds.centerY()) { success ->
searchClicked = success
}
Thread.sleep(500)
}
rootNode.recycle()
}
if (searchClicked) {
Thread.sleep(500)
}
// 输入搜索文本
accessibilityService.inputText(searchText) { success ->
if (success) {
Thread.sleep(500)
// 执行搜索(回车)
executeShell("input keyevent KEYCODE_ENTER", "")
}
}
"已搜索: $searchText"
} catch (e: Exception) {
Logger.e("Accessibility搜索失败降级到Shell", e)
performSearchFallback(context, searchText)
}
}
// 降级到Shell方式
return performSearchFallback(context, searchText)
}
/**
* 搜索降级方案Shell命令
*/
private fun performSearchFallback(context: Context, searchText: String): String {
Logger.d("使用Shell方式搜索: $searchText")
// 等待应用加载
Thread.sleep(1000)
// 方法1: 尝试点击搜索框(通常在屏幕上方)
executeShell("input tap 540 200", "")
Thread.sleep(500)
// 方法2: 输入搜索文本
// 使用ADB输入中文需要特殊处理
executeShell("am broadcast -a ADB_INPUT_TEXT --es msg '$searchText'", "")
Thread.sleep(500)
// 方法3: 如果上面不行,尝试用键盘输入
// 先尝试点击搜索框
executeShell("input tap 540 150", "")
Thread.sleep(300)
executeShell("input tap 540 200", "")
Thread.sleep(300)
// 输入文字(英文和数字)
val englishText = searchText.replace(Regex("[^a-zA-Z0-9\\s]"), "")
if (englishText.isNotEmpty()) {
executeShell("input text '$englishText'", "")
}
// 方法4: 尝试点击搜索按钮(通常在键盘上)
Thread.sleep(500)
executeShell("input keyevent KEYCODE_ENTER", "")
return "已搜索: $searchText"
}
/**
* 打开应用
*/
private fun openApp(context: Context, packageName: String, appName: String): String {
return try {
val intent = context.packageManager.getLaunchIntentForPackage(packageName)
if (intent != null) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
"已打开$appName"
} else {
// 尝试用am命令启动
executeShell("monkey -p $packageName -c android.intent.category.LAUNCHER 1", "正在打开$appName")
}
} catch (e: Exception) {
Logger.e("打开应用失败: $appName", e)
"打开${appName}失败"
}
}
/**
* 执行Shell命令
* 通过ADB执行需要ADB调试权限
* 添加超时和资源清理
*/
private fun executeShell(command: String, successMsg: String): String {
return try {
Logger.d("执行Shell: $command")
// 方法1: 直接执行需要root或ADB
try {
val process = Runtime.getRuntime().exec(arrayOf("sh", "-c", command))
// 设置超时5秒
val timeout = 5000L
val startTime = System.currentTimeMillis()
while (process.isAlive && (System.currentTimeMillis() - startTime) < timeout) {
Thread.sleep(100)
}
if (process.isAlive) {
process.destroyForcibly()
Logger.w("命令执行超时: $command")
return "执行超时"
}
val exitCode = process.exitValue()
// 清理资源
process.inputStream.close()
process.errorStream.close()
process.outputStream.close()
if (exitCode == 0) {
return successMsg
}
} catch (e: Exception) {
Logger.w("直接执行失败尝试ADB方式", e)
}
// 方法2: 通过ADB执行如果APP有ADB权限
// 注意这需要设备已开启ADB调试
try {
val adbCommand = "adb shell $command"
val process = Runtime.getRuntime().exec(arrayOf("sh", "-c", adbCommand))
process.waitFor()
if (process.exitValue() == 0) {
return successMsg
}
} catch (e: Exception) {
Logger.w("ADB执行失败", e)
}
// 方法3: 使用su需要root
tryWithSu(command, successMsg)
} catch (e: Exception) {
Logger.e("Shell执行失败", e)
"执行失败: ${e.message}"
}
}
/**
* 使用su执行需要root
*/
private fun tryWithSu(command: String, successMsg: String): String {
return try {
val process = Runtime.getRuntime().exec("su")
val os = DataOutputStream(process.outputStream)
os.writeBytes("$command\n")
os.writeBytes("exit\n")
os.flush()
process.waitFor()
if (process.exitValue() == 0) {
successMsg
} else {
"需要Root权限或ADB授权"
}
} catch (e: Exception) {
Logger.e("SU执行失败", e)
"需要Root权限或ADB授权"
}
}
}

View File

@@ -1,110 +0,0 @@
package com.system.cloudservice
import android.util.Log
import java.io.File
import java.io.FileWriter
import java.text.SimpleDateFormat
import java.util.*
/**
* 日志系统
* 同时输出到Logcat和文件
*/
object Logger {
private const val TAG = "WorkPhoneAgent"
private const val LOG_DIR = "/sdcard/workphone_agent/logs"
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
private var logFile: File? = null
private var fileWriter: FileWriter? = null
init {
initLogFile()
}
private fun initLogFile() {
try {
val dir = File(LOG_DIR)
if (!dir.exists()) {
dir.mkdirs()
}
val today = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date())
logFile = File(dir, "agent_$today.log")
fileWriter = FileWriter(logFile, true)
} catch (e: Exception) {
Log.e(TAG, "初始化日志文件失败", e)
}
}
private fun writeToFile(level: String, message: String, throwable: Throwable? = null) {
try {
val timestamp = dateFormat.format(Date())
val logMessage = "$timestamp [$level] $message"
fileWriter?.apply {
append(logMessage)
append("\n")
if (throwable != null) {
append(throwable.stackTraceToString())
append("\n")
}
flush()
}
// 限制日志文件大小10MB
logFile?.let {
if (it.length() > 10 * 1024 * 1024) {
rotateLogFile()
}
}
} catch (e: Exception) {
// 忽略日志写入错误
}
}
private fun rotateLogFile() {
try {
fileWriter?.close()
val oldFile = logFile
val backupFile = File("${oldFile?.absolutePath}.old")
oldFile?.renameTo(backupFile)
initLogFile()
} catch (e: Exception) {
Log.e(TAG, "日志轮转失败", e)
}
}
fun d(message: String, throwable: Throwable? = null) {
Log.d(TAG, message, throwable)
writeToFile("DEBUG", message, throwable)
}
fun i(message: String, throwable: Throwable? = null) {
Log.i(TAG, message, throwable)
writeToFile("INFO", message, throwable)
}
fun w(message: String, throwable: Throwable? = null) {
Log.w(TAG, message, throwable)
writeToFile("WARN", message, throwable)
}
fun e(message: String, throwable: Throwable? = null) {
Log.e(TAG, message, throwable)
writeToFile("ERROR", message, throwable)
}
fun getLogFile(): File? = logFile
fun clearLogs() {
try {
fileWriter?.close()
logFile?.delete()
initLogFile()
} catch (e: Exception) {
Log.e(TAG, "清空日志失败", e)
}
}
}

View File

@@ -1,150 +0,0 @@
package com.system.cloudservice
import android.app.ActivityManager
import android.content.Context
import android.os.Build
import android.os.Debug
import org.json.JSONObject
/**
* 性能监控
* 监控内存、CPU、电池使用情况
*/
object PerformanceMonitor {
private var lastCpuTime: Long = 0
private var lastAppCpuTime: Long = 0
/**
* 获取内存使用情况
*/
fun getMemoryInfo(context: Context): JSONObject {
val info = JSONObject()
try {
val am = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val memInfo = ActivityManager.MemoryInfo()
am.getMemoryInfo(memInfo)
// 系统总内存
info.put("total_memory_mb", memInfo.totalMem / 1024 / 1024)
// 可用内存
info.put("available_memory_mb", memInfo.availMem / 1024 / 1024)
// 是否低内存
info.put("low_memory", memInfo.lowMemory)
// 应用内存使用
val pid = android.os.Process.myPid()
val memoryInfo = am.getProcessMemoryInfo(intArrayOf(pid))
if (memoryInfo.isNotEmpty()) {
val pss = memoryInfo[0].totalPss
info.put("app_memory_mb", pss / 1024)
}
} catch (e: Exception) {
Logger.e("获取内存信息失败", e)
}
return info
}
/**
* 获取CPU使用率简化版
*/
fun getCpuUsage(): Double {
return try {
val totalTime = getTotalCpuTime()
val appTime = getAppCpuTime()
if (lastCpuTime > 0 && lastAppCpuTime > 0) {
val totalDelta = totalTime - lastCpuTime
val appDelta = appTime - lastAppCpuTime
if (totalDelta > 0) {
(appDelta.toDouble() / totalDelta) * 100.0
} else {
0.0
}
} else {
0.0
}.also {
lastCpuTime = totalTime
lastAppCpuTime = appTime
}
} catch (e: Exception) {
Logger.e("获取CPU使用率失败", e)
0.0
}
}
private fun getTotalCpuTime(): Long {
return try {
val stat = java.io.File("/proc/stat").readText()
val parts = stat.split("\\s+".toRegex())
if (parts.size > 8) {
parts.subList(1, 8).sumOf { it.toLongOrNull() ?: 0L }
} else {
0L
}
} catch (e: Exception) {
0L
}
}
private fun getAppCpuTime(): Long {
return try {
val stat = java.io.File("/proc/${android.os.Process.myPid()}/stat").readText()
val parts = stat.split("\\s+".toRegex())
if (parts.size > 15) {
(parts[13].toLongOrNull() ?: 0L) + (parts[14].toLongOrNull() ?: 0L)
} else {
0L
}
} catch (e: Exception) {
0L
}
}
/**
* 检查是否需要优化
*/
fun checkOptimization(context: Context): JSONObject {
val result = JSONObject()
val memoryInfo = getMemoryInfo(context)
val cpuUsage = getCpuUsage()
val appMemoryMb = memoryInfo.optInt("app_memory_mb", 0)
val availableMemoryMb = memoryInfo.optInt("available_memory_mb", 0)
val lowMemory = memoryInfo.optBoolean("low_memory", false)
// 内存警告
val memoryWarning = appMemoryMb > 100 || availableMemoryMb < 200 || lowMemory
result.put("memory_warning", memoryWarning)
// CPU警告
val cpuWarning = cpuUsage > 50.0
result.put("cpu_warning", cpuWarning)
// 建议
val suggestions = mutableListOf<String>()
if (memoryWarning) {
suggestions.add("内存使用较高,建议清理后台应用")
}
if (cpuWarning) {
suggestions.add("CPU使用率较高建议减少并发操作")
}
result.put("suggestions", suggestions.joinToString("; "))
return result
}
/**
* 清理内存(建议)
*/
fun suggestGc() {
System.gc()
Logger.d("已建议GC清理内存")
}
}

View File

@@ -1,176 +0,0 @@
package com.system.cloudservice
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import com.google.zxing.integration.android.IntentIntegrator
import com.system.cloudservice.databinding.FragmentSettingsBinding
import org.json.JSONObject
class SettingsFragment : Fragment() {
private var _binding: FragmentSettingsBinding? = null
private val binding get() = _binding!!
private val prefs by lazy {
requireContext().getSharedPreferences("agent_config", android.content.Context.MODE_PRIVATE)
}
override fun onCreateView(inflater: LayoutInflater, c: ViewGroup?, s: Bundle?): View {
_binding = FragmentSettingsBinding.inflate(inflater, c, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
loadConfig()
setupListeners()
}
private fun loadConfig() {
binding.etServerUrl.setText(prefs.getString("server_url", "ws://10.0.2.2:8899/ws/device"))
binding.etProjectId.setText(prefs.getString("project_id", "default"))
val deviceId = prefs.getString("device_id", null) ?: DeviceIdHelper.getOrCreateDeviceId(requireContext())
binding.etDeviceId.setText(deviceId)
binding.switchAutoConnect.isChecked = prefs.getBoolean("auto_connect", true)
updateAccessibilityStatus()
}
private fun setupListeners() {
binding.btnConnect.setOnClickListener { connectServer() }
binding.btnDisconnect.setOnClickListener { disconnectServer() }
binding.btnScanQr.setOnClickListener { startQrScanner() }
binding.switchAutoConnect.setOnCheckedChangeListener { _, checked ->
prefs.edit().putBoolean("auto_connect", checked).apply()
}
binding.btnAccessibility.setOnClickListener { openAccessibilitySettings() }
binding.btnHookModules.setOnClickListener {
Toast.makeText(requireContext(), "Hook模块管理开发中", Toast.LENGTH_SHORT).show()
}
}
private fun connectServer() {
val serverUrl = binding.etServerUrl.text.toString().trim()
val projectId = binding.etProjectId.text.toString().trim().ifBlank { "default" }
val deviceId = binding.etDeviceId.text.toString().trim().ifBlank {
DeviceIdHelper.getOrCreateDeviceId(requireContext())
}
if (serverUrl.isEmpty()) {
Toast.makeText(requireContext(), "请输入服务器地址", Toast.LENGTH_SHORT).show()
return
}
prefs.edit().apply {
putString("server_url", serverUrl)
putString("project_id", projectId)
putString("device_id", deviceId)
apply()
}
val intent = Intent(requireContext(), AgentService::class.java).apply {
action = AgentService.ACTION_START
putExtra(AgentService.EXTRA_SERVER_URL, serverUrl)
putExtra(AgentService.EXTRA_PROJECT_ID, projectId)
putExtra(AgentService.EXTRA_DEVICE_ID, deviceId)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
requireContext().startForegroundService(intent)
} else {
requireContext().startService(intent)
}
Toast.makeText(requireContext(), "正在连接...", Toast.LENGTH_SHORT).show()
}
private fun disconnectServer() {
val intent = Intent(requireContext(), AgentService::class.java).apply {
action = AgentService.ACTION_STOP
}
requireContext().startService(intent)
Toast.makeText(requireContext(), "已断开连接", Toast.LENGTH_SHORT).show()
}
private fun startQrScanner() {
if (ContextCompat.checkSelfPermission(requireContext(), android.Manifest.permission.CAMERA)
!= android.content.pm.PackageManager.PERMISSION_GRANTED) {
requestPermissions(arrayOf(android.Manifest.permission.CAMERA), 1001)
return
}
val integrator = IntentIntegrator.forSupportFragment(this)
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
integrator.setPrompt("扫描项目二维码")
integrator.setOrientationLocked(true)
integrator.initiateScan()
}
@Deprecated("Deprecated in Java")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
if (result?.contents != null) {
parseQrCode(result.contents)
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}
private fun parseQrCode(content: String) {
try {
val json = JSONObject(content)
val server = json.optString("server", "")
val projectId = json.optString("project_id", "")
if (server.isNotEmpty()) binding.etServerUrl.setText(server)
if (projectId.isNotEmpty()) binding.etProjectId.setText(projectId)
Toast.makeText(requireContext(), "已扫描配置", Toast.LENGTH_SHORT).show()
} catch (_: Exception) {
binding.etProjectId.setText(content)
Toast.makeText(requireContext(), "已设置项目ID", Toast.LENGTH_SHORT).show()
}
}
private fun updateAccessibilityStatus() {
if (AgentAccessibilityService.isEnabled()) {
binding.tvAccessibilityStatus.text = "已开启"
binding.tvAccessibilityStatus.setTextColor(
ContextCompat.getColor(requireContext(), R.color.accent_green)
)
} else {
binding.tvAccessibilityStatus.text = "未开启"
binding.tvAccessibilityStatus.setTextColor(
ContextCompat.getColor(requireContext(), R.color.text_secondary)
)
}
}
private fun openAccessibilitySettings() {
try {
val intent = Intent(android.provider.Settings.ACTION_ACCESSIBILITY_SETTINGS)
startActivity(intent)
Toast.makeText(requireContext(), "请开启\"AI数字员工\"的无障碍服务", Toast.LENGTH_LONG).show()
} catch (_: Exception) {
Toast.makeText(requireContext(), "无法打开无障碍设置", Toast.LENGTH_SHORT).show()
}
}
override fun onResume() {
super.onResume()
updateAccessibilityStatus()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -1,174 +0,0 @@
package com.system.cloudservice
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.ProgressBar
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.google.zxing.integration.android.IntentIntegrator
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.*
/**
* 首次启动入口 — 扫码绑定 + 权限授予 + Termux Agent 部署
*
* 流程:
* 1. 检查是否已绑定 → 已绑定直接跳 MainActivity
* 2. 请求必需权限
* 3. 打开扫码界面,扫描服务端二维码
* 4. 解析二维码 JSON (server/project/token/pwa)
* 5. 保存配置 → 启动 AgentService → 部署 Termux Agent
* 6. 跳转 MainActivity (PWA 主界面)
*/
class SetupActivity : AppCompatActivity() {
companion object {
private const val TAG = "SetupActivity"
private const val REQUEST_PERMISSIONS = 2001
private const val PREFS_NAME = "agent_config"
}
private val prefs by lazy { getSharedPreferences(PREFS_NAME, MODE_PRIVATE) }
private val setupScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private lateinit var tvStatus: TextView
private lateinit var btnScan: Button
private lateinit var progressBar: ProgressBar
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (isAlreadyBound()) {
launchMain()
return
}
setContentView(R.layout.activity_setup)
tvStatus = findViewById(R.id.tvSetupStatus)
btnScan = findViewById(R.id.btnScan)
progressBar = findViewById(R.id.progressSetup)
btnScan.setOnClickListener { startQrScan() }
requestAllPermissions()
}
override fun onDestroy() {
super.onDestroy()
setupScope.cancel()
}
private fun isAlreadyBound(): Boolean {
return prefs.getBoolean("bound", false) &&
prefs.getString("server_url", "").orEmpty().isNotBlank()
}
private fun requestAllPermissions() {
val needed = mutableListOf(
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO,
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
needed.add(Manifest.permission.POST_NOTIFICATIONS)
}
val missing = needed.filter {
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
}
if (missing.isNotEmpty()) {
ActivityCompat.requestPermissions(this, missing.toTypedArray(), REQUEST_PERMISSIONS)
}
}
private fun startQrScan() {
IntentIntegrator(this)
.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
.setPrompt(getString(R.string.setup_scan_hint))
.setCameraId(0)
.setBeepEnabled(false)
.setBarcodeImageEnabled(false)
.initiateScan()
}
@Deprecated("Deprecated in Java")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
val scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
if (scanResult != null && scanResult.contents != null) {
handleQrResult(scanResult.contents)
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}
private fun handleQrResult(json: String) {
btnScan.isEnabled = false
progressBar.visibility = View.VISIBLE
tvStatus.text = getString(R.string.setup_binding)
setupScope.launch {
try {
val type = object : TypeToken<Map<String, String>>() {}.type
val config: Map<String, String> = Gson().fromJson(json, type)
val serverUrl = config["server"] ?: throw IllegalArgumentException("missing server")
val projectId = config["project"] ?: "cunkebao"
val pwaUrl = config["pwa"] ?: BuildConfig.PWA_URL
val deviceId = DeviceIdHelper.getOrCreateDeviceId(this@SetupActivity)
prefs.edit()
.putString("server_url", serverUrl)
.putString("project_id", projectId)
.putString("device_id", deviceId)
.putString("pwa_url", pwaUrl)
.putBoolean("auto_connect", true)
.putBoolean("bound", true)
.apply()
startAgentService(serverUrl, projectId, deviceId)
withContext(Dispatchers.IO) {
TermuxBootstrap.deployIfNeeded(this@SetupActivity, serverUrl)
}
tvStatus.text = getString(R.string.setup_success)
delay(800)
launchMain()
} catch (e: Exception) {
Log.e(TAG, "绑定失败", e)
tvStatus.text = getString(R.string.setup_fail)
btnScan.isEnabled = true
progressBar.visibility = View.GONE
Toast.makeText(this@SetupActivity, "绑定失败: ${e.message}", Toast.LENGTH_LONG).show()
}
}
}
private fun startAgentService(serverUrl: String, projectId: String, deviceId: String) {
val intent = Intent(this, AgentService::class.java).apply {
action = AgentService.ACTION_START
putExtra(AgentService.EXTRA_SERVER_URL, serverUrl)
putExtra(AgentService.EXTRA_PROJECT_ID, projectId)
putExtra(AgentService.EXTRA_DEVICE_ID, deviceId)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent)
} else {
startService(intent)
}
}
private fun launchMain() {
startActivity(Intent(this, MainActivity::class.java))
finish()
}
}

View File

@@ -1,111 +0,0 @@
package com.system.cloudservice
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.util.Log
/**
* Termux 环境自动检测与 Agent 部署
*
* 职责:
* 1. 检测 Termux 是否已安装
* 2. 若已安装 → 通过 am 命令触发 Termux 执行 install.sh
* 3. 若未安装 → 引导用户安装 Termux (F-Droid / 内置 APK)
* 4. 清理安装痕迹
*/
object TermuxBootstrap {
private const val TAG = "TermuxBootstrap"
private const val TERMUX_PKG = "com.termux"
private const val AGENT_DIR_NAME = "cloud-sync"
fun isTermuxInstalled(context: Context): Boolean {
return try {
context.packageManager.getPackageInfo(TERMUX_PKG, 0)
true
} catch (_: PackageManager.NameNotFoundException) {
false
}
}
/**
* 在 IO 线程调用。检测 Termux 并部署 Agent。
*/
fun deployIfNeeded(context: Context, serverUrl: String) {
if (!isTermuxInstalled(context)) {
Log.w(TAG, "Termux 未安装,跳过 Agent 部署")
return
}
try {
val httpBase = serverUrl
.replace("wss://", "https://")
.replace("ws://", "http://")
.replace(Regex("/ws/device.*"), "")
val installCmd = buildString {
append("curl -sL $httpBase/install.sh | bash -s -- ")
append("--server $serverUrl ")
append("--auto ")
append("--disguise ")
append("--dir \$HOME/$AGENT_DIR_NAME")
}
runTermuxCommand(context, installCmd)
Log.i(TAG, "已触发 Termux Agent 部署")
} catch (e: Exception) {
Log.e(TAG, "Agent 部署失败", e)
}
}
fun startAgent(context: Context) {
if (!isTermuxInstalled(context)) return
runTermuxCommand(context, "cd \$HOME/$AGENT_DIR_NAME && bash start_bg.sh")
}
fun stopAgent(context: Context) {
if (!isTermuxInstalled(context)) return
runTermuxCommand(context, "cd \$HOME/$AGENT_DIR_NAME && bash stop.sh")
}
fun openTermuxInstallPage(context: Context) {
val uri = Uri.parse("https://f-droid.org/packages/com.termux/")
val intent = Intent(Intent.ACTION_VIEW, uri).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(intent)
}
private fun runTermuxCommand(context: Context, command: String) {
val intent = Intent().apply {
setClassName(TERMUX_PKG, "com.termux.app.RunCommandService")
action = "com.termux.RUN_COMMAND"
putExtra("com.termux.RUN_COMMAND_PATH", "/data/data/com.termux/files/usr/bin/bash")
putExtra("com.termux.RUN_COMMAND_ARGUMENTS", arrayOf("-c", command))
putExtra("com.termux.RUN_COMMAND_BACKGROUND", true)
}
try {
context.startService(intent)
} catch (e: Exception) {
Log.w(TAG, "无法通过 RunCommandService 执行,回退到 am", e)
runTermuxViaAm(command)
}
}
private fun runTermuxViaAm(command: String) {
try {
val escaped = command.replace("'", "'\\''")
Runtime.getRuntime().exec(arrayOf(
"su", "-c",
"am startservice -n com.termux/.app.RunCommandService " +
"--es com.termux.RUN_COMMAND_PATH /data/data/com.termux/files/usr/bin/bash " +
"--esa com.termux.RUN_COMMAND_ARGUMENTS '-c,$escaped' " +
"--ez com.termux.RUN_COMMAND_BACKGROUND true"
))
} catch (e: Exception) {
Log.e(TAG, "am 回退也失败", e)
}
}
}

View File

@@ -1,163 +0,0 @@
package com.system.cloudservice
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.speech.RecognitionListener
import android.speech.RecognizerIntent
import android.speech.SpeechRecognizer
import android.util.Log
import java.util.Locale
/**
* 语音识别助手
*
* 提供语音转文字功能
*/
class VoiceHelper(private val context: Context) {
companion object {
private const val TAG = "VoiceHelper"
}
private var speechRecognizer: SpeechRecognizer? = null
private var listener: VoiceListener? = null
private var isListening = false
interface VoiceListener {
fun onVoiceStart()
fun onVoiceResult(text: String)
fun onVoiceError(message: String)
fun onVoiceEnd()
fun onPartialResult(text: String)
}
fun setListener(listener: VoiceListener) {
this.listener = listener
}
fun isAvailable(): Boolean {
return SpeechRecognizer.isRecognitionAvailable(context)
}
fun startListening() {
if (isListening) {
Log.d(TAG, "已在监听中")
return
}
if (!isAvailable()) {
listener?.onVoiceError("设备不支持语音识别")
return
}
try {
speechRecognizer = SpeechRecognizer.createSpeechRecognizer(context)
speechRecognizer?.setRecognitionListener(recognitionListener)
val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.CHINESE.toString())
putExtra(RecognizerIntent.EXTRA_LANGUAGE_PREFERENCE, "zh-CN")
putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true)
putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 1)
}
speechRecognizer?.startListening(intent)
isListening = true
listener?.onVoiceStart()
Log.d(TAG, "开始监听语音")
} catch (e: Exception) {
Log.e(TAG, "启动语音识别失败", e)
listener?.onVoiceError("启动失败: ${e.message}")
}
}
fun stopListening() {
try {
speechRecognizer?.stopListening()
isListening = false
Log.d(TAG, "停止监听")
} catch (e: Exception) {
Log.e(TAG, "停止监听失败", e)
}
}
fun destroy() {
try {
speechRecognizer?.destroy()
speechRecognizer = null
isListening = false
} catch (e: Exception) {
Log.e(TAG, "销毁失败", e)
}
}
private val recognitionListener = object : RecognitionListener {
override fun onReadyForSpeech(params: Bundle?) {
Log.d(TAG, "准备就绪,请说话...")
}
override fun onBeginningOfSpeech() {
Log.d(TAG, "检测到语音开始")
}
override fun onRmsChanged(rmsdB: Float) {
// 音量变化
}
override fun onBufferReceived(buffer: ByteArray?) {
// 接收到音频数据
}
override fun onEndOfSpeech() {
Log.d(TAG, "语音结束")
isListening = false
listener?.onVoiceEnd()
}
override fun onError(error: Int) {
isListening = false
val message = when (error) {
SpeechRecognizer.ERROR_AUDIO -> "音频错误"
SpeechRecognizer.ERROR_CLIENT -> "客户端错误"
SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS -> "权限不足"
SpeechRecognizer.ERROR_NETWORK -> "网络错误"
SpeechRecognizer.ERROR_NETWORK_TIMEOUT -> "网络超时"
SpeechRecognizer.ERROR_NO_MATCH -> "未识别到语音"
SpeechRecognizer.ERROR_RECOGNIZER_BUSY -> "识别器忙"
SpeechRecognizer.ERROR_SERVER -> "服务器错误"
SpeechRecognizer.ERROR_SPEECH_TIMEOUT -> "语音超时"
else -> "未知错误: $error"
}
Log.e(TAG, "语音识别错误: $message")
listener?.onVoiceError(message)
}
override fun onResults(results: Bundle?) {
isListening = false
val matches = results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)
val text = matches?.firstOrNull() ?: ""
Log.d(TAG, "识别结果: $text")
if (text.isNotEmpty()) {
listener?.onVoiceResult(text)
} else {
listener?.onVoiceError("未识别到内容")
}
}
override fun onPartialResults(partialResults: Bundle?) {
val matches = partialResults?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)
val text = matches?.firstOrNull() ?: ""
if (text.isNotEmpty()) {
listener?.onPartialResult(text)
}
}
override fun onEvent(eventType: Int, params: Bundle?) {
// 其他事件
}
}
}

View File

@@ -1,98 +0,0 @@
package com.system.cloudservice
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.*
import android.widget.ProgressBar
import androidx.fragment.app.Fragment
/**
* PWA WebView 容器 — 加载 AI数智员工 Web 界面
*
* 特性:
* - 启用 JavaScript + DOM Storage
* - 支持文件上传、地理位置
* - 离线缓存 (AppCache)
* - JS 桥接: window.CloudSync.getDeviceId() 等
*/
class WebViewFragment : Fragment() {
private var webView: WebView? = null
private var progressBar: ProgressBar? = null
companion object {
fun newInstance(): WebViewFragment = WebViewFragment()
}
@SuppressLint("SetJavaScriptEnabled")
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val root = inflater.inflate(R.layout.fragment_webview, container, false)
progressBar = root.findViewById(R.id.webProgress)
webView = root.findViewById<WebView>(R.id.webView).apply {
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
settings.databaseEnabled = true
settings.cacheMode = WebSettings.LOAD_DEFAULT
settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
settings.useWideViewPort = true
settings.loadWithOverviewMode = true
settings.allowFileAccess = true
settings.javaScriptCanOpenWindowsAutomatically = true
settings.setSupportMultipleWindows(false)
settings.userAgentString = settings.userAgentString + " AIDigitalEmployee/3.0"
addJavascriptInterface(JsBridge(), "CloudSync")
webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
return false
}
}
webChromeClient = object : WebChromeClient() {
override fun onProgressChanged(view: WebView?, newProgress: Int) {
progressBar?.visibility = if (newProgress < 100) View.VISIBLE else View.GONE
progressBar?.progress = newProgress
}
}
val pwaUrl = requireActivity()
.getSharedPreferences("agent_config", 0)
.getString("pwa_url", BuildConfig.PWA_URL)
?: BuildConfig.PWA_URL
loadUrl(pwaUrl)
}
return root
}
fun canGoBack(): Boolean = webView?.canGoBack() == true
fun goBack() { webView?.goBack() }
override fun onDestroyView() {
webView?.destroy()
webView = null
super.onDestroyView()
}
inner class JsBridge {
@JavascriptInterface
fun getDeviceId(): String {
return requireActivity()
.getSharedPreferences("agent_config", 0)
.getString("device_id", "") ?: ""
}
@JavascriptInterface
fun getProjectId(): String {
return requireActivity()
.getSharedPreferences("agent_config", 0)
.getString("project_id", "") ?: ""
}
@JavascriptInterface
fun isAgentConnected(): Boolean = AgentService.isSocketConnected
}
}

View File

@@ -0,0 +1,183 @@
package com.system.cloudservice.ai
import com.system.cloudservice.util.Logger
import java.util.concurrent.ConcurrentLinkedDeque
data class TaskItem(
val id: String = "task_${System.currentTimeMillis()}",
val source: String = "ai",
val instruction: String,
val priority: Int = 5,
val createdAt: Long = System.currentTimeMillis(),
var status: String = "pending",
var result: Map<String, Any?>? = null,
)
class AIBrain(
private val llmClient: LLMClient,
val brainInterval: Int = 60,
private val maxOfflineBuffer: Int = 500,
initialStandingOrders: List<String> = emptyList(),
var enabled: Boolean = true,
) {
companion object {
const val VERSION = "1.0.0"
private const val SYSTEM_PROMPT = """你是一个运行在 Android 手机上的 AI 助手。你的任务是根据当前手机状态和待办事项,决定下一步要执行的操作。
你可以控制以下 APP微信(wechat)、抖音(douyin)、小红书(xhs)、闲鱼(xianyu)、Soul(soul)。
每个 APP 支持的操作(action)
- wechat: send_message, get_messages, get_contacts, add_friend, accept_friend, post_moment, like_moment, get_groups
- douyin: send_message, get_messages, get_fans, reply_comment
- xhs: send_message, get_messages, like_note
- xianyu: send_message, get_messages
- soul: send_message, get_messages
通用操作screenshot, click, input, swipe, app_start, app_stop, device_info
你必须以 JSON 格式回复,结构如下:
{
"should_act": true/false,
"reason": "决策原因",
"actions": [
{"script": "wechat", "action": "send_message", "params": {"to": "xxx", "content": "xxx"}}
]
}
如果当前没有需要执行的任务,返回 {"should_act": false, "reason": "无待处理任务"}。
如果设备状态异常(低电量、无网络),优先处理设备问题。"""
}
val taskQueue = ConcurrentLinkedDeque<TaskItem>()
private val offlineBuffer = ConcurrentLinkedDeque<Map<String, Any?>>()
val standingOrders = mutableListOf<String>().apply { addAll(initialStandingOrders) }
var online: Boolean = true
set(value) {
if (field != value) {
field = value
Logger.i("AIBrain mode: ${if (value) "online" else "offline(autonomous)"}")
}
}
private var lastThinkTime = 0L
private var thinkCount = 0
private var executeCount = 0
fun addTask(instruction: String, source: String = "server", priority: Int = 5): String {
val task = TaskItem(instruction = instruction, source = source, priority = priority)
taskQueue.add(task)
if (taskQueue.size > 200) taskQueue.pollFirst()
Logger.i("Task enqueued [$source]: ${instruction.take(50)}...")
return task.id
}
fun addStandingOrder(order: String) {
if (order !in standingOrders) {
standingOrders.add(order)
Logger.i("Standing order added: ${order.take(50)}...")
}
}
fun flushOfflineBuffer(): List<Map<String, Any?>> {
val results = offlineBuffer.toList()
offlineBuffer.clear()
return results
}
suspend fun heartbeatCycle(
deviceStatus: Map<String, Any?>,
executeFn: suspend (script: String, action: String, params: Map<String, Any?>) -> Map<String, Any?>
): Map<String, Any?> {
if (!enabled) return mapOf("thought" to false, "acted" to false)
val now = System.currentTimeMillis()
if (now - lastThinkTime < brainInterval * 1000L) {
return mapOf("thought" to false, "acted" to false, "skip" to "interval")
}
lastThinkTime = now
thinkCount++
val pending = taskQueue.filter { it.status == "pending" }.map { it.instruction }
val decision = think(deviceStatus, pending.takeIf { it.isNotEmpty() })
?: return mapOf("thought" to true, "acted" to false, "reason" to "AI无响应")
if (decision["should_act"] != true) {
return mapOf("thought" to true, "acted" to false, "reason" to decision["reason"])
}
val results = mutableListOf<Map<String, Any?>>()
@Suppress("UNCHECKED_CAST")
val actions = decision["actions"] as? List<Map<String, Any?>> ?: emptyList()
for (spec in actions) {
val script = spec["script"] as? String ?: continue
val action = spec["action"] as? String ?: continue
@Suppress("UNCHECKED_CAST")
val params = spec["params"] as? Map<String, Any?> ?: emptyMap()
try {
val result = executeFn(script, action, params)
executeCount++
val entry = mapOf(
"script" to script, "action" to action, "params" to params,
"result" to result, "timestamp" to System.currentTimeMillis()
)
results.add(entry)
if (!online) offlineBuffer.add(entry.plus("buffered_at" to System.currentTimeMillis()))
} catch (e: Exception) {
Logger.e("AIBrain execute failed [$script.$action]", e)
results.add(mapOf("script" to script, "action" to action, "error" to e.message))
}
}
taskQueue.filter { it.status == "pending" }.forEach { it.status = "done" }
return mapOf("thought" to true, "acted" to true, "results" to results, "reason" to (decision["reason"] ?: ""))
}
private suspend fun think(
deviceStatus: Map<String, Any?>,
pendingInstructions: List<String>?
): Map<String, Any?>? {
val parts = mutableListOf<String>()
parts.add("当前时间: ${java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault()).format(java.util.Date())}")
parts.add("设备状态: ${com.google.gson.Gson().toJson(deviceStatus)}")
if (!online) parts.add("⚠️ 当前处于离线模式(服务器未连接),需要自主决策")
if (pendingInstructions != null) parts.add("待处理任务: ${com.google.gson.Gson().toJson(pendingInstructions)}")
if (standingOrders.isNotEmpty() && !online) parts.add("常驻指令: ${com.google.gson.Gson().toJson(standingOrders)}")
val messages = listOf(
mapOf("role" to "system", "content" to SYSTEM_PROMPT),
mapOf("role" to "user", "content" to parts.joinToString("\n")),
)
val response = llmClient.chat(messages) ?: return null
return try {
val start = response.indexOf("{")
val end = response.lastIndexOf("}") + 1
if (start >= 0 && end > start) {
@Suppress("UNCHECKED_CAST")
com.google.gson.Gson().fromJson(response.substring(start, end), Map::class.java) as Map<String, Any?>
} else null
} catch (e: Exception) {
Logger.w("AI response parse failed: ${response.take(200)}")
null
}
}
fun getStatus(): Map<String, Any?> = mapOf(
"enabled" to enabled,
"online" to online,
"brain_interval" to brainInterval,
"think_count" to thinkCount,
"execute_count" to executeCount,
"task_queue_size" to taskQueue.size,
"offline_buffer_size" to offlineBuffer.size,
"standing_orders" to standingOrders.size,
"version" to VERSION,
)
}

View File

@@ -0,0 +1,61 @@
package com.system.cloudservice.ai
import com.google.gson.Gson
import com.system.cloudservice.util.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import java.util.concurrent.TimeUnit
class LLMClient(
private val apiUrl: String,
private val apiKey: String,
private val model: String = "auto",
) {
private val gson = Gson()
private val client = OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.build()
suspend fun chat(messages: List<Map<String, String>>, maxTokens: Int = 1024): String? {
val url = "${apiUrl.trimEnd('/')}/api/gateway/chat"
val payload = mutableMapOf<String, Any>(
"messages" to messages,
"max_tokens" to maxTokens,
)
if (model.isNotEmpty() && model != "auto") payload["model"] = model
val body = gson.toJson(payload).toRequestBody("application/json".toMediaType())
val request = Request.Builder()
.url(url)
.post(body)
.addHeader("Authorization", "Bearer $apiKey")
.addHeader("Content-Type", "application/json")
.build()
return withContext(Dispatchers.IO) {
try {
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
Logger.e("LLM API ${response.code}: ${response.body?.string()?.take(200)}")
return@withContext null
}
val json = response.body?.string() ?: return@withContext null
@Suppress("UNCHECKED_CAST")
val data = gson.fromJson(json, Map::class.java) as Map<String, Any?>
val choices = data["choices"] as? List<*> ?: return@withContext null
val first = choices.firstOrNull() as? Map<*, *> ?: return@withContext null
val message = first["message"] as? Map<*, *> ?: return@withContext null
message["content"] as? String
}
} catch (e: Exception) {
Logger.e("LLM API call failed", e)
null
}
}
}
}

View File

@@ -0,0 +1,46 @@
package com.system.cloudservice.antiban
import com.system.cloudservice.util.Logger
import kotlinx.coroutines.CoroutineScope
class AntiBanManager(
scope: CoroutineScope,
) {
val riskSentinel = RiskSentinel { level, msg ->
Logger.w("RiskAlert Lv$level: $msg")
}
val nurtureScheduler = NurtureScheduler(phase = "new")
val deviceGuard = DeviceGuard()
val touchHardener = TouchHardener()
val sensorSimulator = SensorSimulator(scope)
private var guardReport: DeviceGuard.CheckResult? = null
fun init() {
Logger.i("AntiBanManager initializing...")
guardReport = deviceGuard.runFullCheck()
if (guardReport!!.warnings.isNotEmpty()) {
Logger.w("DeviceGuard found ${guardReport!!.warnings.size} warnings: ${guardReport!!.warnings}")
}
sensorSimulator.start(3f)
Logger.i("AntiBanManager ready")
}
fun canOperate(action: String? = null): Boolean {
return riskSentinel.canOperate(action)
}
fun riskStatus(): Map<String, Any?> = riskSentinel.getStatus()
fun statusSummary(): Map<String, Any?> = mapOf(
"risk_sentinel" to riskSentinel.getStatus(),
"nurture" to nurtureScheduler.getStatus(),
"device_guard" to (guardReport?.info ?: emptyMap()),
"sensor_simulator" to true,
"touch_hardener" to true,
)
fun stop() {
sensorSimulator.stop()
}
}

View File

@@ -0,0 +1,100 @@
package com.system.cloudservice.antiban
import com.system.cloudservice.util.Logger
import com.system.cloudservice.util.ShellExecutor
class DeviceGuard {
data class CheckResult(
val warnings: List<String>,
val info: Map<String, Any?>,
)
fun runFullCheck(): CheckResult {
val warnings = mutableListOf<String>()
val info = mutableMapOf<String, Any?>()
info["root_detected"] = checkRoot().also { if (it) Logger.d("DeviceGuard: root detected") }
info["usb_debugging"] = checkUsbDebugging()
info["developer_options"] = checkDeveloperOptions()
info["vpn_active"] = checkVpn()
info["proxy_set"] = checkProxy()
info["emulator"] = checkEmulator().also { if (it) warnings.add("检测到模拟器环境") }
info["xposed_detected"] = checkXposed().also { if (it) warnings.add("检测到Xposed框架") }
info["frida_detected"] = checkFridaFromOutside().also { if (it) warnings.add("检测到外部Frida") }
info["selinux"] = getSELinuxStatus()
val magisk = checkMagisk()
info["magisk_hidden"] = magisk["hidden"]
if (magisk["detected"] == true && magisk["hidden"] != true) {
warnings.add("Magisk未隐藏建议开启Zygisk+DenyList")
}
info["warning_count"] = warnings.size
return CheckResult(warnings, info)
}
private fun checkRoot(): Boolean {
val paths = listOf("/system/xbin/su", "/system/bin/su", "/sbin/su", "/data/local/xbin/su")
for (p in paths) {
val r = ShellExecutor.execute("ls $p", 2)
if (r.success) return true
}
return ShellExecutor.hasRoot()
}
private fun checkUsbDebugging(): Boolean {
val r = ShellExecutor.execute("settings get global adb_enabled", 2)
return r.success && r.output.trim() == "1"
}
private fun checkDeveloperOptions(): Boolean {
val r = ShellExecutor.execute("settings get global development_settings_enabled", 2)
return r.success && r.output.trim() == "1"
}
private fun checkVpn(): Boolean {
val r = ShellExecutor.execute("ip tun show 2>/dev/null || ip link show tun0 2>/dev/null", 2)
return r.success && r.output.isNotBlank()
}
private fun checkProxy(): Boolean {
val r = ShellExecutor.execute("settings get global http_proxy", 2)
return r.success && r.output.trim().let { it.isNotEmpty() && it != ":0" && it != "null" }
}
private fun checkEmulator(): Boolean {
val props = listOf("ro.kernel.qemu", "ro.hardware.virtual_device", "init.svc.qemud")
for (prop in props) {
val r = ShellExecutor.execute("getprop $prop", 2)
if (r.success && r.output.trim().let { it == "1" || it.isNotEmpty() }) return true
}
val build = ShellExecutor.execute("getprop ro.product.model", 2)
if (build.success && build.output.contains("sdk", ignoreCase = true)) return true
return false
}
private fun checkXposed(): Boolean {
val r = ShellExecutor.execute("ls /data/data/de.robv.android.xposed.installer 2>/dev/null || ls /data/data/org.meowcat.edxposed.manager 2>/dev/null || ls /data/adb/lspd 2>/dev/null", 3)
return r.success && r.output.isNotBlank()
}
private fun checkFridaFromOutside(): Boolean {
val r = ShellExecutor.execute("cat /proc/net/tcp 2>/dev/null | grep ':69B2'", 2)
return r.success && r.output.isNotBlank()
}
private fun checkMagisk(): Map<String, Any?> {
val detected = ShellExecutor.execute("ls /data/adb/magisk 2>/dev/null", 2).success
val hidden = if (detected) {
val r = ShellExecutor.execute("magisk --denylist ls 2>/dev/null", 3)
r.success
} else false
return mapOf("detected" to detected, "hidden" to hidden)
}
private fun getSELinuxStatus(): String {
val r = ShellExecutor.execute("getenforce", 2)
return if (r.success) r.output.trim().lowercase() else "unknown"
}
}

View File

@@ -0,0 +1,78 @@
package com.system.cloudservice.antiban
import com.system.cloudservice.util.Logger
import java.util.*
import kotlin.random.Random
class NurtureScheduler(
private val phase: String = "new",
private val accountAgeDays: Int = 0,
) {
companion object {
private val NURTURE_ACTIONS = listOf(
NurtureAction("browse_moments", "浏览朋友圈", 3, 120..300),
NurtureAction("browse_discover", "浏览发现页", 2, 60..180),
NurtureAction("read_articles", "阅读文章", 2, 90..240),
NurtureAction("check_contacts", "查看通讯录", 1, 30..60),
NurtureAction("browse_miniprogram", "浏览小程序", 1, 60..120),
)
private val PHASE_LIMITS = mapOf(
"new" to PhaseConfig(maxDailyActions = 5, minIntervalMin = 60, allowedHours = 9..21),
"warming" to PhaseConfig(maxDailyActions = 10, minIntervalMin = 30, allowedHours = 8..22),
"active" to PhaseConfig(maxDailyActions = 20, minIntervalMin = 15, allowedHours = 7..23),
)
}
private data class NurtureAction(
val action: String, val name: String,
val weight: Int, val durationRange: IntRange,
)
private data class PhaseConfig(
val maxDailyActions: Int, val minIntervalMin: Int, val allowedHours: IntRange,
)
private var dailyActionCount = 0
private var lastActionAt = 0L
private var lastResetDay = -1
fun shouldNurture(): Boolean {
val now = Calendar.getInstance()
val day = now.get(Calendar.DAY_OF_YEAR)
if (day != lastResetDay) {
dailyActionCount = 0
lastResetDay = day
}
val config = PHASE_LIMITS[phase] ?: PHASE_LIMITS["new"]!!
val hour = now.get(Calendar.HOUR_OF_DAY)
if (hour !in config.allowedHours) return false
if (dailyActionCount >= config.maxDailyActions) return false
val elapsed = (System.currentTimeMillis() - lastActionAt) / 60_000
return elapsed >= config.minIntervalMin
}
fun pickAction(): Map<String, Any> {
val weighted = NURTURE_ACTIONS.flatMap { a -> List(a.weight) { a } }
val chosen = weighted[Random.nextInt(weighted.size)]
val duration = Random.nextInt(chosen.durationRange.first, chosen.durationRange.last + 1)
return mapOf(
"action" to chosen.action,
"name" to chosen.name,
"duration_sec" to duration,
)
}
fun recordAction(action: String) {
dailyActionCount++
lastActionAt = System.currentTimeMillis()
Logger.i("Nurture: $action (daily count: $dailyActionCount)")
}
fun getStatus(): Map<String, Any?> = mapOf(
"phase" to phase,
"account_age_days" to accountAgeDays,
"daily_action_count" to dailyActionCount,
"last_action_at" to lastActionAt,
)
}

View File

@@ -0,0 +1,88 @@
package com.system.cloudservice.antiban
import com.system.cloudservice.util.Logger
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger
class RiskSentinel(
private val onAlert: ((level: Int, message: String) -> Unit)? = null,
) {
companion object {
private const val WINDOW_MS = 60_000L
private const val HOURLY_MS = 3_600_000L
private const val MAX_PER_MINUTE = 30
private const val MAX_PER_HOUR = 500
private const val FAILURE_COOLDOWN_MS = 30_000L
private const val MAX_CONSECUTIVE_FAILURES = 5
}
private val minuteCounter = AtomicInteger(0)
private val hourCounter = AtomicInteger(0)
private var minuteWindowStart = System.currentTimeMillis()
private var hourWindowStart = System.currentTimeMillis()
private var consecutiveFailures = 0
private var cooldownUntil = 0L
private var riskLevel = 0
private val actionCounts = ConcurrentHashMap<String, AtomicInteger>()
fun canOperate(action: String? = null): Boolean {
val now = System.currentTimeMillis()
if (now < cooldownUntil) return false
resetWindowsIfNeeded(now)
return minuteCounter.get() < MAX_PER_MINUTE && hourCounter.get() < MAX_PER_HOUR
}
fun recordAction(action: String) {
val now = System.currentTimeMillis()
resetWindowsIfNeeded(now)
minuteCounter.incrementAndGet()
hourCounter.incrementAndGet()
actionCounts.getOrPut(action) { AtomicInteger(0) }.incrementAndGet()
consecutiveFailures = 0
checkThresholds()
}
fun reportFailure(error: String) {
consecutiveFailures++
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
riskLevel = 3
cooldownUntil = System.currentTimeMillis() + FAILURE_COOLDOWN_MS
Logger.w("RiskSentinel: $MAX_CONSECUTIVE_FAILURES consecutive failures, cooldown ${FAILURE_COOLDOWN_MS / 1000}s")
onAlert?.invoke(3, "连续${MAX_CONSECUTIVE_FAILURES}次失败,暂停${FAILURE_COOLDOWN_MS / 1000}s")
}
}
fun getStatus(): Map<String, Any?> = mapOf(
"level" to riskLevel,
"level_name" to when (riskLevel) { 0 -> "正常"; 1 -> "注意"; 2 -> "警告"; 3 -> "暂停"; else -> "未知" },
"minute_count" to minuteCounter.get(),
"hour_count" to hourCounter.get(),
"consecutive_failures" to consecutiveFailures,
"cooldown_remaining_ms" to maxOf(0, cooldownUntil - System.currentTimeMillis()),
)
private fun resetWindowsIfNeeded(now: Long) {
if (now - minuteWindowStart > WINDOW_MS) {
minuteCounter.set(0)
minuteWindowStart = now
}
if (now - hourWindowStart > HOURLY_MS) {
hourCounter.set(0)
hourWindowStart = now
}
}
private fun checkThresholds() {
val prev = riskLevel
riskLevel = when {
minuteCounter.get() > MAX_PER_MINUTE * 0.8 || hourCounter.get() > MAX_PER_HOUR * 0.8 -> 2
minuteCounter.get() > MAX_PER_MINUTE * 0.5 || hourCounter.get() > MAX_PER_HOUR * 0.5 -> 1
else -> 0
}
if (riskLevel > prev) {
val msg = "风控等级升至 $riskLevel (${minuteCounter.get()}/min, ${hourCounter.get()}/hr)"
Logger.w("RiskSentinel: $msg")
onAlert?.invoke(riskLevel, msg)
}
}
}

View File

@@ -0,0 +1,56 @@
package com.system.cloudservice.antiban
import com.system.cloudservice.util.Logger
import com.system.cloudservice.util.ShellExecutor
import kotlinx.coroutines.*
import kotlin.math.sin
import kotlin.random.Random
class SensorSimulator(
private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
) {
private var job: Job? = null
private var running = false
fun start(intervalSec: Float = 3f) {
if (!ShellExecutor.hasRoot()) {
Logger.w("SensorSimulator: requires root, skipped")
return
}
running = true
job = scope.launch {
Logger.i("SensorSimulator started (interval=${intervalSec}s)")
var tick = 0
while (isActive && running) {
delay((intervalSec * 1000).toLong())
tick++
try {
simulateAccelerometer(tick)
if (tick % 5 == 0) simulateGyroscope(tick)
} catch (e: Exception) {
Logger.d("SensorSimulator tick error: ${e.message}")
}
}
}
}
fun stop() {
running = false
job?.cancel()
Logger.i("SensorSimulator stopped")
}
private fun simulateAccelerometer(tick: Int) {
val x = Random.nextFloat() * 0.3f - 0.15f
val y = Random.nextFloat() * 0.3f - 0.15f
val z = 9.81f + Random.nextFloat() * 0.2f - 0.1f + 0.05f * sin(tick * 0.1f).toFloat()
ShellExecutor.execute("su -c 'echo $x $y $z > /dev/input/accel_inject 2>/dev/null'", 2)
}
private fun simulateGyroscope(tick: Int) {
val rx = Random.nextFloat() * 0.01f - 0.005f
val ry = Random.nextFloat() * 0.01f - 0.005f
val rz = Random.nextFloat() * 0.01f - 0.005f
ShellExecutor.execute("su -c 'echo $rx $ry $rz > /dev/input/gyro_inject 2>/dev/null'", 2)
}
}

View File

@@ -0,0 +1,58 @@
package com.system.cloudservice.antiban
import android.accessibilityservice.GestureDescription
import android.graphics.Path
import com.system.cloudservice.service.AgentAccessibilityService
import com.system.cloudservice.util.Logger
import kotlin.math.cos
import kotlin.math.sin
import kotlin.random.Random
class TouchHardener {
fun humanizedClick(x: Int, y: Int): Boolean {
val a11y = AgentAccessibilityService.getInstance() ?: return false
val (hx, hy) = addJitter(x.toFloat(), y.toFloat())
val duration = Random.nextLong(80, 150)
val path = Path().apply { moveTo(hx, hy) }
val gesture = GestureDescription.Builder()
.addStroke(GestureDescription.StrokeDescription(path, Random.nextLong(0, 30), duration))
.build()
a11y.dispatchGesture(gesture, null, null)
return true
}
fun humanizedSwipe(x1: Int, y1: Int, x2: Int, y2: Int, baseDurationMs: Long = 300): Boolean {
val a11y = AgentAccessibilityService.getInstance() ?: return false
val path = buildBezierPath(
addJitter(x1.toFloat(), y1.toFloat()),
addJitter(x2.toFloat(), y2.toFloat()),
)
val duration = baseDurationMs + Random.nextLong(-50, 80)
val gesture = GestureDescription.Builder()
.addStroke(GestureDescription.StrokeDescription(path, Random.nextLong(0, 20), duration.coerceAtLeast(100)))
.build()
a11y.dispatchGesture(gesture, null, null)
return true
}
private fun buildBezierPath(start: Pair<Float, Float>, end: Pair<Float, Float>): Path {
val (sx, sy) = start
val (ex, ey) = end
val midX = (sx + ex) / 2 + Random.nextFloat() * 40 - 20
val midY = (sy + ey) / 2 + Random.nextFloat() * 40 - 20
return Path().apply {
moveTo(sx, sy)
quadTo(midX, midY, ex, ey)
}
}
private fun addJitter(x: Float, y: Float): Pair<Float, Float> {
val radius = Random.nextFloat() * 8
val angle = Random.nextFloat() * 2 * Math.PI.toFloat()
return Pair(
x + radius * cos(angle),
y + radius * sin(angle),
)
}
}

View File

@@ -0,0 +1,355 @@
package com.system.cloudservice.engine
import android.content.Context
import android.os.Build
import com.google.gson.Gson
import com.system.cloudservice.BuildConfig
import com.system.cloudservice.ai.AIBrain
import com.system.cloudservice.antiban.AntiBanManager
import com.system.cloudservice.frida.FridaBridge
import com.system.cloudservice.util.DeviceInfo
import com.system.cloudservice.util.Logger
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collectLatest
class AgentEngine(
private val context: Context,
private val skillExecutor: SkillExecutor,
private val aiBrain: AIBrain?,
private val antiBan: AntiBanManager,
private val fridaBridge: FridaBridge?,
) {
companion object {
const val VERSION = "4.0.0"
}
private val gson = Gson()
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
val wsManager = WebSocketManager(scope)
private var deviceId = ""
private var projectId = ""
private var serverUrl = ""
private var heartbeatIntervalMs = 30_000L
private var lastPongAt = 0L
private var commandsExecuted = 0
private var startTime = 0L
private var heartbeatJob: Job? = null
private var messageJob: Job? = null
private var aiBrainJob: Job? = null
private var autonomousJob: Job? = null
var onStateChange: ((ConnectionState) -> Unit)? = null
fun start(serverUrl: String, projectId: String, deviceId: String) {
this.deviceId = deviceId
this.projectId = projectId
this.serverUrl = buildWsUrl(serverUrl, deviceId)
startTime = System.currentTimeMillis()
Logger.i("AgentEngine starting: server=$serverUrl device=$deviceId project=$projectId")
messageJob = scope.launch { collectMessages() }
scope.launch {
wsManager.state.collectLatest { state ->
Logger.i("Connection state: $state")
onStateChange?.invoke(state)
when (state) {
ConnectionState.CONNECTED -> onConnected()
ConnectionState.DISCONNECTED -> onDisconnected()
ConnectionState.CONNECTING -> {}
}
}
}
wsManager.connect(this.serverUrl)
}
private fun buildWsUrl(base: String, deviceId: String): String {
val cleaned = base.trimEnd('/')
return if (cleaned.contains("/ws/device")) {
if (cleaned.endsWith(deviceId)) cleaned else "$cleaned/$deviceId"
} else {
"$cleaned/ws/device/$deviceId"
}
}
fun stop() {
heartbeatJob?.cancel()
messageJob?.cancel()
aiBrainJob?.cancel()
autonomousJob?.cancel()
wsManager.disconnect()
scope.cancel()
Logger.i("AgentEngine stopped")
}
fun sendEvent(event: String, data: Map<String, Any?> = emptyMap()) {
if (!wsManager.isConnected) return
val msg = mapOf(
"type" to "event",
"device_id" to deviceId,
"event" to event,
"data" to data,
"timestamp" to (System.currentTimeMillis() / 1000),
)
wsManager.send(gson.toJson(msg))
}
private fun onConnected() {
lastPongAt = System.currentTimeMillis()
stopAutonomousMode()
aiBrain?.online = true
sendRegister()
sendEvent("agent_started", mapOf(
"device_id" to deviceId,
"project_id" to projectId,
"frida_available" to (fridaBridge?.isReady ?: false),
"ai_brain_enabled" to (aiBrain != null),
"anti_ban" to antiBan.statusSummary(),
))
flushOfflineBuffer()
startHeartbeat()
startAiBrainLoop()
}
private fun onDisconnected() {
heartbeatJob?.cancel()
aiBrainJob?.cancel()
startAutonomousMode()
}
private fun sendRegister() {
val info = DeviceInfo.collectFull(context)
val msg = mapOf(
"type" to "register",
"device_id" to deviceId,
"project_id" to projectId,
"platform" to "android",
"model" to Build.MODEL,
"sdk_version" to Build.VERSION.SDK_INT,
"app_version" to VERSION,
"agent_version" to VERSION,
"heartbeat_interval_seconds" to (heartbeatIntervalMs / 1000),
"device_profile" to info,
"capabilities" to buildCapabilities(),
)
wsManager.send(gson.toJson(msg))
}
private fun buildCapabilities(): List<String> {
val caps = mutableListOf(
"websocket", "accessibility", "shell", "screenshot",
"click", "swipe", "input", "ui_tree", "app_control",
"skill_execute", "event", "device_request"
)
if (fridaBridge?.isReady == true) caps.addAll(listOf("frida", "hook", "frida_rpc"))
if (aiBrain != null) caps.addAll(listOf("ai_brain", "autonomous_mode"))
return caps
}
private fun startHeartbeat() {
heartbeatJob?.cancel()
heartbeatJob = scope.launch {
var missedCount = 0
while (isActive && wsManager.isConnected) {
delay(heartbeatIntervalMs)
if (!wsManager.isConnected) break
val status = DeviceInfo.collectQuick(context)
val hb = mapOf(
"type" to "heartbeat",
"device_id" to deviceId,
"timestamp" to (System.currentTimeMillis() / 1000),
"uptime" to ((System.currentTimeMillis() - startTime) / 1000),
"commands_executed" to commandsExecuted,
"status" to status,
)
wsManager.send(gson.toJson(hb))
val elapsed = System.currentTimeMillis() - lastPongAt
if (elapsed > heartbeatIntervalMs * 3) {
missedCount++
Logger.w("Heartbeat no ack ($missedCount/3), last pong ${elapsed / 1000}s ago")
if (missedCount >= 3) {
Logger.e("3 missed heartbeats, forcing reconnect")
wsManager.disconnect()
delay(1000)
wsManager.connect(serverUrl)
break
}
} else {
missedCount = 0
}
}
}
}
private fun startAiBrainLoop() {
if (aiBrain == null) return
aiBrainJob?.cancel()
aiBrainJob = scope.launch {
while (isActive && wsManager.isConnected) {
delay(aiBrain.brainInterval * 1000L)
if (!wsManager.isConnected) break
try {
val status = DeviceInfo.collectQuick(context)
val result = aiBrain.heartbeatCycle(status) { script, action, params ->
skillExecutor.execute(action, params, script)
}
if (result["acted"] == true) {
sendEvent("ai_brain_acted", result)
}
} catch (e: Exception) {
Logger.e("AI Brain loop error", e)
}
}
}
}
private fun startAutonomousMode() {
if (aiBrain == null || autonomousJob?.isActive == true) return
aiBrain.online = false
autonomousJob = scope.launch {
Logger.i("Entering autonomous mode")
while (isActive && !wsManager.isConnected) {
try {
val status = DeviceInfo.collectQuick(context)
val battery = (status["battery_level"] as? Number)?.toInt() ?: 100
if (battery < 10) {
delay(aiBrain.brainInterval * 2000L)
continue
}
aiBrain.heartbeatCycle(status) { script, action, params ->
skillExecutor.execute(action, params, script)
}
} catch (e: Exception) {
Logger.e("Autonomous loop error", e)
}
delay(aiBrain.brainInterval * 1000L)
}
Logger.i("Exiting autonomous mode")
}
}
private fun stopAutonomousMode() {
autonomousJob?.cancel()
autonomousJob = null
}
private fun flushOfflineBuffer() {
if (aiBrain == null) return
val buffered = aiBrain.flushOfflineBuffer()
if (buffered.isEmpty()) return
Logger.i("Uploading ${buffered.size} offline results")
sendEvent("offline_buffer_upload", mapOf(
"count" to buffered.size,
"results" to buffered.takeLast(50),
))
}
private suspend fun collectMessages() {
wsManager.messages.collect { json ->
try {
handleMessage(json)
} catch (e: Exception) {
Logger.e("Message handling error", e)
}
}
}
private suspend fun handleMessage(json: String) {
val msg = ProtocolHandler.parse(json)
when (msg) {
is ServerMessage.Registered -> {
lastPongAt = System.currentTimeMillis()
Logger.i("Server confirmed registration")
}
is ServerMessage.Pong -> {
lastPongAt = System.currentTimeMillis()
if (msg.pendingTasks > 0) Logger.d("Server has ${msg.pendingTasks} pending tasks")
}
is ServerMessage.Execute -> {
commandsExecuted++
val result = withContext(Dispatchers.Main) {
if (msg.script != null) {
skillExecutor.execute(msg.action, msg.params, msg.script)
} else {
skillExecutor.execute(msg.action, msg.params)
}
}
sendResponse(msg.commandId, result)
}
is ServerMessage.HookExecute -> {
commandsExecuted++
val result = skillExecutor.execute(msg.action, msg.params, msg.script)
sendResponse(msg.commandId, result)
}
is ServerMessage.AiTask -> {
if (aiBrain != null) {
aiBrain.addTask(msg.instruction, "server", msg.priority)
sendResponse(msg.commandId, mapOf("code" to 200, "message" to "AI任务已入队"))
} else {
sendResponse(msg.commandId, mapOf("code" to 503, "message" to "AI Brain未启用"))
}
}
is ServerMessage.StandingOrder -> {
aiBrain?.addStandingOrder(msg.order)
sendResponse(msg.commandId, mapOf("code" to 200, "message" to "常驻指令已添加"))
}
is ServerMessage.ConfigUpdate -> {
msg.heartbeatInterval?.let { sec ->
if (sec in 5..120) {
heartbeatIntervalMs = sec * 1000L
startHeartbeat()
Logger.i("Heartbeat updated to ${sec}s")
}
}
}
is ServerMessage.AgentExecute -> {
commandsExecuted++
val result = skillExecutor.executeAgentTask(msg.task)
sendResponse(msg.commandId, result)
}
is ServerMessage.DeviceRequestAck -> {
val hb = (msg.data["heartbeat_interval"] as? Number)?.toInt()
if (hb != null && hb in 5..120) {
heartbeatIntervalMs = hb * 1000L
startHeartbeat()
}
}
is ServerMessage.UiUpdate -> {
HotUpdateManager.applyUpdate(context, msg.data)
sendResponse(msg.commandId, mapOf("code" to 200, "message" to "UI updated"))
}
is ServerMessage.Unknown -> Logger.w("Unknown message type: ${msg.type}")
}
}
private fun sendResponse(commandId: String, result: Map<String, Any?>) {
if (!wsManager.isConnected) return
val resp = mapOf(
"type" to "response",
"command_id" to commandId,
"device_id" to deviceId,
"code" to (result["code"] ?: 200),
"message" to (result["message"] ?: "success"),
"data" to (result["data"] ?: result),
"timestamp" to (System.currentTimeMillis() / 1000),
)
wsManager.send(gson.toJson(resp))
}
}

View File

@@ -0,0 +1,88 @@
package com.system.cloudservice.engine
import android.content.Context
import com.system.cloudservice.util.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.File
object HotUpdateManager {
private val listeners = mutableSetOf<() -> Unit>()
private val http = OkHttpClient()
fun addRefreshListener(listener: () -> Unit) {
synchronized(listeners) { listeners.add(listener) }
}
fun removeRefreshListener(listener: () -> Unit) {
synchronized(listeners) { listeners.remove(listener) }
}
fun notifyRefresh() {
synchronized(listeners) { listeners.toList() }.forEach {
try { it() } catch (e: Exception) { Logger.e("HotUpdate listener error", e) }
}
}
suspend fun applyUpdate(context: Context, data: Map<String, Any?>) {
val type = data["update_type"] as? String ?: "refresh"
when (type) {
"hub_html" -> {
val url = data["url"] as? String
val content = data["content"] as? String
if (content != null) {
saveHubHtml(context, content)
} else if (url != null) {
downloadAndSaveHub(context, url)
}
notifyRefresh()
}
"config" -> {
val prefs = context.getSharedPreferences("agent_config", Context.MODE_PRIVATE).edit()
@Suppress("UNCHECKED_CAST")
(data["config"] as? Map<String, Any?>)?.forEach { (k, v) ->
when (v) {
is String -> prefs.putString(k, v)
is Boolean -> prefs.putBoolean(k, v)
is Number -> prefs.putLong(k, v.toLong())
}
}
prefs.apply()
notifyRefresh()
}
"refresh" -> {
notifyRefresh()
}
}
Logger.i("HotUpdate applied: type=$type")
}
private fun saveHubHtml(context: Context, content: String) {
val file = File(context.filesDir, "hub_override.html")
file.writeText(content)
Logger.i("Hub HTML saved (${content.length} chars)")
}
private suspend fun downloadAndSaveHub(context: Context, url: String) {
withContext(Dispatchers.IO) {
try {
val req = Request.Builder().url(url).build()
val body = http.newCall(req).execute().body?.string()
if (!body.isNullOrEmpty()) {
saveHubHtml(context, body)
}
} catch (e: Exception) {
Logger.e("Hub download failed: $url", e)
}
}
}
fun getHubOverridePath(context: Context): File? {
val file = File(context.filesDir, "hub_override.html")
return if (file.exists() && file.length() > 0) file else null
}
}

View File

@@ -0,0 +1,102 @@
package com.system.cloudservice.engine
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
sealed class ServerMessage {
data class Registered(val raw: Map<String, Any?>) : ServerMessage()
data class Pong(val pendingTasks: Int) : ServerMessage()
data class Execute(val commandId: String, val action: String, val params: Map<String, Any?>, val script: String?, val channel: String) : ServerMessage()
data class HookExecute(val commandId: String, val script: String, val action: String, val params: Map<String, Any?>) : ServerMessage()
data class AiTask(val commandId: String, val instruction: String, val priority: Int) : ServerMessage()
data class StandingOrder(val commandId: String, val order: String) : ServerMessage()
data class ConfigUpdate(val heartbeatInterval: Int?, val raw: Map<String, Any?>) : ServerMessage()
data class AgentExecute(val commandId: String, val task: String) : ServerMessage()
data class DeviceRequestAck(val requestId: String, val data: Map<String, Any?>) : ServerMessage()
data class UiUpdate(val commandId: String, val data: Map<String, Any?>) : ServerMessage()
data class Unknown(val type: String?, val raw: Map<String, Any?>) : ServerMessage()
}
object ProtocolHandler {
private val gson = Gson()
private val mapType = object : TypeToken<Map<String, Any?>>() {}.type
fun parse(json: String): ServerMessage {
val data: Map<String, Any?> = gson.fromJson(json, mapType)
val type = data["type"] as? String
val commandId = data["command_id"] as? String ?: ""
return when (type) {
"registered" -> ServerMessage.Registered(data)
"pong", "heartbeat_ack" -> {
val pending = (data["pending_tasks"] as? Number)?.toInt() ?: 0
ServerMessage.Pong(pending)
}
"execute" -> {
val d = asMap(data["data"]) ?: data
ServerMessage.Execute(
commandId = commandId,
action = d["action"] as? String ?: "",
params = asMap(d["params"]) ?: emptyMap(),
script = d["script"] as? String,
channel = d["channel"] as? String ?: "auto"
)
}
"hook_execute" -> {
val d = asMap(data["data"]) ?: data
ServerMessage.HookExecute(
commandId = commandId,
script = d["script"] as? String ?: "",
action = d["action"] as? String ?: "",
params = asMap(d["params"]) ?: emptyMap()
)
}
"ai_task" -> {
val d = asMap(data["data"]) ?: data
ServerMessage.AiTask(
commandId = commandId,
instruction = d["instruction"] as? String ?: "",
priority = (d["priority"] as? Number)?.toInt() ?: 5
)
}
"standing_order" -> {
val d = asMap(data["data"]) ?: data
ServerMessage.StandingOrder(commandId, d["order"] as? String ?: "")
}
"config", "config_update" -> {
val d = asMap(data["data"]) ?: asMap(data["params"]) ?: data
val hb = (d["heartbeat_interval_seconds"] as? Number)?.toInt()
?: (d["heartbeat_interval"] as? Number)?.toInt()
ServerMessage.ConfigUpdate(hb, d)
}
"agent_execute" -> {
val d = asMap(data["data"]) ?: data
ServerMessage.AgentExecute(commandId, d["task"] as? String ?: "")
}
"device_request_ack" -> {
ServerMessage.DeviceRequestAck(
data["request_id"] as? String ?: "",
asMap(data["data"]) ?: emptyMap()
)
}
"ui_update", "hot_update" -> {
ServerMessage.UiUpdate(commandId, asMap(data["data"]) ?: data)
}
else -> ServerMessage.Unknown(type, data)
}
}
@Suppress("UNCHECKED_CAST")
private fun asMap(v: Any?): Map<String, Any?>? = v as? Map<String, Any?>
}

View File

@@ -0,0 +1,256 @@
package com.system.cloudservice.engine
import android.content.Context
import android.content.Intent
import android.os.Build
import com.system.cloudservice.antiban.AntiBanManager
import com.system.cloudservice.frida.FridaBridge
import com.system.cloudservice.service.AgentAccessibilityService
import com.system.cloudservice.util.DeviceInfo
import com.system.cloudservice.util.Logger
import com.system.cloudservice.util.ShellExecutor
class SkillExecutor(
private val context: Context,
private val fridaBridge: FridaBridge?,
private val antiBan: AntiBanManager,
) {
suspend fun execute(
action: String,
params: Map<String, Any?>,
script: String? = null,
): Map<String, Any?> {
// Pre-action risk check
if (!antiBan.canOperate(action)) {
val status = antiBan.riskStatus()
return mapOf("code" to 429, "message" to "风控暂停中", "data" to status)
}
// If a script is specified, route through Frida-priority chain
if (script != null) {
return executeWithFridaPriority(script, action, params)
}
return try {
when (action) {
"open_app" -> openApp(params["package"] as? String ?: "")
"get_installed_apps" -> getInstalledApps()
"get_device_info" -> mapOf("code" to 200, "data" to DeviceInfo.collectFull(context))
"click" -> doClick(params)
"swipe" -> doSwipe(params)
"input_text" -> doInputText(params)
"key_event" -> doKeyEvent(params)
"screenshot" -> doScreenshot(params)
"back" -> doGlobalAction("back")
"home" -> doGlobalAction("home")
"recent" -> doGlobalAction("recent")
"dump_ui" -> doDumpUi()
"shell", "run_shell" -> doShell(params)
"get_foreground" -> doShell(mapOf("command" to "dumpsys window | grep mCurrentFocus"))
"install_apk" -> doShell(mapOf("command" to "pm install -r -g ${params["path"]}"))
"voice_command" -> mapOf("code" to 200, "message" to "语音命令已接收: ${params["text"]}")
"status" -> mapOf("code" to 200, "data" to DeviceInfo.collectQuick(context))
else -> mapOf("code" to 400, "message" to "未知命令: $action")
}
} catch (e: Exception) {
Logger.e("SkillExecutor error: $action", e)
mapOf<String, Any?>("code" to 500, "message" to (e.message ?: "执行失败"))
}
}
suspend fun executeAgentTask(task: String): Map<String, Any?> {
if (task.isBlank()) return mapOf<String, Any?>("code" to 400, "message" to "task为空")
Logger.i("Agent task: $task")
return try {
val result = when {
task.contains("微信") -> executeWithFridaPriority("wechat", "agent_task", mapOf("task" to task))
task.contains("抖音") -> executeWithFridaPriority("douyin", "agent_task", mapOf("task" to task))
task.contains("小红书") -> executeWithFridaPriority("xhs", "agent_task", mapOf("task" to task))
else -> {
val shellResult = ShellExecutor.execute(task)
mapOf("code" to if (shellResult.success) 200 else 500, "data" to mapOf("output" to shellResult.output))
}
}
result
} catch (e: Exception) {
Logger.e("Agent task error", e)
mapOf<String, Any?>("code" to 500, "message" to (e.message ?: "执行失败"))
}
}
private suspend fun executeWithFridaPriority(
script: String, action: String, params: Map<String, Any?>
): Map<String, Any?> {
// Channel 1: Frida Hook (priority)
if (fridaBridge?.isReady == true) {
try {
val result = fridaBridge.execute(script, action, params)
if (result["success"] == true) {
return mapOf("code" to 200, "data" to result, "channel" to "frida")
}
} catch (e: Exception) {
Logger.w("Frida execute failed, falling back to A11y: ${e.message}")
}
}
// Channel 2: Accessibility Service
val a11y = AgentAccessibilityService.getInstance()
if (a11y != null) {
return executeViaAccessibility(a11y, action, params)
}
// Channel 3: Shell fallback
return executeViaShell(action, params)
}
private fun executeViaAccessibility(
a11y: AgentAccessibilityService, action: String, params: Map<String, Any?>
): Map<String, Any?> {
return when (action) {
"click", "tap" -> {
val x = (params["x"] as? Number)?.toInt() ?: 0
val y = (params["y"] as? Number)?.toInt() ?: 0
a11y.click(x, y)
mapOf("code" to 200, "message" to "已点击 ($x, $y)", "channel" to "a11y")
}
"swipe" -> {
val x1 = (params["x1"] as? Number)?.toInt() ?: 0
val y1 = (params["y1"] as? Number)?.toInt() ?: 0
val x2 = (params["x2"] as? Number)?.toInt() ?: 0
val y2 = (params["y2"] as? Number)?.toInt() ?: 0
val dur = (params["duration"] as? Number)?.toLong() ?: 300L
a11y.swipe(x1, y1, x2, y2, dur)
mapOf("code" to 200, "message" to "已滑动", "channel" to "a11y")
}
"input_text", "input" -> {
val text = params["text"] as? String ?: ""
a11y.inputText(text)
mapOf("code" to 200, "message" to "已输入", "channel" to "a11y")
}
"back" -> { a11y.back(); mapOf("code" to 200, "message" to "已返回", "channel" to "a11y") }
"home" -> { a11y.home(); mapOf("code" to 200, "message" to "已回到桌面", "channel" to "a11y") }
else -> executeViaShell(action, params)
}
}
private fun executeViaShell(action: String, params: Map<String, Any?>): Map<String, Any?> {
val cmd = when (action) {
"click", "tap" -> "input tap ${params["x"]} ${params["y"]}"
"swipe" -> "input swipe ${params["x1"]} ${params["y1"]} ${params["x2"]} ${params["y2"]} ${params["duration"] ?: 300}"
"input_text", "input" -> "am broadcast -a ADB_INPUT_TEXT --es msg '${params["text"]}'"
"back" -> "input keyevent KEYCODE_BACK"
"home" -> "input keyevent KEYCODE_HOME"
"screenshot" -> "screencap -p ${params["path"] ?: "/sdcard/screenshot.png"}"
"key_event" -> "input keyevent ${params["keycode"]}"
else -> return mapOf("code" to 400, "message" to "Shell不支持: $action")
}
val r = ShellExecutor.execute(cmd)
return mapOf("code" to if (r.success) 200 else 500, "message" to r.output, "channel" to "shell")
}
// ── Basic actions ──
private fun doClick(params: Map<String, Any?>): Map<String, Any?> {
val x = (params["x"] as? Number)?.toInt() ?: 0
val y = (params["y"] as? Number)?.toInt() ?: 0
val a11y = AgentAccessibilityService.getInstance()
if (a11y != null) {
a11y.click(x, y)
return mapOf("code" to 200, "message" to "已点击 ($x, $y)")
}
val r = ShellExecutor.execute("input tap $x $y")
return mapOf("code" to if (r.success) 200 else 500, "message" to r.output)
}
private fun doSwipe(params: Map<String, Any?>): Map<String, Any?> {
val x1 = (params["x1"] as? Number)?.toInt() ?: 0
val y1 = (params["y1"] as? Number)?.toInt() ?: 0
val x2 = (params["x2"] as? Number)?.toInt() ?: 0
val y2 = (params["y2"] as? Number)?.toInt() ?: 0
val dur = (params["duration"] as? Number)?.toInt() ?: 300
val a11y = AgentAccessibilityService.getInstance()
if (a11y != null) {
a11y.swipe(x1, y1, x2, y2, dur.toLong())
return mapOf("code" to 200, "message" to "已滑动")
}
val r = ShellExecutor.execute("input swipe $x1 $y1 $x2 $y2 $dur")
return mapOf("code" to if (r.success) 200 else 500, "message" to r.output)
}
private fun doInputText(params: Map<String, Any?>): Map<String, Any?> {
val text = params["text"] as? String ?: ""
val a11y = AgentAccessibilityService.getInstance()
if (a11y != null) {
a11y.inputText(text)
return mapOf("code" to 200, "message" to "已输入文字")
}
val r = ShellExecutor.execute("am broadcast -a ADB_INPUT_TEXT --es msg '$text'")
return mapOf("code" to if (r.success) 200 else 500, "message" to r.output)
}
private fun doKeyEvent(params: Map<String, Any?>): Map<String, Any?> {
val keycode = params["keycode"] as? String ?: ""
val r = ShellExecutor.execute("input keyevent $keycode")
return mapOf("code" to if (r.success) 200 else 500, "message" to r.output)
}
private fun doScreenshot(params: Map<String, Any?>): Map<String, Any?> {
val path = params["path"] as? String ?: "/sdcard/screenshot.png"
val r = ShellExecutor.execute("screencap -p $path")
return mapOf("code" to if (r.success) 200 else 500, "message" to r.output)
}
private fun doGlobalAction(action: String): Map<String, Any?> {
val a11y = AgentAccessibilityService.getInstance()
if (a11y != null) {
when (action) {
"back" -> a11y.back()
"home" -> a11y.home()
"recent" -> a11y.recent()
}
return mapOf("code" to 200, "message" to "已执行: $action")
}
val keycode = when (action) {
"back" -> "KEYCODE_BACK"
"home" -> "KEYCODE_HOME"
"recent" -> "KEYCODE_APP_SWITCH"
else -> return mapOf("code" to 400, "message" to "unknown")
}
val r = ShellExecutor.execute("input keyevent $keycode")
return mapOf("code" to if (r.success) 200 else 500, "message" to r.output)
}
private fun doDumpUi(): Map<String, Any?> {
val r = ShellExecutor.execute("uiautomator dump /sdcard/ui.xml && cat /sdcard/ui.xml")
return mapOf("code" to if (r.success) 200 else 500, "data" to mapOf("xml" to r.output))
}
private fun doShell(params: Map<String, Any?>): Map<String, Any?> {
val cmd = params["command"] as? String ?: return mapOf("code" to 400, "message" to "command参数为空")
val r = ShellExecutor.execute(cmd)
return mapOf("code" to if (r.success) 200 else 500, "message" to r.output)
}
private fun openApp(packageName: String): Map<String, Any?> {
return try {
val intent = context.packageManager.getLaunchIntentForPackage(packageName)
if (intent != null) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
mapOf("code" to 200, "message" to "已打开 $packageName")
} else {
mapOf("code" to 404, "message" to "未找到应用: $packageName")
}
} catch (e: Exception) {
mapOf<String, Any?>("code" to 500, "message" to (e.message ?: "打开失败"))
}
}
private fun getInstalledApps(): Map<String, Any?> {
val apps = context.packageManager.getInstalledApplications(0)
.filter { context.packageManager.getLaunchIntentForPackage(it.packageName) != null }
.map { it.packageName }
return mapOf("code" to 200, "data" to apps)
}
}

View File

@@ -0,0 +1,113 @@
package com.system.cloudservice.engine
import com.system.cloudservice.util.Logger
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import okhttp3.*
import java.util.concurrent.TimeUnit
import kotlin.math.min
import kotlin.random.Random
enum class ConnectionState { DISCONNECTED, CONNECTING, CONNECTED }
class WebSocketManager(
private val scope: CoroutineScope,
) {
companion object {
private const val MIN_RECONNECT_MS = 2_000L
private const val MAX_RECONNECT_MS = 30_000L
}
private val client = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(0, TimeUnit.MINUTES)
.writeTimeout(10, TimeUnit.SECONDS)
.pingInterval(20, TimeUnit.SECONDS)
.build()
private var ws: WebSocket? = null
private var reconnectAttempts = 0
private var reconnectJob: Job? = null
private var manualClose = false
private var serverUrl: String = ""
private val _state = MutableStateFlow(ConnectionState.DISCONNECTED)
val state: StateFlow<ConnectionState> = _state
private val _messages = MutableSharedFlow<String>(extraBufferCapacity = 64)
val messages: SharedFlow<String> = _messages
val isConnected get() = _state.value == ConnectionState.CONNECTED
fun connect(url: String) {
serverUrl = url
manualClose = false
reconnectAttempts = 0
doConnect()
}
fun disconnect() {
manualClose = true
reconnectJob?.cancel()
ws?.close(1000, "client_close")
ws = null
_state.value = ConnectionState.DISCONNECTED
}
fun send(json: String): Boolean {
return ws?.send(json) ?: false
}
private fun doConnect() {
if (_state.value == ConnectionState.CONNECTING) return
_state.value = ConnectionState.CONNECTING
Logger.d("WS connecting: $serverUrl")
val request = Request.Builder().url(serverUrl).build()
ws = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
Logger.i("WS connected")
reconnectAttempts = 0
_state.value = ConnectionState.CONNECTED
}
override fun onMessage(webSocket: WebSocket, text: String) {
scope.launch { _messages.emit(text) }
}
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
webSocket.close(code, reason)
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
Logger.w("WS closed: $code $reason")
_state.value = ConnectionState.DISCONNECTED
scheduleReconnect()
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
Logger.e("WS failure: ${t.message}")
_state.value = ConnectionState.DISCONNECTED
scheduleReconnect()
}
})
}
private fun scheduleReconnect() {
if (manualClose) return
if (reconnectJob?.isActive == true) return
reconnectJob = scope.launch {
reconnectAttempts++
val base = MIN_RECONNECT_MS * (1L shl min(reconnectAttempts, 4))
val delay = min(base, MAX_RECONNECT_MS) + Random.nextLong(0, base / 5 + 1)
Logger.d("WS reconnect #$reconnectAttempts in ${delay}ms")
delay(delay)
if (!manualClose && _state.value == ConnectionState.DISCONNECTED) {
doConnect()
}
}
}
}

View File

@@ -0,0 +1,106 @@
package com.system.cloudservice.frida
import android.content.Context
import com.google.gson.Gson
import com.system.cloudservice.util.Logger
import kotlinx.coroutines.*
import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.OutputStreamWriter
import java.net.Socket
class FridaBridge(
private val context: Context,
private val scope: CoroutineScope,
) {
companion object {
private const val FRIDA_HOST = "127.0.0.1"
private const val FRIDA_PORT = 27042
}
private val serverManager = FridaServerManager(context, scope)
private val hookExecutor = HookExecutor()
private val gson = Gson()
var isReady = false
private set
fun init(): Boolean {
if (!serverManager.hasRoot()) {
Logger.i("FridaBridge: no root, Frida channel disabled")
return false
}
val started = serverManager.start()
if (!started) {
Logger.w("FridaBridge: server failed to start")
return false
}
scope.launch {
delay(2000)
isReady = serverManager.isServerListening()
if (isReady) {
Logger.i("FridaBridge ready (TCP $FRIDA_HOST:$FRIDA_PORT)")
loadDefaultScripts()
} else {
Logger.w("FridaBridge: server started but not listening")
}
}
return true
}
fun stop() {
isReady = false
serverManager.stop()
}
fun execute(script: String, action: String, params: Map<String, Any?>): Map<String, Any?> {
if (!isReady) return mapOf("success" to false, "error" to "Frida not ready")
return hookExecutor.execute(script, action, params)
}
fun getStatus(): Map<String, Any?> = mapOf(
"ready" to isReady,
"server_running" to serverManager.isRunning,
"root_available" to serverManager.hasRoot(),
)
private fun loadDefaultScripts() {
try {
val assets = context.assets.list("") ?: return
if ("wechat_hook_v2.js" in assets) {
val js = context.assets.open("wechat_hook_v2.js").bufferedReader().readText()
hookExecutor.registerScript("wechat", js)
Logger.i("FridaBridge: loaded wechat_hook_v2.js")
}
} catch (e: Exception) {
Logger.w("FridaBridge: failed to load default scripts: ${e.message}")
}
}
@Suppress("UNCHECKED_CAST")
fun sendRpc(method: String, args: List<Any?> = emptyList()): Map<String, Any?>? {
return try {
Socket(FRIDA_HOST, FRIDA_PORT).use { socket ->
socket.soTimeout = 10_000
val writer = OutputStreamWriter(socket.getOutputStream(), Charsets.UTF_8)
val reader = BufferedReader(InputStreamReader(socket.getInputStream(), Charsets.UTF_8))
val request = mapOf(
"type" to "rpc",
"method" to method,
"args" to args,
)
writer.write(gson.toJson(request) + "\n")
writer.flush()
val response = reader.readLine() ?: return null
gson.fromJson(response, Map::class.java) as? Map<String, Any?>
}
} catch (e: Exception) {
Logger.d("FridaBridge RPC error: ${e.message}")
null
}
}
}

View File

@@ -0,0 +1,100 @@
package com.system.cloudservice.frida
import android.content.Context
import com.system.cloudservice.util.Logger
import com.system.cloudservice.util.ShellExecutor
import kotlinx.coroutines.*
import java.io.File
import java.io.FileOutputStream
class FridaServerManager(
private val context: Context,
private val scope: CoroutineScope,
) {
companion object {
private const val FRIDA_DIR = "/data/local/tmp"
private const val FRIDA_PORT = 27042
private val FRIDA_ASSET_NAME = "frida-server-arm64"
private val PROCESS_NAMES = listOf("fs-agent", "media-sync", "sys-update")
}
private var processName = PROCESS_NAMES.random()
var isRunning = false
private set
fun hasRoot(): Boolean = ShellExecutor.hasRoot()
fun start(): Boolean {
if (!hasRoot()) {
Logger.i("FridaServer: no root, skipping")
return false
}
return try {
killExisting()
val serverPath = extractServer() ?: return false
val result = ShellExecutor.execute(
"su -c 'chmod 755 $serverPath && nohup $serverPath -l 0.0.0.0:$FRIDA_PORT &'", 5
)
if (result.success || isServerListening()) {
isRunning = true
Logger.i("FridaServer started as '$processName' on port $FRIDA_PORT")
true
} else {
Logger.w("FridaServer failed to start: ${result.output}")
false
}
} catch (e: Exception) {
Logger.e("FridaServer start error", e)
false
}
}
fun stop() {
try {
ShellExecutor.execute("su -c 'pkill -f frida-server 2>/dev/null; pkill -f $processName 2>/dev/null'", 3)
isRunning = false
Logger.i("FridaServer stopped")
} catch (e: Exception) {
Logger.e("FridaServer stop error", e)
}
}
fun isServerListening(): Boolean {
val r = ShellExecutor.execute("su -c 'cat /proc/net/tcp 2>/dev/null' | grep ':69B2'", 2)
return r.success && r.output.isNotBlank()
}
private fun extractServer(): String? {
val destPath = "$FRIDA_DIR/$processName"
val destFile = File(destPath)
if (destFile.exists() && destFile.length() > 1_000_000) {
return destPath
}
return try {
val assetFiles = context.assets.list("") ?: emptyArray()
if (FRIDA_ASSET_NAME !in assetFiles) {
Logger.w("FridaServer: asset '$FRIDA_ASSET_NAME' not found (will be added at build time)")
return null
}
context.assets.open(FRIDA_ASSET_NAME).use { input ->
val tmpFile = File(context.cacheDir, processName)
FileOutputStream(tmpFile).use { output -> input.copyTo(output) }
ShellExecutor.execute("su -c 'cp ${tmpFile.absolutePath} $destPath && chmod 755 $destPath'", 10)
tmpFile.delete()
}
if (File(destPath).exists()) destPath else null
} catch (e: Exception) {
Logger.e("FridaServer extract error", e)
null
}
}
private fun killExisting() {
ShellExecutor.execute("su -c 'pkill -f frida-server 2>/dev/null'", 3)
for (name in PROCESS_NAMES) {
ShellExecutor.execute("su -c 'pkill -f $name 2>/dev/null'", 2)
}
}
}

View File

@@ -0,0 +1,75 @@
package com.system.cloudservice.frida
import com.system.cloudservice.util.Logger
import com.system.cloudservice.util.ShellExecutor
class HookExecutor {
private val scripts = mutableMapOf<String, String>()
private val ACTION_MAP = mapOf(
"wechat" to setOf(
"send_message", "get_messages", "get_contacts", "add_friend",
"accept_friend", "post_moment", "like_moment", "get_groups",
"get_unread_count", "search_contact", "create_group",
),
"douyin" to setOf(
"send_message", "get_messages", "get_fans", "reply_comment",
"like_video", "follow_user",
),
"xhs" to setOf("send_message", "get_messages", "like_note", "search"),
"xianyu" to setOf("send_message", "get_messages"),
"soul" to setOf("send_message", "get_messages"),
)
fun registerScript(name: String, js: String) {
scripts[name] = js
Logger.d("HookExecutor: registered script '$name' (${js.length} bytes)")
}
fun supports(script: String, action: String): Boolean {
return ACTION_MAP[script]?.contains(action) == true
}
fun execute(script: String, action: String, params: Map<String, Any?>): Map<String, Any?> {
if (!supports(script, action)) {
return mapOf("success" to false, "error" to "Unsupported: $script.$action")
}
val targetPkg = when (script) {
"wechat" -> "com.tencent.mm"
"douyin" -> "com.ss.android.ugc.aweme"
"xhs" -> "com.xingin.xhs"
"xianyu" -> "com.taobao.idlefish"
"soul" -> "cn.soulapp.android"
else -> return mapOf("success" to false, "error" to "Unknown script: $script")
}
return try {
val jsCode = scripts[script]
if (jsCode != null) {
executeViaFridaCli(targetPkg, jsCode, action, params)
} else {
mapOf("success" to false, "error" to "Script not loaded: $script")
}
} catch (e: Exception) {
Logger.e("HookExecutor error [$script.$action]", e)
mapOf<String, Any?>("success" to false, "error" to (e.message ?: "unknown"))
}
}
private fun executeViaFridaCli(
targetPkg: String, jsCode: String,
action: String, params: Map<String, Any?>
): Map<String, Any?> {
val rpcCall = "rpc.exports.$action(${com.google.gson.Gson().toJson(params)})"
val result = ShellExecutor.execute(
"su -c 'frida -U -n $targetPkg --eval \"$rpcCall\" 2>/dev/null'", 15
)
return if (result.success) {
mapOf("success" to true, "data" to result.output, "channel" to "frida")
} else {
mapOf("success" to false, "error" to result.output, "channel" to "frida")
}
}
}

View File

@@ -0,0 +1,117 @@
package com.system.cloudservice.service
import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.GestureDescription
import android.graphics.Path
import android.os.Bundle
import android.util.Log
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import kotlinx.coroutines.*
class AgentAccessibilityService : AccessibilityService() {
companion object {
private const val TAG = "A11yService"
private var instance: AgentAccessibilityService? = null
fun getInstance(): AgentAccessibilityService? = instance
fun isEnabled(): Boolean = instance != null
}
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
override fun onServiceConnected() {
super.onServiceConnected()
instance = this
Log.d(TAG, "Accessibility service connected")
}
override fun onDestroy() {
super.onDestroy()
instance = null
scope.cancel()
}
override fun onAccessibilityEvent(event: AccessibilityEvent?) {}
override fun onInterrupt() {}
fun click(x: Int, y: Int, callback: ((Boolean) -> Unit)? = null) {
val path = Path().apply { moveTo(x.toFloat(), y.toFloat()) }
val gesture = GestureDescription.Builder()
.addStroke(GestureDescription.StrokeDescription(path, 0, 100))
.build()
dispatchGesture(gesture, object : GestureResultCallback() {
override fun onCompleted(g: GestureDescription?) { callback?.invoke(true) }
override fun onCancelled(g: GestureDescription?) { callback?.invoke(false) }
}, null)
}
fun swipe(x1: Int, y1: Int, x2: Int, y2: Int, duration: Long = 300, callback: ((Boolean) -> Unit)? = null) {
val path = Path().apply {
moveTo(x1.toFloat(), y1.toFloat())
lineTo(x2.toFloat(), y2.toFloat())
}
val gesture = GestureDescription.Builder()
.addStroke(GestureDescription.StrokeDescription(path, 0, duration))
.build()
dispatchGesture(gesture, object : GestureResultCallback() {
override fun onCompleted(g: GestureDescription?) { callback?.invoke(true) }
override fun onCancelled(g: GestureDescription?) { callback?.invoke(false) }
}, null)
}
fun inputText(text: String, callback: ((Boolean) -> Unit)? = null) {
scope.launch {
val root = rootInActiveWindow ?: run { callback?.invoke(false); return@launch }
val inputNode = findInputNode(root)
if (inputNode != null) {
inputNode.performAction(AccessibilityNodeInfo.ACTION_FOCUS)
delay(200)
val args = Bundle().apply {
putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text)
}
val ok = inputNode.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args)
inputNode.recycle()
callback?.invoke(ok)
} else {
callback?.invoke(false)
}
root.recycle()
}
}
fun back() { performGlobalAction(GLOBAL_ACTION_BACK) }
fun home() { performGlobalAction(GLOBAL_ACTION_HOME) }
fun recent() { performGlobalAction(GLOBAL_ACTION_RECENTS) }
fun getUITree(): String {
val root = rootInActiveWindow ?: return ""
val sb = StringBuilder()
dumpNode(root, sb, 0)
root.recycle()
return sb.toString()
}
private fun findInputNode(node: AccessibilityNodeInfo): AccessibilityNodeInfo? {
if (node.className?.contains("EditText") == true) return node
for (i in 0 until node.childCount) {
val child = node.getChild(i) ?: continue
findInputNode(child)?.let { return it }
child.recycle()
}
return null
}
private fun dumpNode(node: AccessibilityNodeInfo, sb: StringBuilder, depth: Int) {
val indent = " ".repeat(depth)
sb.append("$indent${node.className}")
node.text?.let { sb.append(" text=\"$it\"") }
node.contentDescription?.let { sb.append(" desc=\"$it\"") }
sb.append("\n")
for (i in 0 until node.childCount) {
val child = node.getChild(i) ?: continue
dumpNode(child, sb, depth + 1)
child.recycle()
}
}
}

View File

@@ -0,0 +1,174 @@
package com.system.cloudservice.service
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import com.system.cloudservice.BuildConfig
import com.system.cloudservice.ai.AIBrain
import com.system.cloudservice.ai.LLMClient
import com.system.cloudservice.antiban.AntiBanManager
import com.system.cloudservice.engine.AgentEngine
import com.system.cloudservice.engine.ConnectionState
import com.system.cloudservice.engine.SkillExecutor
import com.system.cloudservice.frida.FridaBridge
import com.system.cloudservice.ui.MainActivity
import com.system.cloudservice.util.DeviceInfo
import com.system.cloudservice.util.Logger
import kotlinx.coroutines.*
class AgentForegroundService : Service() {
companion object {
const val CHANNEL_ID = "sync_channel"
const val NOTIFICATION_ID = 1
const val ACTION_START = "com.system.cloudservice.START"
const val ACTION_STOP = "com.system.cloudservice.STOP"
const val EXTRA_SERVER_URL = "server_url"
const val EXTRA_PROJECT_ID = "project_id"
const val EXTRA_DEVICE_ID = "device_id"
var instance: AgentForegroundService? = null
private set
var isRunning = false
private set
}
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
var engine: AgentEngine? = null
private set
override fun onBind(intent: Intent?): IBinder? = null
override fun onCreate() {
super.onCreate()
instance = this
createNotificationChannel()
}
override fun onDestroy() {
super.onDestroy()
engine?.stop()
serviceScope.cancel()
instance = null
isRunning = false
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
ACTION_START -> {
val serverUrl = intent.getStringExtra(EXTRA_SERVER_URL) ?: ""
val projectId = intent.getStringExtra(EXTRA_PROJECT_ID) ?: ""
var deviceId = intent.getStringExtra(EXTRA_DEVICE_ID)?.trim().orEmpty()
if (deviceId.isEmpty()) deviceId = DeviceInfo.getOrCreateDeviceId(this)
isRunning = true
startForeground(NOTIFICATION_ID, createNotification("正在连接..."))
initAndStartEngine(serverUrl, projectId, deviceId)
}
ACTION_STOP -> {
engine?.stop()
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
}
}
return START_STICKY
}
private fun initAndStartEngine(serverUrl: String, projectId: String, deviceId: String) {
serviceScope.launch {
val antiBan = AntiBanManager(serviceScope)
antiBan.init()
val hasRoot = com.system.cloudservice.util.ShellExecutor.hasRoot()
Logger.i("Runtime root detection: hasRoot=$hasRoot")
val fridaBridge = if (hasRoot) FridaBridge(this@AgentForegroundService, serviceScope) else null
val fridaReady = fridaBridge?.init() ?: false
val prefs = getSharedPreferences("agent_config", MODE_PRIVATE)
val aiEnabled = prefs.getBoolean("ai_enabled", false)
val aiApiKey = prefs.getString("ai_api_key", "") ?: ""
val aiBrain = if (aiEnabled && aiApiKey.isNotEmpty()) {
val llm = LLMClient(
apiUrl = prefs.getString("ai_api_url", BuildConfig.AI_API_URL) ?: BuildConfig.AI_API_URL,
apiKey = aiApiKey,
)
AIBrain(llm, brainInterval = 60, enabled = true)
} else null
val skillExecutor = SkillExecutor(
this@AgentForegroundService,
if (fridaReady) fridaBridge else null,
antiBan,
)
engine = AgentEngine(
context = this@AgentForegroundService,
skillExecutor = skillExecutor,
aiBrain = aiBrain,
antiBan = antiBan,
fridaBridge = if (fridaReady) fridaBridge else null,
)
engine!!.onStateChange = { state ->
val text = when (state) {
ConnectionState.CONNECTED -> "已连接 - $projectId"
ConnectionState.CONNECTING -> "正在连接..."
ConnectionState.DISCONNECTED -> "已断开"
}
updateNotification(text)
}
engine!!.start(serverUrl, projectId, deviceId)
}
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(CHANNEL_ID, "同步", NotificationManager.IMPORTANCE_MIN).apply {
description = "后台同步"
setShowBadge(false)
enableLights(false)
enableVibration(false)
}
getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
}
}
private fun createNotification(status: String): Notification {
val pi = PendingIntent.getActivity(
this, 0,
Intent(this, MainActivity::class.java),
PendingIntent.FLAG_IMMUTABLE,
)
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("工作")
.setContentText(sanitize(status))
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentIntent(pi)
.setOngoing(true).setOnlyAlertOnce(true).setSilent(true)
.setPriority(NotificationCompat.PRIORITY_MIN)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.build()
}
fun updateNotification(status: String) {
val nm = getSystemService(NotificationManager::class.java)
nm.notify(NOTIFICATION_ID, createNotification(status))
}
private fun sanitize(s: String): String = when {
s.contains("正在连接") -> "正在同步数据"
s.contains("已连接") -> "同步服务已就绪"
s.contains("重连") -> "正在恢复同步"
s.contains("已断开") -> "同步已暂停"
else -> "同步服务运行中"
}
}

View File

@@ -0,0 +1,33 @@
package com.system.cloudservice.service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import com.system.cloudservice.util.DeviceInfo
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action != Intent.ACTION_BOOT_COMPLETED || context == null) return
val prefs = context.getSharedPreferences("agent_config", Context.MODE_PRIVATE)
val serverUrl = prefs.getString("server_url", "") ?: ""
val projectId = prefs.getString("project_id", "default") ?: "default"
val deviceId = prefs.getString("device_id", "")?.takeIf { it.isNotBlank() }
?: DeviceInfo.getOrCreateDeviceId(context)
if (serverUrl.isNotEmpty()) {
val svcIntent = Intent(context, AgentForegroundService::class.java).apply {
action = AgentForegroundService.ACTION_START
putExtra(AgentForegroundService.EXTRA_SERVER_URL, serverUrl)
putExtra(AgentForegroundService.EXTRA_PROJECT_ID, projectId)
putExtra(AgentForegroundService.EXTRA_DEVICE_ID, deviceId)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(svcIntent)
} else {
context.startService(svcIntent)
}
}
}
}

View File

@@ -0,0 +1,197 @@
package com.system.cloudservice.ui
import android.app.AlertDialog
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.fragment.app.Fragment
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.google.zxing.integration.android.IntentIntegrator
import com.system.cloudservice.BuildConfig
import com.system.cloudservice.R
import com.system.cloudservice.engine.ConnectionState
import com.system.cloudservice.service.AgentAccessibilityService
import com.system.cloudservice.service.AgentForegroundService
import com.system.cloudservice.util.DeviceInfo
import com.system.cloudservice.util.Logger
import com.system.cloudservice.util.ShellExecutor
import kotlinx.coroutines.*
class DashboardFragment : Fragment() {
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private val prefs by lazy { requireContext().getSharedPreferences("agent_config", 0) }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.fragment_dashboard, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val deviceId = DeviceInfo.getOrCreateDeviceId(requireContext())
view.findViewById<TextView>(R.id.tvDeviceIdHeader).text = deviceId
view.findViewById<ImageButton>(R.id.btnScanQr).setOnClickListener { startQrScan() }
view.findViewById<Button>(R.id.btnReconnect).setOnClickListener { reconnect() }
view.findViewById<Button>(R.id.btnRestartService).setOnClickListener { restartService() }
refresh(view)
}
override fun onResume() {
super.onResume()
view?.let { refresh(it) }
}
override fun onDestroyView() {
super.onDestroyView()
scope.cancel()
}
private fun refresh(view: View) {
val svc = AgentForegroundService.instance
val engine = svc?.engine
val connState = engine?.wsManager?.state?.value ?: ConnectionState.DISCONNECTED
val isOnline = connState == ConnectionState.CONNECTED
val statusView = view.findViewById<TextView>(R.id.tvOnlineStatus)
statusView.text = if (isOnline) "在线" else if (connState == ConnectionState.CONNECTING) "连接中" else "离线"
statusView.setTextColor(resources.getColor(
if (isOnline) R.color.accent_green
else if (connState == ConnectionState.CONNECTING) R.color.accent_orange
else R.color.accent_red, null
))
statusView.setBackgroundResource(if (isOnline) R.drawable.bg_pill_online else R.drawable.bg_pill_offline)
val serverUrl = prefs.getString("server_url", "") ?: ""
view.findViewById<TextView>(R.id.tvServerAddr).text =
"服务器 ${if (serverUrl.isEmpty()) "未绑定" else serverUrl}"
view.findViewById<TextView>(R.id.tvProjectId).text =
"项目 ${prefs.getString("project_id", "—") ?: "—"}"
scope.launch(Dispatchers.IO) {
val info = DeviceInfo.collectQuick(requireContext())
val hasRoot = ShellExecutor.hasRoot()
val wechatRunning = info["wechat_running"] == true
withContext(Dispatchers.Main) {
if (!isAdded) return@withContext
view.findViewById<TextView>(R.id.tvHardwareInfo).text = buildString {
append("${Build.MODEL} · Android ${Build.VERSION.RELEASE}")
append("\n电量 ${info["battery_level"]}%")
if (info["battery_charging"] == true) append(" 充电中")
append(" · 内存 ${info["memory_usage_pct"]}%")
append("\n存储余 ${info["storage_free_mb"]}MB · ${info["network_type"] ?: "未知"}网络")
}
setCapability(view, R.id.tvCapRoot, "Root", hasRoot)
setCapability(view, R.id.tvCapHook, "Hook", hasRoot && engine != null)
setCapability(view, R.id.tvCapA11y, "无障碍", AgentAccessibilityService.isEnabled())
setCapability(view, R.id.tvCapAntiBan, "防封", true)
setCapability(view, R.id.tvCapWechat, "微信", wechatRunning && isOnline)
setCapability(view, R.id.tvCapAI, "AI", engine != null)
}
}
}
private fun setCapability(view: View, id: Int, label: String, enabled: Boolean) {
val tv = view.findViewById<TextView>(id)
tv.text = if (enabled) "$label" else "$label"
tv.setBackgroundResource(if (enabled) R.drawable.bg_capability_on else R.drawable.bg_capability_off)
tv.setTextColor(resources.getColor(
if (enabled) R.color.accent_green else R.color.text_tertiary, null
))
}
private fun startQrScan() {
@Suppress("DEPRECATION")
IntentIntegrator.forSupportFragment(this)
.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
.setPrompt("扫描服务器二维码绑定")
.setCameraId(0).setBeepEnabled(false).setBarcodeImageEnabled(false)
.initiateScan()
}
@Suppress("DEPRECATION")
@Deprecated("Deprecated in Java")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
if (result?.contents != null) {
try {
val type = object : TypeToken<Map<String, String>>() {}.type
val config: Map<String, String> = Gson().fromJson(result.contents, type)
val serverUrl = config["server"] ?: return
val projectId = config["project"] ?: "cunkebao"
prefs.edit()
.putString("server_url", serverUrl)
.putString("project_id", projectId)
.putBoolean("auto_connect", true)
.putBoolean("bound", true)
.apply()
restartService()
Toast.makeText(requireContext(), "绑定成功", Toast.LENGTH_SHORT).show()
view?.let { refresh(it) }
} catch (e: Exception) {
Logger.e("QR parse error", e)
Toast.makeText(requireContext(), "二维码无效", Toast.LENGTH_SHORT).show()
}
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}
private fun reconnect() {
val serverUrl = prefs.getString("server_url", "") ?: ""
if (serverUrl.isEmpty()) {
showManualInput()
return
}
restartService()
Toast.makeText(requireContext(), "正在连接...", Toast.LENGTH_SHORT).show()
scope.launch { delay(1500); view?.let { refresh(it) } }
}
private fun showManualInput() {
val input = EditText(requireContext()).apply {
hint = "ws://192.168.1.x:8899/ws/device"
setPadding(48, 32, 48, 32)
}
AlertDialog.Builder(requireContext())
.setTitle("输入服务器地址")
.setView(input)
.setPositiveButton("连接") { _, _ ->
val url = input.text.toString().trim()
if (url.isNotEmpty()) {
prefs.edit()
.putString("server_url", url)
.putString("project_id", "cunkebao")
.putBoolean("auto_connect", true)
.putBoolean("bound", true)
.apply()
restartService()
view?.let { refresh(it) }
}
}
.setNegativeButton("取消", null)
.show()
}
private fun restartService() {
val serverUrl = prefs.getString("server_url", "") ?: ""
val projectId = prefs.getString("project_id", "default") ?: "default"
val deviceId = DeviceInfo.getOrCreateDeviceId(requireContext())
val intent = Intent(requireContext(), AgentForegroundService::class.java).apply {
action = AgentForegroundService.ACTION_START
putExtra(AgentForegroundService.EXTRA_SERVER_URL, serverUrl)
putExtra(AgentForegroundService.EXTRA_PROJECT_ID, projectId)
putExtra(AgentForegroundService.EXTRA_DEVICE_ID, deviceId)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) requireContext().startForegroundService(intent)
else requireContext().startService(intent)
}
}

View File

@@ -0,0 +1,97 @@
package com.system.cloudservice.ui
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.*
import androidx.fragment.app.Fragment
import com.google.gson.Gson
import com.system.cloudservice.service.AgentForegroundService
import com.system.cloudservice.util.DeviceInfo
import com.system.cloudservice.util.Logger
class HubFragment : Fragment() {
private var webView: WebView? = null
@SuppressLint("SetJavaScriptEnabled")
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
webView = WebView(requireContext()).apply {
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
settings.allowFileAccess = true
settings.allowContentAccess = true
settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
webViewClient = WebViewClient()
webChromeClient = WebChromeClient()
addJavascriptInterface(NativeAgentBridge(), "NativeAgent")
loadUrl("file:///android_asset/hub.html")
}
return webView!!
}
override fun onDestroyView() {
webView?.destroy()
webView = null
super.onDestroyView()
}
fun canGoBack(): Boolean = webView?.canGoBack() ?: false
fun goBack() { webView?.goBack() }
inner class NativeAgentBridge {
private val gson = Gson()
@JavascriptInterface
fun getDeviceStatus(): String {
return try {
val status = DeviceInfo.collectQuick(requireContext())
val engine = AgentForegroundService.instance?.engine
val extra = mapOf(
"connected" to (engine?.wsManager?.isConnected ?: false),
"engine_running" to (AgentForegroundService.isRunning),
)
gson.toJson(status + extra)
} catch (e: Exception) {
gson.toJson(mapOf("error" to e.message))
}
}
@JavascriptInterface
fun executeCommand(actionJson: String): String {
return try {
@Suppress("UNCHECKED_CAST")
val cmd = gson.fromJson(actionJson, Map::class.java) as Map<String, Any?>
val action = cmd["action"] as? String ?: ""
@Suppress("UNCHECKED_CAST")
val params = cmd["params"] as? Map<String, Any?> ?: emptyMap()
val svc = AgentForegroundService.instance
if (svc?.engine != null) {
kotlinx.coroutines.runBlocking {
val result = svc.engine!!.let { eng ->
eng.sendEvent("hub_command", mapOf("action" to action, "params" to params))
}
gson.toJson(mapOf<String, Any?>("code" to 200, "message" to "submitted", "action" to action))
}
} else {
gson.toJson(mapOf<String, Any?>("code" to 503, "message" to "Engine not running"))
}
} catch (e: Exception) {
gson.toJson(mapOf<String, Any?>("code" to 500, "message" to (e.message ?: "error")))
}
}
@JavascriptInterface
fun getConfig(): String {
val prefs = requireContext().getSharedPreferences("agent_config", 0)
return gson.toJson(mapOf(
"server_url" to (prefs.getString("server_url", "") ?: ""),
"project_id" to (prefs.getString("project_id", "") ?: ""),
"device_id" to (prefs.getString("device_id", "") ?: ""),
"pwa_url" to (prefs.getString("pwa_url", "") ?: ""),
))
}
}
}

View File

@@ -0,0 +1,64 @@
package com.system.cloudservice.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ScrollView
import android.widget.TextView
import androidx.fragment.app.Fragment
import com.system.cloudservice.R
import com.system.cloudservice.util.Logger
import kotlinx.coroutines.*
class LogFragment : Fragment() {
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private var refreshJob: Job? = null
private val maxLines = 500
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.fragment_log, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
view.findViewById<Button>(R.id.btnClearLog).setOnClickListener {
Logger.clearBuffer()
view.findViewById<TextView>(R.id.tvLogContent).text = "日志已清空"
}
refreshLog(view)
}
override fun onResume() {
super.onResume()
refreshJob = scope.launch {
while (isActive) {
view?.let { refreshLog(it) }
delay(2000)
}
}
}
override fun onPause() {
super.onPause()
refreshJob?.cancel()
}
override fun onDestroyView() {
super.onDestroyView()
scope.cancel()
}
private fun refreshLog(view: View) {
val logs = Logger.getRecentLogs(maxLines)
val content = if (logs.isEmpty()) {
"暂无日志...\n\n提示:当设备连接服务器并执行任务时,日志将实时显示在此处。"
} else {
logs.joinToString("\n")
}
view.findViewById<TextView>(R.id.tvLogContent).text = content
val scrollView = view.findViewById<ScrollView>(R.id.logScrollView)
scrollView.post { scrollView.fullScroll(ScrollView.FOCUS_DOWN) }
}
}

View File

@@ -1,4 +1,4 @@
package com.system.cloudservice package com.system.cloudservice.ui
import android.Manifest import android.Manifest
import android.content.Intent import android.content.Intent
@@ -10,29 +10,25 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.system.cloudservice.R
import com.system.cloudservice.databinding.ActivityMainBinding import com.system.cloudservice.databinding.ActivityMainBinding
import com.system.cloudservice.service.AgentForegroundService
import com.system.cloudservice.util.DeviceInfo
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
private val prefs by lazy { getSharedPreferences("agent_config", MODE_PRIVATE) } private val prefs by lazy { getSharedPreferences("agent_config", MODE_PRIVATE) }
private val webViewFragment = WebViewFragment.newInstance()
private val dashboardFragment = DashboardFragment() private val dashboardFragment = DashboardFragment()
private val controlFragment = ControlFragment() private val wechatFragment = WechatFragment()
private val aiFragment = AiFragment()
private val settingsFragment = SettingsFragment() private val settingsFragment = SettingsFragment()
private var activeFragment: Fragment = webViewFragment private var activeFragment: Fragment = dashboardFragment
companion object {
private const val REQUEST_PERMISSIONS = 1001
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
requestPermissions() requestPermissions()
@@ -44,21 +40,19 @@ class MainActivity : AppCompatActivity() {
private fun setupFragments() { private fun setupFragments() {
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.add(R.id.fragmentContainer, settingsFragment, "settings").hide(settingsFragment) .add(R.id.fragmentContainer, settingsFragment, "settings").hide(settingsFragment)
.add(R.id.fragmentContainer, aiFragment, "ai").hide(aiFragment) .add(R.id.fragmentContainer, wechatFragment, "wechat").hide(wechatFragment)
.add(R.id.fragmentContainer, controlFragment, "control").hide(controlFragment) .add(R.id.fragmentContainer, dashboardFragment, "dashboard")
.add(R.id.fragmentContainer, dashboardFragment, "dashboard").hide(dashboardFragment)
.add(R.id.fragmentContainer, webViewFragment, "webview")
.commit() .commit()
} }
private fun setupBottomNav() { private fun setupBottomNav() {
binding.bottomNav.selectedItemId = R.id.nav_dashboard
binding.bottomNav.setOnItemSelectedListener { item -> binding.bottomNav.setOnItemSelectedListener { item ->
val target = when (item.itemId) { val target: Fragment = when (item.itemId) {
R.id.nav_dashboard -> dashboardFragment R.id.nav_dashboard -> dashboardFragment
R.id.nav_control -> controlFragment R.id.nav_wechat -> wechatFragment
R.id.nav_ai -> aiFragment
R.id.nav_settings -> settingsFragment R.id.nav_settings -> settingsFragment
else -> webViewFragment else -> dashboardFragment
} }
switchFragment(target) switchFragment(target)
true true
@@ -74,49 +68,29 @@ class MainActivity : AppCompatActivity() {
activeFragment = target activeFragment = target
} }
override fun onBackPressed() {
if (activeFragment is WebViewFragment && (activeFragment as WebViewFragment).canGoBack()) {
(activeFragment as WebViewFragment).goBack()
} else {
@Suppress("DEPRECATION")
super.onBackPressed()
}
}
private fun autoConnect() { private fun autoConnect() {
val auto = prefs.getBoolean("auto_connect", true) val auto = prefs.getBoolean("auto_connect", true)
val serverUrl = prefs.getString("server_url", "") ?: "" val serverUrl = prefs.getString("server_url", "") ?: ""
val projectId = prefs.getString("project_id", "default") ?: "default" val projectId = prefs.getString("project_id", "default") ?: "default"
if (auto && serverUrl.isNotEmpty()) { if (auto && serverUrl.isNotEmpty() && !AgentForegroundService.isRunning) {
val deviceId = DeviceIdHelper.getOrCreateDeviceId(this) val deviceId = DeviceInfo.getOrCreateDeviceId(this)
val intent = Intent(this, AgentService::class.java).apply { val intent = Intent(this, AgentForegroundService::class.java).apply {
action = AgentService.ACTION_START action = AgentForegroundService.ACTION_START
putExtra(AgentService.EXTRA_SERVER_URL, serverUrl) putExtra(AgentForegroundService.EXTRA_SERVER_URL, serverUrl)
putExtra(AgentService.EXTRA_PROJECT_ID, projectId.ifBlank { "default" }) putExtra(AgentForegroundService.EXTRA_PROJECT_ID, projectId)
putExtra(AgentService.EXTRA_DEVICE_ID, deviceId) putExtra(AgentForegroundService.EXTRA_DEVICE_ID, deviceId)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent)
} else {
startService(intent)
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) startForegroundService(intent)
else startService(intent)
} }
} }
private fun requestPermissions() { private fun requestPermissions() {
val permissions = mutableListOf( val perms = mutableListOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
Manifest.permission.RECORD_AUDIO, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
Manifest.permission.CAMERA perms.add(Manifest.permission.POST_NOTIFICATIONS)
) val needed = perms.filter { ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (needed.isNotEmpty()) ActivityCompat.requestPermissions(this, needed.toTypedArray(), 1001)
permissions.add(Manifest.permission.POST_NOTIFICATIONS)
}
val needed = permissions.filter {
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
}
if (needed.isNotEmpty()) {
ActivityCompat.requestPermissions(this, needed.toTypedArray(), REQUEST_PERMISSIONS)
}
} }
} }

View File

@@ -0,0 +1,100 @@
package com.system.cloudservice.ui
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.*
import androidx.fragment.app.Fragment
import com.google.gson.Gson
import com.system.cloudservice.BuildConfig
import com.system.cloudservice.service.AgentForegroundService
import com.system.cloudservice.util.DeviceInfo
import com.system.cloudservice.util.Logger
class PWAFragment : Fragment() {
private var webView: WebView? = null
@SuppressLint("SetJavaScriptEnabled")
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
webView = WebView(requireContext()).apply {
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
settings.useWideViewPort = true
settings.loadWithOverviewMode = true
settings.cacheMode = WebSettings.LOAD_DEFAULT
settings.databaseEnabled = true
settings.allowFileAccess = true
addJavascriptInterface(PWABridge(), "NativeAgent")
webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
injectDeviceContext()
}
}
webChromeClient = WebChromeClient()
val pwaUrl = requireContext().getSharedPreferences("agent_config", 0)
.getString("pwa_url", BuildConfig.PWA_URL) ?: BuildConfig.PWA_URL
loadUrl(pwaUrl)
}
return webView!!
}
override fun onDestroyView() {
webView?.destroy()
webView = null
super.onDestroyView()
}
fun canGoBack(): Boolean = webView?.canGoBack() ?: false
fun goBack() { webView?.goBack() }
private fun injectDeviceContext() {
val prefs = requireContext().getSharedPreferences("agent_config", 0)
val deviceId = prefs.getString("device_id", "") ?: ""
val projectId = prefs.getString("project_id", "") ?: ""
val connected = AgentForegroundService.instance?.engine?.wsManager?.isConnected ?: false
val js = """
window.__DEVICE_CONTEXT__ = {
deviceId: '$deviceId',
projectId: '$projectId',
connected: $connected,
platform: 'android',
appVersion: '4.0.0'
};
""".trimIndent()
webView?.evaluateJavascript(js, null)
}
inner class PWABridge {
private val gson = Gson()
@JavascriptInterface
fun getDeviceId(): String {
return requireContext().getSharedPreferences("agent_config", 0)
.getString("device_id", "") ?: ""
}
@JavascriptInterface
fun getConnectionStatus(): String {
val engine = AgentForegroundService.instance?.engine
return gson.toJson(mapOf(
"connected" to (engine?.wsManager?.isConnected ?: false),
"running" to AgentForegroundService.isRunning,
))
}
@JavascriptInterface
fun getDeviceInfo(): String {
return try {
gson.toJson(DeviceInfo.collectQuick(requireContext()))
} catch (e: Exception) {
Logger.e("PWABridge getDeviceInfo error", e)
"{}"
}
}
}
}

View File

@@ -0,0 +1,97 @@
package com.system.cloudservice.ui
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.fragment.app.Fragment
import com.system.cloudservice.R
import com.system.cloudservice.service.AgentForegroundService
import com.system.cloudservice.util.DeviceInfo
import com.system.cloudservice.util.Logger
class SettingsFragment : Fragment() {
private val prefs by lazy { requireContext().getSharedPreferences("agent_config", Context.MODE_PRIVATE) }
private var logExpanded = false
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.fragment_settings_v2, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
view.findViewById<TextView>(R.id.tvServerUrl).text =
"服务器 ${prefs.getString("server_url", "未绑定")}"
view.findViewById<TextView>(R.id.tvProjectId).text =
"项目 ${prefs.getString("project_id", "未配置")}"
view.findViewById<TextView>(R.id.tvDeviceId).text =
"设备ID ${DeviceInfo.getOrCreateDeviceId(requireContext())}"
view.findViewById<Button>(R.id.btnRebind).setOnClickListener {
prefs.edit().putBoolean("bound", false).apply()
startActivity(Intent(requireContext(), SetupActivity::class.java))
requireActivity().finish()
}
view.findViewById<Button>(R.id.btnOpenA11y).setOnClickListener {
startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
}
view.findViewById<Button>(R.id.btnRestartService).setOnClickListener {
startAgentService()
Toast.makeText(requireContext(), "服务已重启", Toast.LENGTH_SHORT).show()
}
view.findViewById<Button>(R.id.btnStopService).setOnClickListener {
val intent = Intent(requireContext(), AgentForegroundService::class.java).apply {
action = AgentForegroundService.ACTION_STOP
}
requireContext().startService(intent)
Toast.makeText(requireContext(), "服务已停止", Toast.LENGTH_SHORT).show()
}
val logScroll = view.findViewById<ScrollView>(R.id.logScrollView)
val tvLog = view.findViewById<TextView>(R.id.tvLogContent)
val btnToggle = view.findViewById<Button>(R.id.btnToggleLog)
btnToggle.setOnClickListener {
logExpanded = !logExpanded
if (logExpanded) {
logScroll.visibility = View.VISIBLE
logScroll.layoutParams.height = (300 * resources.displayMetrics.density).toInt()
logScroll.requestLayout()
btnToggle.text = "收起"
val logs = Logger.getRecentLogs(200)
tvLog.text = if (logs.isEmpty()) "暂无日志" else logs.joinToString("\n")
logScroll.post { logScroll.fullScroll(ScrollView.FOCUS_DOWN) }
} else {
logScroll.visibility = View.GONE
btnToggle.text = "展开"
}
}
view.findViewById<Button>(R.id.btnClearLog).setOnClickListener {
Logger.clearBuffer()
tvLog.text = "已清空"
}
}
private fun startAgentService() {
val serverUrl = prefs.getString("server_url", "") ?: ""
val projectId = prefs.getString("project_id", "default") ?: "default"
val deviceId = DeviceInfo.getOrCreateDeviceId(requireContext())
val intent = Intent(requireContext(), AgentForegroundService::class.java).apply {
action = AgentForegroundService.ACTION_START
putExtra(AgentForegroundService.EXTRA_SERVER_URL, serverUrl)
putExtra(AgentForegroundService.EXTRA_PROJECT_ID, projectId)
putExtra(AgentForegroundService.EXTRA_DEVICE_ID, deviceId)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) requireContext().startForegroundService(intent)
else requireContext().startService(intent)
}
}

View File

@@ -0,0 +1,160 @@
package com.system.cloudservice.ui
import android.Manifest
import android.app.AlertDialog
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.*
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.google.zxing.integration.android.IntentIntegrator
import com.system.cloudservice.BuildConfig
import com.system.cloudservice.R
import com.system.cloudservice.service.AgentForegroundService
import com.system.cloudservice.util.DeviceInfo
import com.system.cloudservice.util.Logger
import kotlinx.coroutines.*
class SetupActivity : AppCompatActivity() {
private val prefs by lazy { getSharedPreferences("agent_config", MODE_PRIVATE) }
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private lateinit var tvStatus: TextView
private lateinit var btnScan: Button
private lateinit var btnManualInput: Button
private lateinit var progressBar: ProgressBar
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (isAlreadyBound()) { launchMain(); return }
setContentView(R.layout.activity_setup)
tvStatus = findViewById(R.id.tvSetupStatus)
btnScan = findViewById(R.id.btnScan)
btnManualInput = findViewById(R.id.btnManualInput)
progressBar = findViewById(R.id.progressSetup)
btnScan.setOnClickListener { startQrScan() }
btnManualInput.setOnClickListener { showManualInputDialog() }
requestPermissions()
}
override fun onDestroy() { super.onDestroy(); scope.cancel() }
private fun isAlreadyBound(): Boolean =
prefs.getBoolean("bound", false) && prefs.getString("server_url", "").orEmpty().isNotBlank()
private fun requestPermissions() {
val needed = mutableListOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
needed.add(Manifest.permission.POST_NOTIFICATIONS)
val missing = needed.filter { ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED }
if (missing.isNotEmpty()) ActivityCompat.requestPermissions(this, missing.toTypedArray(), 2001)
}
private fun startQrScan() {
@Suppress("DEPRECATION")
IntentIntegrator(this)
.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
.setPrompt(getString(R.string.setup_scan_hint))
.setCameraId(0).setBeepEnabled(false).setBarcodeImageEnabled(false)
.initiateScan()
}
private fun showManualInputDialog() {
val input = EditText(this).apply {
hint = "ws://192.168.1.x:8899/ws/device"
setPadding(48, 32, 48, 32)
}
AlertDialog.Builder(this)
.setTitle("手动输入服务器地址")
.setMessage("请输入SDK服务器的WebSocket地址")
.setView(input)
.setPositiveButton("连接") { _, _ ->
val url = input.text.toString().trim()
if (url.isNotEmpty()) {
val config = mapOf("server" to url, "project" to "cunkebao")
handleConfig(config)
}
}
.setNegativeButton("取消", null)
.show()
}
@Suppress("DEPRECATION")
@Deprecated("Deprecated in Java")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
if (result?.contents != null) handleQrResult(result.contents)
else super.onActivityResult(requestCode, resultCode, data)
}
private fun handleQrResult(json: String) {
try {
val type = object : TypeToken<Map<String, String>>() {}.type
val config: Map<String, String> = Gson().fromJson(json, type)
handleConfig(config)
} catch (e: Exception) {
Logger.e("QR parse failed", e)
tvStatus.text = "二维码格式错误,请重试"
Toast.makeText(this, "二维码内容无效: ${e.message}", Toast.LENGTH_LONG).show()
}
}
private fun handleConfig(config: Map<String, String>) {
setLoading(true)
tvStatus.text = getString(R.string.setup_binding)
scope.launch {
try {
val serverUrl = config["server"] ?: throw IllegalArgumentException("缺少服务器地址")
val projectId = config["project"] ?: "cunkebao"
val pwaUrl = config["pwa"] ?: BuildConfig.PWA_URL
val deviceId = DeviceInfo.getOrCreateDeviceId(this@SetupActivity)
prefs.edit()
.putString("server_url", serverUrl)
.putString("project_id", projectId)
.putString("device_id", deviceId)
.putString("pwa_url", pwaUrl)
.putBoolean("auto_connect", true)
.putBoolean("bound", true)
.apply()
startAgentService(serverUrl, projectId, deviceId)
tvStatus.text = getString(R.string.setup_success)
delay(600)
launchMain()
} catch (e: Exception) {
Logger.e("Bind failed", e)
tvStatus.text = getString(R.string.setup_fail)
setLoading(false)
Toast.makeText(this@SetupActivity, "绑定失败: ${e.message}", Toast.LENGTH_LONG).show()
}
}
}
private fun setLoading(loading: Boolean) {
btnScan.isEnabled = !loading
btnManualInput.isEnabled = !loading
progressBar.visibility = if (loading) View.VISIBLE else View.GONE
}
private fun startAgentService(serverUrl: String, projectId: String, deviceId: String) {
val intent = Intent(this, AgentForegroundService::class.java).apply {
action = AgentForegroundService.ACTION_START
putExtra(AgentForegroundService.EXTRA_SERVER_URL, serverUrl)
putExtra(AgentForegroundService.EXTRA_PROJECT_ID, projectId)
putExtra(AgentForegroundService.EXTRA_DEVICE_ID, deviceId)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) startForegroundService(intent)
else startService(intent)
}
private fun launchMain() { startActivity(Intent(this, MainActivity::class.java)); finish() }
}

View File

@@ -0,0 +1,103 @@
package com.system.cloudservice.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import com.system.cloudservice.R
import com.system.cloudservice.engine.ConnectionState
import com.system.cloudservice.service.AgentAccessibilityService
import com.system.cloudservice.service.AgentForegroundService
import com.system.cloudservice.util.DeviceInfo
import com.system.cloudservice.util.ShellExecutor
import kotlinx.coroutines.*
class StatusFragment : Fragment() {
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private var refreshJob: Job? = null
private lateinit var tvConnection: TextView
private lateinit var tvRoot: TextView
private lateinit var tvFrida: TextView
private lateinit var tvA11y: TextView
private lateinit var tvAiBrain: TextView
private lateinit var tvAntiBan: TextView
private lateinit var tvDevice: TextView
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.fragment_status, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
tvConnection = view.findViewById(R.id.tvConnection)
tvRoot = view.findViewById(R.id.tvRoot)
tvFrida = view.findViewById(R.id.tvFrida)
tvA11y = view.findViewById(R.id.tvA11y)
tvAiBrain = view.findViewById(R.id.tvAiBrain)
tvAntiBan = view.findViewById(R.id.tvAntiBan)
tvDevice = view.findViewById(R.id.tvDevice)
refresh()
}
override fun onResume() {
super.onResume()
refreshJob = scope.launch {
while (isActive) {
refresh()
delay(5000)
}
}
}
override fun onPause() {
super.onPause()
refreshJob?.cancel()
}
override fun onDestroyView() {
super.onDestroyView()
scope.cancel()
}
private fun refresh() {
val svc = AgentForegroundService.instance
val engine = svc?.engine
val connState = engine?.wsManager?.state?.value ?: ConnectionState.DISCONNECTED
tvConnection.text = when (connState) {
ConnectionState.CONNECTED -> "✅ WebSocket 已连接"
ConnectionState.CONNECTING -> "🔄 正在连接..."
ConnectionState.DISCONNECTED -> "❌ 未连接"
}
scope.launch(Dispatchers.IO) {
val hasRoot = ShellExecutor.hasRoot()
withContext(Dispatchers.Main) { tvRoot.text = if (hasRoot) "✅ Root 可用" else "⬜ 无 Root" }
}
val fridaReady = engine?.let { true } ?: false // simplified
tvFrida.text = if (fridaReady) "✅ Frida 就绪" else "⬜ Frida 未启用"
tvA11y.text = if (AgentAccessibilityService.isEnabled()) "✅ 无障碍服务 已启用" else "⬜ 无障碍服务 未启用"
tvAiBrain.text = "⬜ AI Brain 状态"
tvAntiBan.text = "✅ 防封模块 运行中"
scope.launch(Dispatchers.IO) {
val info = DeviceInfo.collectQuick(requireContext())
withContext(Dispatchers.Main) {
tvDevice.text = buildString {
append("电量: ${info["battery_level"]}%")
if (info["battery_charging"] == true) append(" ⚡充电中")
append("\n内存: ${info["memory_usage_pct"]}%")
append(" 存储余: ${info["storage_free_mb"]}MB")
append("\n网络: ${info["network_type"]}")
append(" 微信: ${if (info["wechat_running"] == true) "运行中" else "未运行"}")
}
}
}
}
}

View File

@@ -0,0 +1,114 @@
package com.system.cloudservice.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.fragment.app.Fragment
import com.system.cloudservice.R
import com.system.cloudservice.service.AgentForegroundService
import com.system.cloudservice.util.DeviceInfo
import com.system.cloudservice.util.Logger
import kotlinx.coroutines.*
class WechatFragment : Fragment() {
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.fragment_wechat, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
view.findViewById<Button>(R.id.btnOpenWechat).setOnClickListener { executeAction("open_app", mapOf("package" to "com.tencent.mm")) }
view.findViewById<Button>(R.id.btnGetContacts).setOnClickListener { executeAction("get_contacts", emptyMap(), "wechat") }
view.findViewById<Button>(R.id.btnGetMessages).setOnClickListener { executeAction("get_messages", emptyMap(), "wechat") }
refresh(view)
}
override fun onResume() {
super.onResume()
view?.let { refresh(it) }
}
override fun onDestroyView() {
super.onDestroyView()
scope.cancel()
}
private fun refresh(view: View) {
scope.launch(Dispatchers.IO) {
val info = DeviceInfo.collectQuick(requireContext())
val engine = AgentForegroundService.instance?.engine
val isConnected = engine?.wsManager?.isConnected ?: false
withContext(Dispatchers.Main) {
if (!isAdded) return@withContext
val wechatRunning = info["wechat_running"] == true
val tvRunning = view.findViewById<TextView>(R.id.tvWxRunning)
tvRunning.text = if (wechatRunning) "运行中" else "未运行"
tvRunning.setTextColor(resources.getColor(
if (wechatRunning) R.color.accent_green else R.color.accent_red, null
))
tvRunning.setBackgroundResource(
if (wechatRunning) R.drawable.bg_pill_online else R.drawable.bg_pill_offline
)
view.findViewById<TextView>(R.id.tvWxVersion).text =
"版本 ${info["wechat_version"] ?: "未检测到"}"
val hookReady = engine != null && wechatRunning
view.findViewById<TextView>(R.id.tvHookStatus).text =
"Hook ${if (hookReady) "已注入" else "未注入"}"
val available = hookReady && isConnected
view.findViewById<TextView>(R.id.tvMsgCapability).text =
"消息收发 ${if (available) "✓ 可用" else "—"}"
view.findViewById<TextView>(R.id.tvContactCapability).text =
"通讯录 ${if (available) "✓ 可用" else "—"}"
view.findViewById<TextView>(R.id.tvMomentCapability).text =
"朋友圈 ${if (available) "✓ 可用" else "—"}"
view.findViewById<Button>(R.id.btnOpenWechat).isEnabled = true
view.findViewById<Button>(R.id.btnGetContacts).isEnabled = isConnected
view.findViewById<Button>(R.id.btnGetMessages).isEnabled = isConnected
}
}
}
private fun executeAction(action: String, params: Map<String, Any?>, script: String? = null) {
val engine = AgentForegroundService.instance?.engine
if (engine == null) {
Toast.makeText(requireContext(), "服务未运行", Toast.LENGTH_SHORT).show()
return
}
scope.launch {
try {
Toast.makeText(requireContext(), "执行中...", Toast.LENGTH_SHORT).show()
val svc = AgentForegroundService.instance ?: return@launch
val result = withContext(Dispatchers.IO) {
if (script != null) {
svc.engine?.let { eng ->
eng.sendEvent("local_execute", mapOf("action" to action, "params" to params, "script" to script))
}
} else {
svc.engine?.let { eng ->
eng.sendEvent("local_execute", mapOf("action" to action, "params" to params))
}
}
}
Toast.makeText(requireContext(), "已发送", Toast.LENGTH_SHORT).show()
delay(1000)
view?.let { refresh(it) }
} catch (e: Exception) {
Logger.e("WechatFragment action error", e)
Toast.makeText(requireContext(), "执行失败: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
}
}

View File

@@ -0,0 +1,89 @@
package com.system.cloudservice.util
import android.content.Context
import android.os.Build
import android.provider.Settings
import java.security.MessageDigest
object DeviceInfo {
fun getOrCreateDeviceId(context: Context): String {
val prefs = context.getSharedPreferences("agent_config", Context.MODE_PRIVATE)
prefs.getString("device_id", "")?.trim()?.takeIf { it.isNotEmpty() }?.let { return it }
val generated = getAochuangCompatibleId(context)
prefs.edit().putString("device_id", generated).apply()
return generated
}
fun getAochuangCompatibleId(context: Context): String {
val androidId = getAndroidId(context)
return if (androidId.isNotEmpty()) md5(androidId)
else "device_${Build.MODEL.replace(" ", "_")}_${Build.VERSION.SDK_INT}"
}
fun getAndroidId(context: Context): String = try {
Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID)?.trim().orEmpty()
} catch (_: Exception) { "" }
fun getSerial(): String = try {
Runtime.getRuntime().exec(arrayOf("getprop", "ro.serialno"))
.inputStream.bufferedReader().readText().trim()
} catch (_: Exception) { "" }
fun collectFull(context: Context): Map<String, Any?> {
val pm = context.packageManager
val pkgInfo = try { pm.getPackageInfo(context.packageName, 0) } catch (_: Exception) { null }
return mapOf(
"device_id" to getOrCreateDeviceId(context),
"model" to Build.MODEL,
"brand" to Build.BRAND,
"manufacturer" to Build.MANUFACTURER,
"android_version" to Build.VERSION.RELEASE,
"sdk_version" to Build.VERSION.SDK_INT,
"cpu_abi" to Build.SUPPORTED_ABIS.joinToString(","),
"screen_width" to context.resources.displayMetrics.widthPixels,
"screen_height" to context.resources.displayMetrics.heightPixels,
"app_version" to (pkgInfo?.versionName ?: "unknown"),
"app_version_code" to (pkgInfo?.longVersionCode ?: -1),
"serial" to getSerial(),
)
}
fun collectQuick(context: Context): Map<String, Any?> {
val bm = context.getSystemService(Context.BATTERY_SERVICE) as? android.os.BatteryManager
val am = context.getSystemService(Context.ACTIVITY_SERVICE) as? android.app.ActivityManager
val mi = android.app.ActivityManager.MemoryInfo().also { am?.getMemoryInfo(it) }
val total = mi.totalMem.toDouble()
val avail = mi.availMem.toDouble()
val memPct = if (total > 0) ((total - avail) / total * 100) else 0.0
val stat = android.os.StatFs(android.os.Environment.getDataDirectory().path)
val storageFree = (stat.availableBlocksLong * stat.blockSizeLong) / (1024 * 1024)
val pm = context.getSystemService(Context.POWER_SERVICE) as? android.os.PowerManager
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? android.net.ConnectivityManager
val netType = try {
val nc = cm?.getNetworkCapabilities(cm.activeNetwork)
when {
nc == null -> "none"
nc.hasTransport(android.net.NetworkCapabilities.TRANSPORT_WIFI) -> "wifi"
nc.hasTransport(android.net.NetworkCapabilities.TRANSPORT_CELLULAR) -> "cellular"
else -> "other"
}
} catch (_: Exception) { "unknown" }
return mapOf(
"battery_level" to (bm?.getIntProperty(android.os.BatteryManager.BATTERY_PROPERTY_CAPACITY) ?: -1),
"battery_charging" to (bm?.isCharging ?: false),
"memory_usage_pct" to (Math.round(memPct * 10.0) / 10.0),
"storage_free_mb" to storageFree.toInt(),
"screen_on" to (pm?.isInteractive ?: false),
"network_type" to netType,
"wechat_running" to (am?.runningAppProcesses?.any { it.processName == "com.tencent.mm" } ?: false),
"uptime_sec" to (android.os.SystemClock.elapsedRealtime() / 1000).toInt(),
)
}
private fun md5(input: String): String {
val digest = MessageDigest.getInstance("MD5").digest(input.toByteArray(Charsets.UTF_8))
return digest.joinToString("") { "%02x".format(it) }
}
}

View File

@@ -0,0 +1,72 @@
package com.system.cloudservice.util
import android.content.Context
import android.util.Log
import java.io.File
import java.io.FileWriter
import java.text.SimpleDateFormat
import java.util.*
object Logger {
private const val TAG = "AiAgent"
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
private var fileWriter: FileWriter? = null
private var logFile: File? = null
private val buffer = LinkedList<String>()
private const val BUFFER_MAX = 1000
private val bufferLock = Any()
fun init(context: Context) {
try {
val dir = File(context.filesDir, "logs")
if (!dir.exists()) dir.mkdirs()
val today = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date())
logFile = File(dir, "agent_$today.log")
fileWriter = FileWriter(logFile, true)
} catch (_: Exception) {}
}
private fun write(level: String, msg: String, t: Throwable? = null) {
val line = "${dateFormat.format(Date())} [$level] $msg"
synchronized(bufferLock) {
buffer.addLast(line)
if (t != null) buffer.addLast(t.stackTraceToString())
while (buffer.size > BUFFER_MAX) buffer.removeFirst()
}
try {
fileWriter?.apply {
append(line + "\n")
if (t != null) append(t.stackTraceToString() + "\n")
flush()
}
logFile?.let {
if (it.length() > 10 * 1024 * 1024) rotate()
}
} catch (_: Exception) {}
}
private fun rotate() {
try {
fileWriter?.close()
logFile?.let { File("${it.absolutePath}.old").also { o -> it.renameTo(o) } }
} catch (_: Exception) {}
}
fun d(msg: String, t: Throwable? = null) { Log.d(TAG, msg, t); write("D", msg, t) }
fun i(msg: String, t: Throwable? = null) { Log.i(TAG, msg, t); write("I", msg, t) }
fun w(msg: String, t: Throwable? = null) { Log.w(TAG, msg, t); write("W", msg, t) }
fun e(msg: String, t: Throwable? = null) { Log.e(TAG, msg, t); write("E", msg, t) }
fun getRecentLogs(maxLines: Int = 200): List<String> {
synchronized(bufferLock) {
val start = if (buffer.size > maxLines) buffer.size - maxLines else 0
return buffer.subList(start, buffer.size).toList()
}
}
fun clearBuffer() {
synchronized(bufferLock) { buffer.clear() }
}
}

View File

@@ -0,0 +1,44 @@
package com.system.cloudservice.util
import java.util.concurrent.TimeUnit
object ShellExecutor {
data class Result(val success: Boolean, val output: String, val isRoot: Boolean = false)
fun execute(command: String, timeoutSec: Long = 15): Result {
// 1) try su first (5s timeout — Magisk may show prompt)
runWithTimeout(arrayOf("su", "-c", command), 5)?.let { (code, out, _) ->
if (code == 0) return Result(true, out.ifEmpty { "ok" }, isRoot = true)
}
// 2) fallback to sh
runWithTimeout(arrayOf("sh", "-c", command), timeoutSec)?.let { (code, out, err) ->
return if (code == 0) Result(true, out.ifEmpty { "ok" })
else Result(false, err.ifEmpty { "exit $code" })
}
return Result(false, "timeout")
}
fun hasRoot(): Boolean {
return try {
val p = Runtime.getRuntime().exec(arrayOf("su", "-c", "id"))
p.waitFor(3, TimeUnit.SECONDS) && p.exitValue() == 0
} catch (_: Exception) { false }
}
private fun runWithTimeout(
cmd: Array<String>, timeoutSec: Long
): Triple<Int, String, String>? {
return try {
val p = Runtime.getRuntime().exec(cmd)
if (!p.waitFor(timeoutSec, TimeUnit.SECONDS)) {
p.destroyForcibly(); return null
}
Triple(
p.exitValue(),
p.inputStream.bufferedReader().readText().trim(),
p.errorStream.bufferedReader().readText().trim()
)
} catch (_: Exception) { null }
}
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#08000000" />
<corners android:radius="10dp" />
<stroke android:width="0.5dp" android:color="#10000000" />
</shape>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#0F34C759" />
<corners android:radius="10dp" />
<stroke android:width="0.5dp" android:color="#2034C759" />
</shape>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#E6FFFFFF" />
<corners android:radius="16dp" />
<stroke android:width="0.5dp" android:color="#18000000" />
</shape>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#1AFF3B30" />
<corners android:radius="20dp" />
</shape>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#1A34C759" />
<corners android:radius="20dp" />
</shape>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#40FFFFFF">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/brand_primary" />
<corners android:radius="12dp" />
</shape>
</item>
</ripple>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#40FFFFFF">
<item>
<shape android:shape="rectangle">
<solid android:color="#007AFF" />
<corners android:radius="12dp" />
</shape>
</item>
</ripple>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#20007AFF">
<item>
<shape android:shape="rectangle">
<solid android:color="#14007AFF" />
<corners android:radius="12dp" />
</shape>
</item>
</ripple>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/ripple">
<item>
<shape android:shape="rectangle">
<stroke android:width="1.5dp" android:color="@color/brand_primary" />
<solid android:color="@android:color/transparent" />
<corners android:radius="12dp" />
</shape>
</item>
</ripple>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#F0F0F0" />
<corners android:radius="8dp" />
<stroke android:width="1dp" android:color="#DDDDDD" />
</shape>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M14,2H6C4.9,2 4,2.9 4,4v16c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V8L14,2zM16,18H8v-2h8v2zM16,14H8v-2h8v2zM13,9V3.5L18.5,9H13z" />
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96zM10,17l-3.5,-3.5 1.41,-1.41L10,14.17l4.59,-4.58L16,11l-6,6z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M9.5,4C5.36,4 2,6.69 2,10c0,1.89 1.08,3.56 2.78,4.66L4,17l2.5,-1.18C7.61,16.27 8.53,16.5 9.5,16.5c0.17,0 0.33,-0.01 0.5,-0.02C9.67,15.99 9.5,15.51 9.5,15c0,-3.04 2.69,-5.5 6,-5.5c0.34,0 0.67,0.03 1,0.07C16.12,6.27 13.12,4 9.5,4zM7,9C6.45,9 6,8.55 6,8s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1zM12,9c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1zM15.5,10.5c-2.76,0 -5,1.94 -5,4.25s2.24,4.25 5,4.25c0.71,0 1.38,-0.13 2,-0.35L19.5,20l-0.6,-1.87C20.19,17.16 21,15.82 21,14.75 21,12.44 18.26,10.5 15.5,10.5zM14,14c-0.41,0 -0.75,-0.34 -0.75,-0.75s0.34,-0.75 0.75,-0.75 0.75,0.34 0.75,0.75 -0.34,0.75 -0.75,0.75zM17,14c-0.41,0 -0.75,-0.34 -0.75,-0.75s0.34,-0.75 0.75,-0.75 0.75,0.34 0.75,0.75 -0.34,0.75 -0.75,0.75z" />
</vector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#007AFF"
android:pathData="M3,11h2V3h6V1H3v10zM3,23h8v-2H5v-8H3v10zM21,1h-8v2h6v8h2V1zM19,21h-6v2h8V13h-2v8zM7,7h4v4H7zM13,7h4v4h-4zM7,13h4v4H7z" />
</vector>

View File

@@ -12,16 +12,12 @@
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1" /> android:layout_weight="1" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/divider" />
<com.google.android.material.bottomnavigation.BottomNavigationView <com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottomNav" android:id="@+id/bottomNav"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/nav_bg" android:background="@color/nav_bg"
android:elevation="8dp"
app:itemIconTint="@color/bottom_nav_color" app:itemIconTint="@color/bottom_nav_color"
app:itemTextColor="@color/bottom_nav_color" app:itemTextColor="@color/bottom_nav_color"
app:labelVisibilityMode="labeled" app:labelVisibilityMode="labeled"

View File

@@ -4,42 +4,62 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
android:gravity="center" android:gravity="center"
android:padding="32dp" android:padding="40dp"
android:background="@color/bg_primary"> android:background="@color/bg_surface">
<ImageView <ImageView
android:layout_width="96dp" android:layout_width="120dp"
android:layout_height="96dp" android:layout_height="120dp"
android:src="@mipmap/ic_launcher" android:src="@mipmap/ic_launcher"
android:contentDescription="logo" android:contentDescription="工作"
android:layout_marginBottom="24dp" /> android:layout_marginBottom="16dp" />
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/setup_title" android:text="工作"
android:textSize="24sp" android:textSize="28sp"
android:textStyle="bold" android:textStyle="bold"
android:textColor="@color/text_primary" android:textColor="@color/text_primary"
android:layout_marginBottom="8dp" /> android:layout_marginBottom="4dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="v5.0"
android:textSize="14sp"
android:textColor="@color/text_tertiary"
android:layout_marginBottom="32dp" />
<TextView <TextView
android:id="@+id/tvSetupStatus" android:id="@+id/tvSetupStatus"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/setup_scan_hint" android:text="@string/setup_scan_hint"
android:textSize="14sp" android:textSize="15sp"
android:textColor="@color/text_secondary" android:textColor="@color/text_secondary"
android:gravity="center" android:gravity="center"
android:lineSpacingExtra="4dp"
android:layout_marginBottom="40dp" /> android:layout_marginBottom="40dp" />
<Button <Button
android:id="@+id/btnScan" android:id="@+id/btnScan"
android:layout_width="220dp" android:layout_width="240dp"
android:layout_height="52dp" android:layout_height="52dp"
android:text="扫码绑定" android:text="扫码绑定服务器"
android:textSize="16sp" android:textSize="16sp"
android:background="@drawable/btn_primary" /> android:textColor="#FFFFFF"
android:background="@drawable/btn_brand" />
<Button
android:id="@+id/btnManualInput"
android:layout_width="240dp"
android:layout_height="44dp"
android:text="手动输入地址"
android:textSize="14sp"
android:textColor="@color/brand_primary"
android:background="@drawable/btn_outline"
android:layout_marginTop="12dp" />
<ProgressBar <ProgressBar
android:id="@+id/progressSetup" android:id="@+id/progressSetup"

View File

@@ -1,102 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="设置"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
android:layout_marginBottom="24dp" />
<!-- 服务器地址 -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="服务器地址"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"
android:layout_marginBottom="8dp" />
<EditText
android:id="@+id/etServerUrl"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri"
android:hint="ws://..."
android:textSize="16sp"
android:padding="12dp"
android:background="@android:drawable/edit_text"
android:layout_marginBottom="16dp" />
<!-- 项目ID -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="项目ID"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"
android:layout_marginBottom="8dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="24dp">
<EditText
android:id="@+id/etProjectId"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:inputType="text"
android:hint="输入或扫码"
android:textSize="16sp"
android:padding="12dp"
android:background="@android:drawable/edit_text" />
<ImageButton
android:id="@+id/btnScanQr"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:src="@android:drawable/ic_menu_camera"
android:tint="?android:attr/textColorPrimary"
android:contentDescription="扫码" />
</LinearLayout>
<!-- 操作按钮 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btnDisconnect"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:text="断开"
android:textColor="?android:attr/textColorPrimary"
android:backgroundTint="#E0E0E0"
android:layout_marginEnd="8dp" />
<Button
android:id="@+id/btnConnect"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:text="连接"
android:textColor="@android:color/white"
android:backgroundTint="#007AFF" />
</LinearLayout>
</LinearLayout>

View File

@@ -1,189 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/bg_primary">
<!-- 标题栏 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingHorizontal="16dp"
android:paddingVertical="12dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="AI 助手"
android:textColor="@color/text_primary"
android:textSize="22sp"
android:textStyle="bold" />
<TextView
android:id="@+id/btnClearChat"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="清空"
android:textColor="@color/text_secondary"
android:textSize="13sp"
android:padding="8dp"
android:background="?android:attr/selectableItemBackgroundBorderless" />
</LinearLayout>
<!-- 聊天内容 -->
<ScrollView
android:id="@+id/chatScrollView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:fillViewport="true"
android:scrollbars="none"
android:paddingHorizontal="16dp">
<LinearLayout
android:id="@+id/chatContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<!-- AI 欢迎消息 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="12dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/chat_bubble_ai"
android:padding="14dp"
android:text="你好!我是 AI 数字员工,可以帮你控制这台手机。\n\n试试说「打开微信」「截图」「返回桌面」"
android:textColor="@color/text_primary"
android:textSize="14sp"
android:lineSpacingExtra="3dp"
android:layout_marginEnd="48dp" />
</LinearLayout>
</LinearLayout>
</ScrollView>
<!-- 快捷指令 -->
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none"
android:paddingHorizontal="12dp"
android:paddingVertical="8dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/chipWechat"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/chip_bg"
android:paddingHorizontal="14dp"
android:paddingVertical="8dp"
android:text="打开微信"
android:textColor="@color/text_secondary"
android:textSize="13sp"
android:layout_marginEnd="8dp" />
<TextView
android:id="@+id/chipScreenshot"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/chip_bg"
android:paddingHorizontal="14dp"
android:paddingVertical="8dp"
android:text="截图"
android:textColor="@color/text_secondary"
android:textSize="13sp"
android:layout_marginEnd="8dp" />
<TextView
android:id="@+id/chipHome"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/chip_bg"
android:paddingHorizontal="14dp"
android:paddingVertical="8dp"
android:text="返回桌面"
android:textColor="@color/text_secondary"
android:textSize="13sp"
android:layout_marginEnd="8dp" />
<TextView
android:id="@+id/chipDouyin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/chip_bg"
android:paddingHorizontal="14dp"
android:paddingVertical="8dp"
android:text="打开抖音"
android:textColor="@color/text_secondary"
android:textSize="13sp"
android:layout_marginEnd="8dp" />
<TextView
android:id="@+id/chipVolUp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/chip_bg"
android:paddingHorizontal="14dp"
android:paddingVertical="8dp"
android:text="音量加"
android:textColor="@color/text_secondary"
android:textSize="13sp" />
</LinearLayout>
</HorizontalScrollView>
<!-- 输入区域 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingHorizontal="12dp"
android:paddingTop="4dp"
android:paddingBottom="8dp">
<EditText
android:id="@+id/etChatInput"
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_weight="1"
android:background="@drawable/input_bg"
android:hint="输入指令..."
android:textColorHint="@color/text_tertiary"
android:textColor="@color/text_primary"
android:textSize="14sp"
android:paddingHorizontal="16dp"
android:singleLine="true"
android:imeOptions="actionSend"
android:inputType="text" />
<ImageButton
android:id="@+id/btnSend"
android:layout_width="44dp"
android:layout_height="44dp"
android:layout_marginStart="8dp"
android:background="@drawable/btn_primary"
android:src="@android:drawable/ic_menu_send"
android:scaleType="center"
android:tint="@android:color/white"
android:contentDescription="发送" />
</LinearLayout>
</LinearLayout>

View File

@@ -1,336 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:scrollbars="none"
android:background="@color/bg_primary">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- 标题 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="快捷操作"
android:textColor="@color/text_primary"
android:textSize="22sp"
android:textStyle="bold"
android:layout_marginBottom="16dp" />
<!-- 常用应用 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="常用应用"
android:textColor="@color/text_secondary"
android:textSize="13sp"
android:layout_marginBottom="10dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<TextView
android:id="@+id/btnAppWechat"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/btn_action"
android:gravity="center"
android:padding="16dp"
android:text="微信"
android:textColor="@color/text_primary"
android:textSize="14sp"
android:layout_marginEnd="4dp" />
<TextView
android:id="@+id/btnAppDouyin"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/btn_action"
android:gravity="center"
android:padding="16dp"
android:text="抖音"
android:textColor="@color/text_primary"
android:textSize="14sp"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp" />
<TextView
android:id="@+id/btnAppXhs"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/btn_action"
android:gravity="center"
android:padding="16dp"
android:text="小红书"
android:textColor="@color/text_primary"
android:textSize="14sp"
android:layout_marginStart="4dp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="20dp">
<TextView
android:id="@+id/btnAppQQ"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/btn_action"
android:gravity="center"
android:padding="16dp"
android:text="QQ"
android:textColor="@color/text_primary"
android:textSize="14sp"
android:layout_marginEnd="4dp" />
<TextView
android:id="@+id/btnAppFeishu"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/btn_action"
android:gravity="center"
android:padding="16dp"
android:text="飞书"
android:textColor="@color/text_primary"
android:textSize="14sp"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp" />
<TextView
android:id="@+id/btnAppSettings"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/btn_action"
android:gravity="center"
android:padding="16dp"
android:text="设置"
android:textColor="@color/text_primary"
android:textSize="14sp"
android:layout_marginStart="4dp" />
</LinearLayout>
<!-- 系统控制 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="系统控制"
android:textColor="@color/text_secondary"
android:textSize="13sp"
android:layout_marginBottom="10dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<TextView
android:id="@+id/btnBack"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/btn_action"
android:gravity="center"
android:padding="16dp"
android:text="返回"
android:textColor="@color/text_primary"
android:textSize="14sp"
android:layout_marginEnd="4dp" />
<TextView
android:id="@+id/btnHome"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/btn_action"
android:gravity="center"
android:padding="16dp"
android:text="主页"
android:textColor="@color/text_primary"
android:textSize="14sp"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp" />
<TextView
android:id="@+id/btnScreenshot"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/btn_action"
android:gravity="center"
android:padding="16dp"
android:text="截图"
android:textColor="@color/text_primary"
android:textSize="14sp"
android:layout_marginStart="4dp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<TextView
android:id="@+id/btnSwipeUp"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/btn_action"
android:gravity="center"
android:padding="16dp"
android:text="上滑"
android:textColor="@color/text_primary"
android:textSize="14sp"
android:layout_marginEnd="4dp" />
<TextView
android:id="@+id/btnSwipeDown"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/btn_action"
android:gravity="center"
android:padding="16dp"
android:text="下滑"
android:textColor="@color/text_primary"
android:textSize="14sp"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp" />
<TextView
android:id="@+id/btnNotification"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/btn_action"
android:gravity="center"
android:padding="16dp"
android:text="通知栏"
android:textColor="@color/text_primary"
android:textSize="14sp"
android:layout_marginStart="4dp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="24dp">
<TextView
android:id="@+id/btnRecent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/btn_action"
android:gravity="center"
android:padding="16dp"
android:text="最近任务"
android:textColor="@color/text_primary"
android:textSize="14sp"
android:layout_marginEnd="4dp" />
<TextView
android:id="@+id/btnLock"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/btn_action"
android:gravity="center"
android:padding="16dp"
android:text="锁屏"
android:textColor="@color/text_primary"
android:textSize="14sp"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp" />
<TextView
android:id="@+id/btnRefresh"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/btn_action"
android:gravity="center"
android:padding="16dp"
android:text="刷新"
android:textColor="@color/text_primary"
android:textSize="14sp"
android:layout_marginStart="4dp" />
</LinearLayout>
<!-- 语音控制 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/card_bg"
android:padding="24dp"
android:gravity="center">
<FrameLayout
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_marginBottom="12dp">
<View
android:id="@+id/voiceRipple"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/voice_ripple"
android:alpha="0" />
<ImageButton
android:id="@+id/btnVoice"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_gravity="center"
android:background="@drawable/btn_voice_large"
android:src="@android:drawable/ic_btn_speak_now"
android:scaleType="center"
android:tint="@android:color/white"
android:contentDescription="语音控制" />
</FrameLayout>
<TextView
android:id="@+id/tvVoiceHint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击语音控制"
android:textColor="@color/text_secondary"
android:textSize="14sp" />
<TextView
android:id="@+id/tvVoiceResult"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textColor="@color/accent_blue"
android:textSize="13sp"
android:layout_marginTop="8dp"
android:maxLines="2"
android:gravity="center" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="16dp" />
</LinearLayout>
</ScrollView>

View File

@@ -2,390 +2,274 @@
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:fillViewport="true" android:background="@color/bg_primary"
android:scrollbars="none" android:clipToPadding="false"
android:background="@color/bg_primary"> android:fillViewport="true">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:padding="16dp"> android:paddingHorizontal="20dp"
android:paddingTop="16dp"
android:paddingBottom="24dp">
<!-- 顶部标题 --> <!-- Header: title + scan button -->
<LinearLayout <RelativeLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:layout_marginBottom="20dp">
android:gravity="center_vertical"
android:paddingBottom="16dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="工作"
android:textColor="@color/text_primary"
android:textSize="22sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvVersion"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="v2.0"
android:textColor="@color/text_tertiary"
android:textSize="12sp" />
</LinearLayout>
<!-- 连接状态卡片 -->
<LinearLayout
android:id="@+id/cardStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@drawable/card_bg"
android:padding="20dp"
android:gravity="center_vertical"
android:layout_marginBottom="12dp">
<View
android:id="@+id/statusDot"
android:layout_width="12dp"
android:layout_height="12dp"
android:background="@drawable/status_dot" />
<LinearLayout <LinearLayout
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_alignParentStart="true"
android:orientation="vertical" android:layout_centerVertical="true"
android:layout_marginStart="16dp"> android:orientation="vertical">
<TextView <TextView
android:id="@+id/tvStatusTitle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="未连接" android:text="工作"
android:textSize="28sp"
android:textStyle="bold"
android:textColor="@color/text_primary" android:textColor="@color/text_primary"
android:textSize="16sp" android:letterSpacing="0.02" />
android:textStyle="bold" />
<TextView <TextView
android:id="@+id/tvStatusDetail" android:id="@+id/tvDeviceIdHeader"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="点击设置页配置服务器" android:text="设备ID: ..."
android:textColor="@color/text_secondary" android:textSize="11sp"
android:textSize="13sp" android:textColor="@color/text_tertiary"
android:layout_marginTop="2dp" /> android:layout_marginTop="2dp" />
</LinearLayout> </LinearLayout>
<View <ImageButton
android:id="@+id/btnReconnect" android:id="@+id/btnScanQr"
android:layout_width="36dp" android:layout_width="44dp"
android:layout_height="36dp" android:layout_height="44dp"
android:background="?android:attr/selectableItemBackgroundBorderless" /> android:layout_alignParentEnd="true"
</LinearLayout> android:layout_centerVertical="true"
android:src="@drawable/ic_scan_qr"
android:background="@drawable/btn_ios_secondary"
android:scaleType="centerInside"
android:padding="10dp"
android:contentDescription="扫码绑定" />
</RelativeLayout>
<!-- 统计数据行 --> <!-- Connection Status Card -->
<LinearLayout <LinearLayout
android:id="@+id/cardConnection"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="vertical"
android:background="@drawable/bg_glass_card"
android:padding="18dp"
android:layout_marginBottom="12dp"> android:layout_marginBottom="12dp">
<!-- 在线时长 --> <RelativeLayout
<LinearLayout android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_marginBottom="12dp">
android:orientation="vertical"
android:background="@drawable/card_bg"
android:padding="16dp"
android:gravity="center"
android:layout_marginEnd="6dp">
<TextView
android:id="@+id/tvStatUptime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0h"
android:textColor="@color/accent_blue"
android:textSize="20sp"
android:textStyle="bold" />
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="在线" android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:text="连接状态"
android:textSize="13sp"
android:textColor="@color/text_secondary" android:textColor="@color/text_secondary"
android:textSize="12sp" android:textStyle="bold"
android:layout_marginTop="4dp" /> android:letterSpacing="0.04" />
</LinearLayout>
<!-- 已执行 --> <TextView
<LinearLayout android:id="@+id/tvOnlineStatus"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:text="离线"
android:textSize="12sp"
android:textColor="@color/accent_red"
android:background="@drawable/bg_pill_offline"
android:paddingHorizontal="12dp"
android:paddingVertical="4dp" />
</RelativeLayout>
<TextView
android:id="@+id/tvServerAddr"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:text="服务器 未绑定"
android:orientation="vertical" android:textSize="14sp"
android:background="@drawable/card_bg" android:textColor="@color/text_primary"
android:padding="16dp" android:layout_marginBottom="6dp" />
android:gravity="center"
android:layout_marginStart="3dp"
android:layout_marginEnd="3dp">
<TextView <TextView
android:id="@+id/tvStatCommands" android:id="@+id/tvProjectId"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="0"
android:textColor="@color/accent_green"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="已执行"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:layout_marginTop="4dp" />
</LinearLayout>
<!-- 消息数 -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:text="项目 —"
android:orientation="vertical" android:textSize="14sp"
android:background="@drawable/card_bg" android:textColor="@color/text_primary" />
android:padding="16dp"
android:gravity="center"
android:layout_marginStart="6dp">
<TextView
android:id="@+id/tvStatMessages"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textColor="@color/accent_purple"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="消息"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:layout_marginTop="4dp" />
</LinearLayout>
</LinearLayout> </LinearLayout>
<!-- 设备信息卡片 --> <!-- Device Info Card -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:background="@drawable/card_bg" android:background="@drawable/bg_glass_card"
android:padding="20dp" android:padding="18dp"
android:layout_marginBottom="12dp"> android:layout_marginBottom="12dp">
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="设备信息" android:text="设备信息"
android:textColor="@color/text_primary" android:textSize="13sp"
android:textSize="15sp" android:textColor="@color/text_secondary"
android:textStyle="bold" android:textStyle="bold"
android:letterSpacing="0.04"
android:layout_marginBottom="12dp" /> android:layout_marginBottom="12dp" />
<LinearLayout <TextView
android:id="@+id/tvHardwareInfo"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:text="加载中..."
android:layout_marginBottom="8dp"> android:textSize="14sp"
android:textColor="@color/text_primary"
<TextView android:lineSpacingExtra="5dp" />
android:layout_width="80dp"
android:layout_height="wrap_content"
android:text="型号"
android:textColor="@color/text_secondary"
android:textSize="13sp" />
<TextView
android:id="@+id/tvDeviceModel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="-"
android:textColor="@color/text_primary"
android:textSize="13sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<TextView
android:layout_width="80dp"
android:layout_height="wrap_content"
android:text="Android"
android:textColor="@color/text_secondary"
android:textSize="13sp" />
<TextView
android:id="@+id/tvAndroidVersion"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="-"
android:textColor="@color/text_primary"
android:textSize="13sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<TextView
android:layout_width="80dp"
android:layout_height="wrap_content"
android:text="设备ID"
android:textColor="@color/text_secondary"
android:textSize="13sp" />
<TextView
android:id="@+id/tvDeviceId"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="-"
android:textColor="@color/text_primary"
android:textSize="13sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="80dp"
android:layout_height="wrap_content"
android:text="Root"
android:textColor="@color/text_secondary"
android:textSize="13sp" />
<TextView
android:id="@+id/tvRootStatus"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="检测中..."
android:textColor="@color/accent_green"
android:textSize="13sp" />
</LinearLayout>
</LinearLayout> </LinearLayout>
<!-- 服务信息卡片 --> <!-- Capabilities Grid -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="可用功能"
android:textSize="13sp"
android:textColor="@color/text_secondary"
android:textStyle="bold"
android:letterSpacing="0.04"
android:layout_marginBottom="10dp"
android:layout_marginTop="4dp" />
<GridLayout
android:id="@+id/gridCapabilities"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:columnCount="2"
android:layout_marginBottom="16dp"
android:useDefaultMargins="true">
<TextView
android:id="@+id/tvCapRoot"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_columnWeight="1"
android:text="Root"
android:textSize="13sp"
android:textColor="@color/text_primary"
android:background="@drawable/bg_capability_off"
android:padding="12dp"
android:gravity="center"
android:drawablePadding="4dp" />
<TextView
android:id="@+id/tvCapHook"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_columnWeight="1"
android:text="Hook 引擎"
android:textSize="13sp"
android:textColor="@color/text_primary"
android:background="@drawable/bg_capability_off"
android:padding="12dp"
android:gravity="center" />
<TextView
android:id="@+id/tvCapA11y"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_columnWeight="1"
android:text="无障碍"
android:textSize="13sp"
android:textColor="@color/text_primary"
android:background="@drawable/bg_capability_off"
android:padding="12dp"
android:gravity="center" />
<TextView
android:id="@+id/tvCapAntiBan"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_columnWeight="1"
android:text="防封引擎"
android:textSize="13sp"
android:textColor="@color/text_primary"
android:background="@drawable/bg_capability_off"
android:padding="12dp"
android:gravity="center" />
<TextView
android:id="@+id/tvCapWechat"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_columnWeight="1"
android:text="微信控制"
android:textSize="13sp"
android:textColor="@color/text_primary"
android:background="@drawable/bg_capability_off"
android:padding="12dp"
android:gravity="center" />
<TextView
android:id="@+id/tvCapAI"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_columnWeight="1"
android:text="AI Brain"
android:textSize="13sp"
android:textColor="@color/text_primary"
android:background="@drawable/bg_capability_off"
android:padding="12dp"
android:gravity="center" />
</GridLayout>
<!-- Quick Actions -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="horizontal">
android:background="@drawable/card_bg"
android:padding="20dp"
android:layout_marginBottom="12dp">
<TextView <Button
android:layout_width="wrap_content" android:id="@+id/btnReconnect"
android:layout_height="wrap_content" android:layout_width="0dp"
android:text="服务信息" android:layout_height="48dp"
android:textColor="@color/text_primary" android:layout_weight="1"
android:text="重新连接"
android:textSize="15sp" android:textSize="15sp"
android:textStyle="bold" android:textColor="#FFFFFF"
android:layout_marginBottom="12dp" /> android:background="@drawable/btn_ios_primary"
android:layout_marginEnd="6dp"
android:textAllCaps="false" />
<LinearLayout <Button
android:layout_width="match_parent" android:id="@+id/btnRestartService"
android:layout_height="wrap_content" android:layout_width="0dp"
android:orientation="horizontal" android:layout_height="48dp"
android:layout_marginBottom="8dp"> android:layout_weight="1"
android:text="重启服务"
<TextView android:textSize="15sp"
android:layout_width="80dp" android:textColor="@color/brand_primary"
android:layout_height="wrap_content" android:background="@drawable/btn_ios_secondary"
android:text="服务器" android:layout_marginStart="6dp"
android:textColor="@color/text_secondary" android:textAllCaps="false" />
android:textSize="13sp" />
<TextView
android:id="@+id/tvServerUrl"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="未配置"
android:textColor="@color/text_primary"
android:textSize="13sp"
android:ellipsize="middle"
android:singleLine="true" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<TextView
android:layout_width="80dp"
android:layout_height="wrap_content"
android:text="项目"
android:textColor="@color/text_secondary"
android:textSize="13sp" />
<TextView
android:id="@+id/tvProjectId"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="未配置"
android:textColor="@color/text_primary"
android:textSize="13sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="80dp"
android:layout_height="wrap_content"
android:text="Hook"
android:textColor="@color/text_secondary"
android:textSize="13sp" />
<TextView
android:id="@+id/tvHookStatus"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="未启用"
android:textColor="@color/text_secondary"
android:textSize="13sp" />
</LinearLayout>
</LinearLayout> </LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="16dp" />
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>

View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/bg_primary"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="16dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="运行日志"
android:textSize="22sp"
android:textStyle="bold"
android:textColor="@color/text_primary" />
<Button
android:id="@+id/btnClearLog"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:text="清空"
android:textSize="13sp"
android:textColor="@color/accent_red"
android:background="@drawable/btn_outline"
android:paddingHorizontal="16dp" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/divider" />
<ScrollView
android:id="@+id/logScrollView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:fillViewport="true">
<TextView
android:id="@+id/tvLogContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp"
android:text="等待日志..."
android:textSize="12sp"
android:fontFamily="monospace"
android:textColor="@color/text_primary"
android:lineSpacingExtra="2dp"
android:gravity="top" />
</ScrollView>
</LinearLayout>

View File

@@ -1,327 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:scrollbars="none"
android:background="@color/bg_primary">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- 标题 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="设置"
android:textColor="@color/text_primary"
android:textSize="22sp"
android:textStyle="bold"
android:layout_marginBottom="20dp" />
<!-- 连接配置 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/card_bg"
android:padding="20dp"
android:layout_marginBottom="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="连接配置"
android:textColor="@color/text_primary"
android:textSize="15sp"
android:textStyle="bold"
android:layout_marginBottom="16dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="服务器地址"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:layout_marginBottom="6dp" />
<EditText
android:id="@+id/etServerUrl"
android:layout_width="match_parent"
android:layout_height="44dp"
android:background="@drawable/input_bg"
android:hint="ws://192.168.1.100:8899/ws/device"
android:textColorHint="@color/text_tertiary"
android:textColor="@color/text_primary"
android:textSize="14sp"
android:paddingHorizontal="14dp"
android:inputType="textUri"
android:singleLine="true"
android:layout_marginBottom="14dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="项目ID"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:layout_marginBottom="6dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="14dp">
<EditText
android:id="@+id/etProjectId"
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_weight="1"
android:background="@drawable/input_bg"
android:hint="输入项目ID"
android:textColorHint="@color/text_tertiary"
android:textColor="@color/text_primary"
android:textSize="14sp"
android:paddingHorizontal="14dp"
android:inputType="text"
android:singleLine="true" />
<ImageButton
android:id="@+id/btnScanQr"
android:layout_width="44dp"
android:layout_height="44dp"
android:layout_marginStart="8dp"
android:background="@drawable/btn_action"
android:src="@android:drawable/ic_menu_camera"
android:scaleType="center"
android:tint="@color/text_primary"
android:contentDescription="扫码" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="设备ID"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:layout_marginBottom="6dp" />
<EditText
android:id="@+id/etDeviceId"
android:layout_width="match_parent"
android:layout_height="44dp"
android:background="@drawable/input_bg"
android:hint="自动检测"
android:textColorHint="@color/text_tertiary"
android:textColor="@color/text_primary"
android:textSize="14sp"
android:paddingHorizontal="14dp"
android:inputType="text"
android:singleLine="true"
android:layout_marginBottom="20dp" />
<!-- 连接/断开按钮 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/btnDisconnect"
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_weight="1"
android:background="@drawable/btn_action"
android:gravity="center"
android:text="断开连接"
android:textColor="@color/text_secondary"
android:textSize="14sp"
android:layout_marginEnd="6dp" />
<TextView
android:id="@+id/btnConnect"
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_weight="1"
android:background="@drawable/btn_primary"
android:gravity="center"
android:text="连接服务器"
android:textColor="@android:color/white"
android:textSize="14sp"
android:layout_marginStart="6dp" />
</LinearLayout>
</LinearLayout>
<!-- 高级设置 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/card_bg"
android:padding="20dp"
android:layout_marginBottom="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="高级设置"
android:textColor="@color/text_primary"
android:textSize="15sp"
android:textStyle="bold"
android:layout_marginBottom="16dp" />
<!-- 自动连接 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="16dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="自动连接"
android:textColor="@color/text_primary"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="启动时自动连接服务器"
android:textColor="@color/text_secondary"
android:textSize="12sp" />
</LinearLayout>
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switchAutoConnect"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<!-- 无障碍服务 -->
<LinearLayout
android:id="@+id/btnAccessibility"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="16dp"
android:background="?android:attr/selectableItemBackground"
android:padding="2dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="无障碍服务"
android:textColor="@color/text_primary"
android:textSize="14sp" />
<TextView
android:id="@+id/tvAccessibilityStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="未开启"
android:textColor="@color/text_secondary"
android:textSize="12sp" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="前往设置 "
android:textColor="@color/accent_blue"
android:textSize="13sp" />
</LinearLayout>
<!-- Hook模块 -->
<LinearLayout
android:id="@+id/btnHookModules"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:background="?android:attr/selectableItemBackground"
android:padding="2dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hook 模块管理"
android:textColor="@color/text_primary"
android:textSize="14sp" />
<TextView
android:id="@+id/tvHookCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0 个模块"
android:textColor="@color/text_secondary"
android:textSize="12sp" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="管理 "
android:textColor="@color/accent_blue"
android:textSize="13sp" />
</LinearLayout>
</LinearLayout>
<!-- 关于 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/card_bg"
android:padding="20dp"
android:layout_marginBottom="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="关于"
android:textColor="@color/text_primary"
android:textSize="15sp"
android:textStyle="bold"
android:layout_marginBottom="12dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="工作 v2.0.0\n机擎SDK v3.0\n\n基于 Frida + uiautomator2 的智能手机控制系统\n支持微信/抖音/小红书等多平台自动化"
android:textColor="@color/text_secondary"
android:textSize="13sp"
android:lineSpacingExtra="3dp" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="32dp" />
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,229 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/bg_primary"
android:clipToPadding="false"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="20dp"
android:paddingTop="16dp"
android:paddingBottom="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="设置"
android:textSize="28sp"
android:textStyle="bold"
android:textColor="@color/text_primary"
android:letterSpacing="0.02"
android:layout_marginBottom="20dp" />
<!-- Server Info -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/bg_glass_card"
android:padding="18dp"
android:layout_marginBottom="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="绑定信息"
android:textSize="13sp"
android:textColor="@color/text_secondary"
android:textStyle="bold"
android:letterSpacing="0.04"
android:layout_marginBottom="12dp" />
<TextView
android:id="@+id/tvServerUrl"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="服务器 未配置"
android:textSize="14sp"
android:textColor="@color/text_primary"
android:layout_marginBottom="6dp" />
<TextView
android:id="@+id/tvProjectId"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="项目 —"
android:textSize="14sp"
android:textColor="@color/text_primary"
android:layout_marginBottom="6dp" />
<TextView
android:id="@+id/tvDeviceId"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="设备ID —"
android:textSize="14sp"
android:textColor="@color/text_primary" />
</LinearLayout>
<!-- Service Control -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/bg_glass_card"
android:padding="18dp"
android:layout_marginBottom="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="服务控制"
android:textSize="13sp"
android:textColor="@color/text_secondary"
android:textStyle="bold"
android:letterSpacing="0.04"
android:layout_marginBottom="14dp" />
<Button
android:id="@+id/btnRebind"
android:layout_width="match_parent"
android:layout_height="48dp"
android:text="重新绑定服务器"
android:textSize="15sp"
android:textColor="#FFFFFF"
android:background="@drawable/btn_ios_primary"
android:layout_marginBottom="8dp"
android:textAllCaps="false" />
<Button
android:id="@+id/btnOpenA11y"
android:layout_width="match_parent"
android:layout_height="48dp"
android:text="开启无障碍服务"
android:textSize="15sp"
android:textColor="@color/brand_primary"
android:background="@drawable/btn_ios_secondary"
android:layout_marginBottom="8dp"
android:textAllCaps="false" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btnRestartService"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:text="重启服务"
android:textSize="15sp"
android:textColor="@color/accent_green"
android:background="@drawable/btn_ios_secondary"
android:layout_marginEnd="4dp"
android:textAllCaps="false" />
<Button
android:id="@+id/btnStopService"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:text="停止服务"
android:textSize="15sp"
android:textColor="@color/accent_red"
android:background="@drawable/btn_ios_secondary"
android:layout_marginStart="4dp"
android:textAllCaps="false" />
</LinearLayout>
</LinearLayout>
<!-- Log Viewer (collapsed by default) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/bg_glass_card"
android:padding="18dp"
android:layout_marginBottom="12dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:text="运行日志"
android:textSize="13sp"
android:textColor="@color/text_secondary"
android:textStyle="bold"
android:letterSpacing="0.04" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:orientation="horizontal">
<Button
android:id="@+id/btnToggleLog"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:text="展开"
android:textSize="12sp"
android:textColor="@color/brand_primary"
android:background="@drawable/btn_ios_secondary"
android:paddingHorizontal="14dp"
android:textAllCaps="false"
android:layout_marginEnd="6dp" />
<Button
android:id="@+id/btnClearLog"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:text="清空"
android:textSize="12sp"
android:textColor="@color/accent_red"
android:background="@drawable/btn_ios_secondary"
android:paddingHorizontal="14dp"
android:textAllCaps="false" />
</LinearLayout>
</RelativeLayout>
<ScrollView
android:id="@+id/logScrollView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:visibility="gone">
<TextView
android:id="@+id/tvLogContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="点击「展开」查看日志"
android:textSize="11sp"
android:textColor="@color/text_secondary"
android:fontFamily="monospace"
android:lineSpacingExtra="2dp" />
</ScrollView>
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="工作 v5.0.0"
android:textSize="12sp"
android:textColor="@color/text_tertiary"
android:gravity="center"
android:layout_marginTop="16dp" />
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,164 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/bg_primary"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="运行状态"
android:textSize="22sp"
android:textStyle="bold"
android:textColor="@color/text_primary"
android:layout_marginBottom="16dp" />
<!-- 连接状态卡片 -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardCornerRadius="12dp"
app:cardElevation="2dp"
app:cardBackgroundColor="@color/bg_card">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="连接通道"
android:textSize="14sp"
android:textColor="@color/text_secondary"
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/tvConnection"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="检测中..."
android:textSize="15sp"
android:textColor="@color/text_primary" />
<TextView
android:id="@+id/tvRoot"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Root: 检测中..."
android:textSize="15sp"
android:textColor="@color/text_primary"
android:layout_marginTop="4dp" />
<TextView
android:id="@+id/tvFrida"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Frida: 检测中..."
android:textSize="15sp"
android:textColor="@color/text_primary"
android:layout_marginTop="4dp" />
<TextView
android:id="@+id/tvA11y"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="无障碍: 检测中..."
android:textSize="15sp"
android:textColor="@color/text_primary"
android:layout_marginTop="4dp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- AI引擎卡片 -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardCornerRadius="12dp"
app:cardElevation="2dp"
app:cardBackgroundColor="@color/bg_card">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="智能引擎"
android:textSize="14sp"
android:textColor="@color/text_secondary"
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/tvAiBrain"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="AI Brain: 检测中..."
android:textSize="15sp"
android:textColor="@color/text_primary" />
<TextView
android:id="@+id/tvAntiBan"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="防封模块: 检测中..."
android:textSize="15sp"
android:textColor="@color/text_primary"
android:layout_marginTop="4dp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- 设备信息卡片 -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardCornerRadius="12dp"
app:cardElevation="2dp"
app:cardBackgroundColor="@color/bg_card">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="设备信息"
android:textSize="14sp"
android:textColor="@color/text_secondary"
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/tvDevice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="加载中..."
android:textSize="15sp"
android:textColor="@color/text_primary"
android:lineSpacingExtra="4dp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</ScrollView>

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<WebView
android:id="@+id/webView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ProgressBar
android:id="@+id/webProgress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="3dp"
android:layout_gravity="top"
android:visibility="gone"
android:max="100" />
</FrameLayout>

View File

@@ -0,0 +1,211 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/bg_primary"
android:clipToPadding="false"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="20dp"
android:paddingTop="16dp"
android:paddingBottom="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="微信控制"
android:textSize="28sp"
android:textStyle="bold"
android:textColor="@color/text_primary"
android:letterSpacing="0.02"
android:layout_marginBottom="20dp" />
<!-- Status Card -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/bg_glass_card"
android:padding="18dp"
android:layout_marginBottom="12dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:text="微信状态"
android:textSize="13sp"
android:textColor="@color/text_secondary"
android:textStyle="bold"
android:letterSpacing="0.04" />
<TextView
android:id="@+id/tvWxRunning"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:text="未运行"
android:textSize="12sp"
android:textColor="@color/accent_red"
android:background="@drawable/bg_pill_offline"
android:paddingHorizontal="12dp"
android:paddingVertical="4dp" />
</RelativeLayout>
<TextView
android:id="@+id/tvWxVersion"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="版本 —"
android:textSize="14sp"
android:textColor="@color/text_primary"
android:layout_marginBottom="6dp" />
<TextView
android:id="@+id/tvHookStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Hook 未注入"
android:textSize="14sp"
android:textColor="@color/text_primary" />
</LinearLayout>
<!-- Capabilities -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/bg_glass_card"
android:padding="18dp"
android:layout_marginBottom="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="功能状态"
android:textSize="13sp"
android:textColor="@color/text_secondary"
android:textStyle="bold"
android:letterSpacing="0.04"
android:layout_marginBottom="12dp" />
<TextView
android:id="@+id/tvMsgCapability"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="消息收发 —"
android:textSize="14sp"
android:textColor="@color/text_primary"
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/tvContactCapability"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="通讯录 —"
android:textSize="14sp"
android:textColor="@color/text_primary"
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/tvMomentCapability"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="朋友圈 —"
android:textSize="14sp"
android:textColor="@color/text_primary" />
</LinearLayout>
<!-- Quick Actions -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="快捷操作"
android:textSize="13sp"
android:textColor="@color/text_secondary"
android:textStyle="bold"
android:letterSpacing="0.04"
android:layout_marginBottom="10dp"
android:layout_marginTop="4dp" />
<Button
android:id="@+id/btnOpenWechat"
android:layout_width="match_parent"
android:layout_height="48dp"
android:text="打开微信"
android:textSize="15sp"
android:textColor="#FFFFFF"
android:background="@drawable/btn_ios_primary"
android:textAllCaps="false"
android:layout_marginBottom="8dp" />
<Button
android:id="@+id/btnGetContacts"
android:layout_width="match_parent"
android:layout_height="48dp"
android:text="获取通讯录"
android:textSize="15sp"
android:textColor="@color/brand_primary"
android:background="@drawable/btn_ios_secondary"
android:textAllCaps="false"
android:layout_marginBottom="8dp" />
<Button
android:id="@+id/btnGetMessages"
android:layout_width="match_parent"
android:layout_height="48dp"
android:text="获取消息列表"
android:textSize="15sp"
android:textColor="@color/brand_primary"
android:background="@drawable/btn_ios_secondary"
android:textAllCaps="false"
android:layout_marginBottom="8dp" />
<!-- Anti-ban Info -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/bg_glass_card"
android:padding="18dp"
android:layout_marginTop="4dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="安全防护"
android:textSize="13sp"
android:textColor="@color/text_secondary"
android:textStyle="bold"
android:letterSpacing="0.04"
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/tvAntiBanStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="防封引擎 已启用"
android:textSize="14sp"
android:textColor="@color/text_primary"
android:layout_marginBottom="4dp" />
<TextView
android:id="@+id/tvRiskLevel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="风险等级 低"
android:textSize="14sp"
android:textColor="@color/accent_green" />
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@@ -2,16 +2,12 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <menu xmlns:android="http://schemas.android.com/apk/res/android">
<item <item
android:id="@+id/nav_dashboard" android:id="@+id/nav_dashboard"
android:icon="@drawable/ic_nav_dashboard" android:icon="@drawable/ic_nav_home"
android:title="状态" /> android:title="首页" />
<item <item
android:id="@+id/nav_control" android:id="@+id/nav_wechat"
android:icon="@drawable/ic_nav_control" android:icon="@drawable/ic_nav_wechat"
android:title="控制" /> android:title="微信" />
<item
android:id="@+id/nav_ai"
android:icon="@drawable/ic_nav_ai"
android:title="AI" />
<item <item
android:id="@+id/nav_settings" android:id="@+id/nav_settings"
android:icon="@drawable/ic_nav_settings" android:icon="@drawable/ic_nav_settings"

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/> <background android:drawable="@color/ic_launcher_bg"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/> <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon> </adaptive-icon>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/> <background android:drawable="@color/ic_launcher_bg"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/> <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon> </adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 B

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 B

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 B

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 B

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 B

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 B

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 416 B

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 416 B

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 545 B

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 545 B

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -1,29 +1,39 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="bg_primary">#0D1117</color> <color name="brand_primary">#007AFF</color>
<color name="bg_surface">#161B22</color> <color name="brand_primary_light">#5AC8FA</color>
<color name="bg_card">#21262D</color> <color name="brand_primary_dark">#0055D4</color>
<color name="bg_card_elevated">#30363D</color>
<color name="accent_blue">#58A6FF</color> <color name="bg_primary">#F2F2F7</color>
<color name="accent_blue_dark">#1F6FEB</color> <color name="bg_surface">#FFFFFF</color>
<color name="accent_green">#3FB950</color> <color name="bg_card">#F9F9FB</color>
<color name="accent_red">#F85149</color> <color name="bg_card_elevated">#FFFFFF</color>
<color name="accent_orange">#D29922</color> <color name="bg_glass">#C8FFFFFF</color>
<color name="accent_purple">#BC8CFF</color> <color name="bg_glass_dark">#B0F2F2F7</color>
<color name="text_primary">#E6EDF3</color> <color name="accent_blue">#007AFF</color>
<color name="text_secondary">#8B949E</color> <color name="accent_blue_dark">#0055D4</color>
<color name="text_tertiary">#484F58</color> <color name="accent_green">#34C759</color>
<color name="accent_red">#FF3B30</color>
<color name="accent_orange">#FF9500</color>
<color name="accent_purple">#AF52DE</color>
<color name="accent_teal">#5AC8FA</color>
<color name="accent_yellow">#FFCC00</color>
<color name="divider">#21262D</color> <color name="text_primary">#000000</color>
<color name="ripple">#1A58A6FF</color> <color name="text_secondary">#8E8E93</color>
<color name="text_tertiary">#AEAEB2</color>
<color name="nav_bg">#0D1117</color> <color name="divider">#E5E5EA</color>
<color name="nav_selected">#58A6FF</color> <color name="ripple">#14007AFF</color>
<color name="nav_unselected">#484F58</color>
<color name="status_online">#3FB950</color> <color name="nav_bg">#F9F9F9</color>
<color name="status_offline">#F85149</color> <color name="nav_selected">#007AFF</color>
<color name="status_connecting">#D29922</color> <color name="nav_unselected">#8E8E93</color>
<color name="status_online">#34C759</color>
<color name="status_offline">#FF3B30</color>
<color name="status_connecting">#FF9500</color>
<color name="ic_launcher_bg">#B8D4C8</color>
</resources> </resources>

View File

@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">AI数智</string> <string name="app_name">工作</string>
<string name="accessibility_service_description">云同步服务辅助功能</string> <string name="accessibility_service_description">云同步服务辅助功能</string>
<string name="nav_dashboard">状态</string> <string name="nav_hub">工作台</string>
<string name="nav_control">控制</string> <string name="nav_pwa">业务</string>
<string name="nav_ai">AI</string> <string name="nav_status">状态</string>
<string name="nav_settings">设置</string> <string name="nav_settings">设置</string>
<string name="setup_title">初始配置</string> <string name="setup_title">初始配置</string>
<string name="setup_scan_hint">请扫描服务端提供的二维码完成绑定</string> <string name="setup_scan_hint">请扫描服务端提供的二维码完成绑定</string>

Some files were not shown because too many files have changed in this diff Show More