feat: Android 重构 service/engine/ui/frida/antiban、AgentForegroundService、APK产品说明、架构图
Made-with: Cursor
@@ -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'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
1612
sdk/android-app/app/src/main/assets/hub.html
Normal file
2071
sdk/android-app/app/src/main/assets/wechat_hook_v2.js
Normal 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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")
|
|
||||||
}
|
|
||||||
|
|
||||||
// === 安装APK(Root静默安装) ===
|
|
||||||
"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 -c,5秒超时(Magisk可能弹窗阻塞)
|
|
||||||
val suResult = runWithTimeout(arrayOf("su", "-c", command), 5)
|
|
||||||
if (suResult != null && suResult.first == 0) {
|
|
||||||
return Pair(true, suResult.second.ifEmpty { "执行成功(root)" })
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) 回退到 sh -c,15秒超时
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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'")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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) 默认使用奥创兼容ID(md5(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) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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授权"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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清理内存")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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?) {
|
|
||||||
// 其他事件
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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?>
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 -> "同步服务运行中"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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", "") ?: ""),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
"{}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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() }
|
||||||
|
}
|
||||||
@@ -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 "未运行"}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
10
sdk/android-app/app/src/main/res/drawable/btn_brand.xml
Normal 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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
11
sdk/android-app/app/src/main/res/drawable/btn_outline.xml
Normal 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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
10
sdk/android-app/app/src/main/res/drawable/ic_nav_log.xml
Normal 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>
|
||||||
@@ -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>
|
||||||
10
sdk/android-app/app/src/main/res/drawable/ic_nav_wechat.xml
Normal 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>
|
||||||
10
sdk/android-app/app/src/main/res/drawable/ic_scan_qr.xml
Normal 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>
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
||||||
|
|||||||
62
sdk/android-app/app/src/main/res/layout/fragment_log.xml
Normal 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>
|
||||||
@@ -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>
|
|
||||||
229
sdk/android-app/app/src/main/res/layout/fragment_settings_v2.xml
Normal 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>
|
||||||
164
sdk/android-app/app/src/main/res/layout/fragment_status.xml
Normal 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>
|
||||||
@@ -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>
|
|
||||||
211
sdk/android-app/app/src/main/res/layout/fragment_wechat.xml
Normal 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>
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 197 B After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 197 B After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 160 B After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 160 B After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 271 B After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 271 B After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 416 B After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 416 B After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 545 B After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 545 B After Width: | Height: | Size: 7.9 KiB |
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||