Add AutoRobot directory with Windows line endings

This commit is contained in:
2025-10-20 09:04:09 +08:00
parent a7ade87dde
commit d663118a73
124 changed files with 22719 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
package com.joyd.autobot
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.joyd.autobot", appContext.packageName)
}
}

View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- 添加必要的权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<!-- 用于支持截屏功能 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" android:minSdkVersion="33" />
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" android:minSdkVersion="34" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AutoBot"
tools:replace="android:label">
<!-- 添加系统属性访问兼容性配置 -->
<meta-data
android:name="android.content.APP_RESOURCE_ACCESS_FIX"
android:value="true" />
<!-- 针对SELinux策略的兼容性配置 -->
<meta-data
android:name="android.content.SELINUX_COMPAT_MODE"
android:value="relaxed" />
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- 注册设置Activity -->
<activity
android:name=".SettingsActivity"
android:exported="false" />
<!-- 注册前台服务 -->
<service
android:name="com.joyd.autobot.WebSocketForegroundService"
android:exported="false"
android:foregroundServiceType="mediaProjection" />
</application>
</manifest>

View File

@@ -0,0 +1,676 @@
package com.joyd.autobot
import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.BroadcastReceiver
import android.content.pm.PackageManager
import android.content.SharedPreferences
import android.media.projection.MediaProjectionManager
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.activity.result.contract.ActivityResultContracts
import com.joyd.joydlib.io.WebSocketClient
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import okio.ByteString
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
class MainActivity : AppCompatActivity(), WebSocketForegroundService.Companion.ServiceReadyCallback {
private val TAG = "MainActivity"
private val SCREENSHOT_COMMAND = "screenshot"
private val REQUEST_NOTIFICATION_PERMISSION = 1002
// 定义截图数据广播的action
private val SCREENSHOT_DATA_ACTION = "com.joyd.autobot.SCREENSHOT_DATA"
private val SCREENSHOT_DATA_EXTRA = "screenshot_data"
// 定义MediaProjection权限请求广播的action
private val MEDIA_PROJECTION_PERMISSION_REQUIRED = "com.joyd.autobot.MEDIA_PROJECTION_PERMISSION_REQUIRED"
private lateinit var connectButton: Button
private lateinit var connectionStatusIndicator: View
private lateinit var connectionStatusText: TextView
private lateinit var settingsButton: Button
private lateinit var webSocketUrl: String
private var webSocketClient: WebSocketClient? = null
private var isConnected = false
// 重连延迟常量 - 保留此常量用于连接断开后的重连逻辑
private val RECONNECT_DELAY_MS = 5000L // 重连延迟5秒
// 重连状态标志
private val isReconnecting = AtomicBoolean(false)
// 处理异步事件的Handler
private lateinit var handler: Handler
// UI组件引用
private var screenshotButton: Button? = null
// 创建截图数据广播接收器
private val screenshotBroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == SCREENSHOT_DATA_ACTION) {
val screenshotData = intent.getByteArrayExtra(SCREENSHOT_DATA_EXTRA)
if (screenshotData != null) {
Log.d(TAG, "接收到截图数据,大小: ${screenshotData.size} bytes")
// 发送截图数据到服务端
sendScreenshotData(screenshotData)
} else {
Log.e(TAG, "接收到的截图数据为null")
}
} else if (intent?.action == MEDIA_PROJECTION_PERMISSION_REQUIRED) {
// 接收到需要重新请求MediaProjection权限的广播
Log.d(TAG, "接收到需要重新请求MediaProjection权限的广播")
runOnUiThread {
Toast.makeText(this@MainActivity, "需要重新请求屏幕捕获权限以继续截图功能", Toast.LENGTH_SHORT).show()
requestScreenshotPermission()
}
}
}
}
// Activity Result API launcher for media projection
private val mediaProjectionLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK && it.data != null) {
Log.d(TAG, "用户授予了屏幕捕获权限")
WebSocketForegroundService.setMediaProjection(this, it.resultCode, it.data!!)
} else {
// 用户拒绝了屏幕捕获权限
Log.d(TAG, "用户拒绝了屏幕捕获权限resultCode: ${it.resultCode}")
Toast.makeText(this, "需要屏幕捕获权限才能执行截图操作", Toast.LENGTH_SHORT).show()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.d(TAG, "onCreate()被调用")
// 初始化Handler
handler = Handler(Looper.getMainLooper())
// 初始化UI组件
initUI()
// 从SharedPreferences获取配置的WebSocket URL
val sharedPreferences = androidx.preference.PreferenceManager.getDefaultSharedPreferences(this)
val defaultUrl = getString(R.string.web_socket_url)
webSocketUrl = sharedPreferences.getString(getString(R.string.pref_key_web_socket_url), defaultUrl) ?: defaultUrl
Log.d(TAG, "从配置获取WebSocket URL: $webSocketUrl")
// 初始化WebSocket客户端
initWebSocketClient()
// 检查并请求通知权限
checkNotificationPermission()
// 注册截图数据广播接收器
registerScreenshotBroadcastReceiver()
}
// 注册截图数据广播接收器
private fun registerScreenshotBroadcastReceiver() {
val filter = IntentFilter()
filter.addAction(SCREENSHOT_DATA_ACTION)
filter.addAction(MEDIA_PROJECTION_PERMISSION_REQUIRED)
ContextCompat.registerReceiver(
this,
screenshotBroadcastReceiver,
filter,
ContextCompat.RECEIVER_NOT_EXPORTED
)
Log.d(TAG, "截图数据广播接收器和MediaProjection权限请求广播接收器已注册")
}
// 注销截图数据广播接收器
private fun unregisterScreenshotBroadcastReceiver() {
try {
unregisterReceiver(screenshotBroadcastReceiver)
Log.d(TAG, "截图数据广播接收器已注销")
} catch (e: Exception) {
Log.e(TAG, "注销广播接收器异常: ${e.message}", e)
}
}
private fun initUI() {
connectButton = findViewById<Button>(R.id.connect_button)
connectionStatusIndicator = findViewById<View>(R.id.connection_status_indicator)
connectionStatusText = findViewById<TextView>(R.id.connection_status_text)
settingsButton = findViewById<Button>(R.id.settings_btn)
// 初始状态设置为未连接
updateConnectionStatus(false)
// 设置连接按钮点击事件
setupConnectButton()
// 设置设置按钮点击事件
setupSettingsButton()
}
private fun checkNotificationPermission() {
// Android 13及以上需要通知权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "通知权限未授予,请求权限")
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), REQUEST_NOTIFICATION_PERMISSION)
} else {
Log.d(TAG, "通知权限已授予")
}
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
REQUEST_NOTIFICATION_PERMISSION -> {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "用户授予了通知权限")
} else {
Log.w(TAG, "用户拒绝了通知权限")
Toast.makeText(this, "需要通知权限以在后台保持WebSocket连接", Toast.LENGTH_SHORT).show()
}
}
}
}
private fun initWebSocketClient() {
try {
Log.d(TAG, "开始初始化WebSocket客户端")
// 创建WebSocket客户端实例
webSocketClient = WebSocketClient(webSocketUrl)
if (webSocketClient == null) {
Log.e(TAG, "WebSocketClient创建失败对象为null")
return
}
Log.d(TAG, "WebSocket客户端创建成功: $webSocketClient")
// 配置重连参数
val reconnectConfigResult = webSocketClient?.setReconnectConfig(
maxAttempts = 10, // 最大重连次数
baseDelayMs = 1000L, // 基础重连延迟(毫秒)
maxDelayMs = 30000L // 最大重连延迟(毫秒)
)
Log.d(TAG, "WebSocket重连参数配置完成结果: $reconnectConfigResult")
// 设置消息接收回调
val messageCallbackResult = webSocketClient?.setOnMessageCallback {
// 处理接收到的文本消息
Log.d(TAG, "收到WebSocket消息: $it")
// 检查是否是截屏命令
if (it == SCREENSHOT_COMMAND) {
Log.d(TAG, "收到截屏命令,开始执行截屏操作")
// 在后台线程执行截屏操作
Thread {
try {
takeScreenshot()
} catch (e: Exception) {
Log.e(TAG, "执行截屏操作异常: ${e.message}", e)
}
}.start()
} else {
runOnUiThread {
Toast.makeText(this@MainActivity, "收到消息: $it", Toast.LENGTH_SHORT).show()
// 收到消息意味着连接成功如果UI还显示未连接强制更新UI
if (!isConnected) {
Log.d(TAG, "收到消息但isConnected为false强制更新UI为连接状态")
isConnected = true
updateConnectionStatus(true)
}
}
}
}
Log.d(TAG, "WebSocket消息接收回调设置完成结果: $messageCallbackResult")
// 设置连接状态变化回调 - 完善实现更新连接状态和UI
val stateCallbackResult = webSocketClient?.setOnStateChangeCallback { state ->
// 处理连接状态变化
Log.d(TAG, "WebSocket连接状态变化: $state")
// 状态变化日志
val statusMessage = when(state) {
WebSocketClient.ConnectionState.Disconnected -> "连接已断开"
WebSocketClient.ConnectionState.Connecting -> "正在连接"
WebSocketClient.ConnectionState.Connected -> "WebSocket连接成功"
WebSocketClient.ConnectionState.Paused -> "连接已暂停"
else -> "未知状态"
}
Log.d(TAG, "显示Toast消息: $statusMessage")
// 根据实际连接状态更新isConnected变量和UI
runOnUiThread {
Toast.makeText(this@MainActivity, statusMessage, Toast.LENGTH_SHORT).show()
// 只有当状态为Connected时才更新UI为已连接
val newConnectionStatus = (state == WebSocketClient.ConnectionState.Connected)
if (isConnected != newConnectionStatus) {
isConnected = newConnectionStatus
updateConnectionStatus(isConnected)
}
}
}
Log.d(TAG, "WebSocket连接状态回调设置完成结果: $stateCallbackResult")
// 设置错误回调
val errorCallbackResult = webSocketClient?.setOnErrorCallback {
Log.e(TAG, "WebSocket连接错误: ${it?.message}")
runOnUiThread {
val errorMsg = it?.message ?: "未知错误"
Toast.makeText(this@MainActivity, "WebSocket连接错误: $errorMsg", Toast.LENGTH_SHORT).show()
}
}
Log.d(TAG, "WebSocket错误回调设置完成")
Log.d(TAG, "WebSocket客户端初始化完成")
} catch (e: Exception) {
Log.e(TAG, "初始化WebSocket失败: ${e.message}", e)
runOnUiThread {
Toast.makeText(this, "初始化WebSocket失败: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
}
/**
* 执行截屏操作
* 现在通过前台服务请求截图
*/
private fun takeScreenshot() {
Log.d(TAG, "takeScreenshot方法被调用")
// 检查WebSocket连接状态
if (!isConnected) {
Log.w(TAG, "WebSocket未连接无法发送截图")
runOnUiThread {
Toast.makeText(this, "WebSocket未连接请先连接", Toast.LENGTH_SHORT).show()
}
return
}
// 获取当前服务实例状态
val service = WebSocketForegroundService.getInstance()
Log.d(TAG, "当前服务实例状态: $service")
// 确保前台服务已启动
if (service == null) {
Log.d(TAG, "前台服务未启动,先启动服务")
WebSocketForegroundService.startService(this)
Log.d(TAG, "前台服务启动命令已发送")
// 增加尝试次数计数器
val attempts = AtomicInteger(0)
val maxAttempts = 5
// 延迟尝试截图,给服务启动时间
val handler = Handler(Looper.getMainLooper())
val runnable = object : Runnable {
override fun run() {
val currentAttempt = attempts.incrementAndGet()
if (currentAttempt <= maxAttempts) {
val currentService = WebSocketForegroundService.getInstance()
Log.d(TAG, "尝试 #$currentAttempt 检查服务状态: $currentService")
if (currentService == null) {
Log.d(TAG, "服务仍未启动,$currentAttempt 秒后再次尝试")
handler.postDelayed(this, 1000)
} else {
Log.d(TAG, "服务已成功启动,开始截图操作")
takeScreenshot()
}
} else {
Log.e(TAG, "服务启动超时,无法完成截图操作")
runOnUiThread {
Toast.makeText(this@MainActivity, "服务启动超时,截图失败", Toast.LENGTH_SHORT).show()
}
}
}
}
handler.postDelayed(runnable, 1000)
return
}
Log.d(TAG, "服务实例已获取正在检查MediaProjection状态")
// 直接通过前台服务请求截图
try {
// 先检查是否已有可用的MediaProjection实例
if (WebSocketForegroundService.hasActiveMediaProjection()) {
Log.d(TAG, "发现已有可用的MediaProjection实例直接请求截图")
val currentService = WebSocketForegroundService.getInstance()
Log.d(TAG, "获取到的前台服务实例: $currentService")
if (currentService != null) {
Log.d(TAG, "调用服务的requestScreenshotSafely方法")
currentService.requestScreenshotSafely()
} else {
Log.e(TAG, "前台服务实例获取失败,无法请求截图")
runOnUiThread {
Toast.makeText(this@MainActivity, "截图服务不可用", Toast.LENGTH_SHORT).show()
}
}
return
} else {
Log.d(TAG, "没有可用的MediaProjection实例")
}
// 如果没有可用的MediaProjection实例启动服务并请求权限
Log.d(TAG, "准备启动服务并请求MediaProjection权限")
WebSocketForegroundService.startServiceForMediaProjection(this, object : WebSocketForegroundService.Companion.ServiceReadyCallback {
override fun onServiceReady() {
Log.d(TAG, "服务已准备就绪,开始请求权限")
// 服务启动后,请求权限
runOnUiThread {
val mainActivity = this@MainActivity
try {
val mediaProjectionManager = mainActivity.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
val captureIntent = mediaProjectionManager.createScreenCaptureIntent()
Log.d(TAG, "创建了屏幕捕获意图,准备启动权限请求对话框")
mainActivity.mediaProjectionLauncher.launch(captureIntent)
Log.d(TAG, "权限请求对话框已启动")
} catch (e: Exception) {
Log.e(TAG, "创建屏幕捕获意图失败: ${e.message}", e)
Toast.makeText(mainActivity, "无法请求截图权限: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
}
})
} catch (e: Exception) {
Log.e(TAG, "启动截图服务失败: ${e.message}", e)
runOnUiThread {
Toast.makeText(this@MainActivity, "截图失败: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
}
override fun onServiceReady() {
Log.d(TAG, "前台服务已准备就绪,请求屏幕捕获权限")
requestScreenshotPermission()
}
// 请求截图权限的方法
private fun requestScreenshotPermission() {
try {
// 检查是否有MediaProjection权限
val mediaProjectionManager = getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
val captureIntent = mediaProjectionManager.createScreenCaptureIntent()
mediaProjectionLauncher.launch(captureIntent)
} catch (e: Exception) {
Log.e(TAG, "请求截图权限失败", e)
runOnUiThread {
Toast.makeText(this, "请求截图权限失败", Toast.LENGTH_SHORT).show()
}
}
}
/**
* 发送截图数据到服务端
*/
fun sendScreenshotData(data: ByteArray) {
try {
// 检查WebSocket客户端是否连接
if (webSocketClient != null && isConnected) {
// 直接发送二进制数据
val byteString = ByteString.of(*data)
val sendResult = webSocketClient?.send(byteString)
Log.d(TAG, "发送二进制截图数据结果: $sendResult")
Log.d(TAG, "发送的二进制数据大小: ${data.size} 字节")
} else {
Log.e(TAG, "WebSocket未连接无法发送截图数据")
}
} catch (e: Exception) {
Log.e(TAG, "发送截图数据异常: ${e.message}", e)
}
}
private fun setupConnectButton() {
connectButton.setOnClickListener {
if (isConnected) {
// 断开连接
disconnect()
} else {
// 建立连接
connect()
}
}
}
/**
* 设置设置按钮的点击事件
*/
private fun setupSettingsButton() {
settingsButton.setOnClickListener {
// 启动设置页面
val intent = Intent(this, SettingsActivity::class.java)
startActivity(intent)
}
}
/**
* 尝试主动检查WebSocket的连接状态
* 这是一个辅助方法,用于在状态回调可能未被触发的情况下检查连接状态
*/
// 检查WebSocket连接状态
private fun checkWebSocketConnectionState(): Boolean {
try {
// 先检查客户端实例是否存在
if (webSocketClient == null) {
Log.d(TAG, "WebSocket client is null")
return false
}
// 获取当前连接状态
val actualConnectedState = webSocketClient!!.connectionState.value == WebSocketClient.ConnectionState.Connected
Log.d(TAG, "WebSocket connection state: $actualConnectedState")
// 更新类变量isConnected
this.isConnected = actualConnectedState
// 更新UI显示的连接状态
runOnUiThread {
updateConnectionStatus(actualConnectedState)
}
return actualConnectedState
} catch (e: Exception) {
Log.e(TAG, "Error checking WebSocket connection state", e)
runOnUiThread {
updateConnectionStatus(false)
}
return false
}
}
/**
* 连接到WebSocket服务器
*/
fun connect(): Boolean {
// 检查webSocketClient实例是否存在
if (webSocketClient == null) {
Log.e(TAG, "WebSocket client is not initialized")
return false
}
try {
// 检查当前连接状态,如果已经连接则先断开
if (webSocketClient!!.connectionState.value == WebSocketClient.ConnectionState.Connected) {
Log.d(TAG, "WebSocket is already connected, disconnecting first")
webSocketClient!!.disconnect()
}
// 连接到WebSocket服务器
webSocketClient!!.connect()
Log.d(TAG, "Attempting to connect to WebSocket server")
// 不立即更新UI为已连接状态等待实际连接结果
// UI更新将由连接状态回调处理
return true
} catch (e: Exception) {
Log.e(TAG, "Error connecting to WebSocket server", e)
runOnUiThread {
updateConnectionStatus(false)
}
return false
}
}
/**
* 断开WebSocket连接
*/
fun disconnect() {
try {
if (webSocketClient != null && webSocketClient!!.connectionState.value == WebSocketClient.ConnectionState.Connected) {
webSocketClient!!.disconnect()
Log.d(TAG, "WebSocket disconnected")
}
} catch (e: Exception) {
Log.e(TAG, "Error disconnecting WebSocket", e)
}
}
/**
* 重新连接WebSocket
*/
private fun reconnectWebSocket() {
try {
// 标记重连中状态
isReconnecting.set(true)
// 断开现有连接
disconnect()
// 延迟一段时间后重新连接
Handler(Looper.getMainLooper()).postDelayed({
try {
Log.d(TAG, "Attempting to reconnect WebSocket")
connect()
} catch (e: Exception) {
Log.e(TAG, "Reconnection failed", e)
} finally {
isReconnecting.set(false)
}
}, RECONNECT_DELAY_MS)
} catch (e: Exception) {
Log.e(TAG, "Error during reconnection", e)
isReconnecting.set(false)
}
}
// 更新连接状态显示
private fun updateConnectionStatus(isConnected: Boolean) {
try {
if (!isFinishing) {
runOnUiThread {
try {
// 更新连接状态文本和图标
connectionStatusText.text = if (isConnected) "已连接" else "未连接"
connectionStatusText.setTextColor(
if (isConnected) android.graphics.Color.GREEN else android.graphics.Color.RED
)
// 更新连接按钮状态
connectButton.isEnabled = (!isConnected) && (!isReconnecting.get())
// 更新连接状态指示器
connectionStatusIndicator.setBackgroundResource(
if (isConnected) R.drawable.connection_status_circle_connected else R.drawable.connection_status_circle
)
} catch (e: Exception) {
Log.e(TAG, "Error updating UI in updateConnectionStatus", e)
}
}
}
} catch (e: Exception) {
Log.e(TAG, "Error in updateConnectionStatus", e)
}
}
override fun onPause() {
super.onPause()
Log.d(TAG, "onPause()被调用,应用进入后台")
// 如果WebSocket连接已建立启动前台服务以保持连接
if (isConnected) {
Log.d(TAG, "WebSocket已连接启动前台服务")
WebSocketForegroundService.startService(this)
}
}
override fun onStop() {
super.onStop()
Log.d(TAG, "onStop()被调用")
// 双重检查如果WebSocket连接已建立但前台服务未启动这里再启动一次
if (isConnected) {
Log.d(TAG, "onStop(): WebSocket已连接确认前台服务状态")
}
}
override fun onDestroy() {
super.onDestroy()
Log.d(TAG, "onDestroy()被调用,释放资源")
// 停止前台服务
WebSocketForegroundService.stopService(this)
// 确保在Activity销毁时释放WebSocket资源
webSocketClient?.release()
webSocketClient = null
// 注销截图数据广播接收器
unregisterScreenshotBroadcastReceiver()
}
override fun onResume() {
super.onResume()
Log.d(TAG, "onResume()被调用检查URL配置是否变更")
// 重新从SharedPreferences获取配置的WebSocket URL
val sharedPreferences = androidx.preference.PreferenceManager.getDefaultSharedPreferences(this)
val defaultUrl = getString(R.string.web_socket_url)
val newWebSocketUrl = sharedPreferences.getString(getString(R.string.pref_key_web_socket_url), defaultUrl) ?: defaultUrl
// 比较URL是否发生了变化
if (newWebSocketUrl != webSocketUrl) {
Log.d(TAG, "检测到WebSocket URL发生变化: 旧URL='$webSocketUrl', 新URL='$newWebSocketUrl'")
// 更新URL
webSocketUrl = newWebSocketUrl
// 保存当前连接状态
val wasConnected = isConnected
// 断开现有连接并释放资源
webSocketClient?.release()
webSocketClient = null
// 重新初始化WebSocket客户端
initWebSocketClient()
// 如果之前是连接状态尝试使用新URL重新连接
if (wasConnected) {
Log.d(TAG, "尝试使用新URL重新连接WebSocket")
connect()
}
}
}
}

View File

@@ -0,0 +1,75 @@
package com.joyd.autobot
import android.content.SharedPreferences
import android.os.Bundle
import android.preference.PreferenceManager
import android.util.Log
import android.widget.Button
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
class SettingsActivity : AppCompatActivity() {
private lateinit var sharedPreferences: SharedPreferences
private lateinit var webSocketUrlEditText: EditText
private lateinit var saveButton: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
// 初始化Preferences
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
// 绑定UI组件
webSocketUrlEditText = findViewById(R.id.web_socket_url_edit_text)
saveButton = findViewById(R.id.save_settings_btn)
// 加载保存的设置
loadSavedSettings()
// 设置保存按钮的点击事件
saveButton.setOnClickListener {
saveSettings()
}
}
private fun loadSavedSettings() {
// 加载保存的WebSocket地址如果没有保存则使用默认地址
val defaultUrl = "ws://192.168.2.236:8805"
val webSocketUrl = sharedPreferences.getString(getString(R.string.pref_key_web_socket_url), defaultUrl)
webSocketUrlEditText.setText(webSocketUrl)
}
private fun saveSettings() {
// 获取用户输入的WebSocket地址
val webSocketUrl = webSocketUrlEditText.text.toString().trim()
// 简单验证URL格式
if (webSocketUrl.isEmpty() || !isValidWebSocketUrl(webSocketUrl)) {
Toast.makeText(this, "请输入有效的WebSocket地址", Toast.LENGTH_SHORT).show()
return
}
// 保存设置
with(sharedPreferences.edit()) {
putString(getString(R.string.pref_key_web_socket_url), webSocketUrl)
apply()
}
Log.d("SettingsActivity", "保存WebSocket地址: $webSocketUrl")
Toast.makeText(this, "设置已保存", Toast.LENGTH_SHORT).show()
// 设置结果码为OK
setResult(RESULT_OK)
// 关闭Activity
finish()
}
private fun isValidWebSocketUrl(url: String): Boolean {
// 简单的WebSocket URL验证逻辑
return url.startsWith("ws://") || url.startsWith("wss://")
}
}

View File

@@ -0,0 +1,569 @@
package com.joyd.autobot
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.hardware.display.DisplayManager
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.PixelFormat
import android.hardware.display.VirtualDisplay
import android.media.Image
import android.media.ImageReader
import android.media.projection.MediaProjection
import android.media.projection.MediaProjectionManager
import android.os.Build
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.util.Log
import androidx.core.app.NotificationCompat
import kotlin.math.max
import java.lang.ref.WeakReference
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
class WebSocketForegroundService : Service() {
// 修复TAG常量定义
private val TAG = "WebSocketForegroundService"
companion object {
const val CHANNEL_ID = "WebSocketServiceChannel"
const val NOTIFICATION_ID = 1001
const val ACTION_START = "com.joyd.autobot.action.START"
const val ACTION_STOP = "com.joyd.autobot.action.STOP"
const val IMAGE_READER_FORMAT = android.graphics.PixelFormat.RGBA_8888
const val IMAGE_READER_MAX_IMAGES = 2
const val VIRTUAL_DISPLAY_FLAGS = DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR
// 用于存储服务启动回调的静态变量
private var serviceReadyCallback: ServiceReadyCallback? = null
// 服务实例引用
private var instance: WeakReference<WebSocketForegroundService>? = null
// 清理服务实例引用的方法
fun clearInstance() {
instance?.clear()
instance = null
}
// 静态方法设置MediaProjection实例
fun setMediaProjection(context: Context, resultCode: Int, resultData: Intent) {
val service = instance?.get()
if (service != null) {
service.setMediaProjection(resultCode, resultData)
}
}
// 静态方法检查是否有活跃的MediaProjection实例
fun hasActiveMediaProjection(): Boolean {
val service = instance?.get()
return service != null && service.mediaProjection != null
}
// 静态方法:启动服务
fun startService(context: Context) {
val intent = Intent(context, WebSocketForegroundService::class.java)
intent.action = ACTION_START
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}
// 静态方法:停止服务
fun stopService(context: Context) {
val intent = Intent(context, WebSocketForegroundService::class.java)
intent.action = ACTION_STOP
context.stopService(intent)
}
// 获取服务实例的方法
fun getInstance(): WebSocketForegroundService? {
return instance?.get()
}
// 静态方法:启动服务并设置回调
fun startServiceForMediaProjection(context: Context, callback: ServiceReadyCallback) {
// 设置回调
serviceReadyCallback = callback
// 启动服务
val intent = Intent(context, WebSocketForegroundService::class.java)
intent.action = ACTION_START
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}
// 定义服务准备就绪回调接口
interface ServiceReadyCallback {
fun onServiceReady()
}
}
// WebSocket客户端实例 - 类型为Any因为无法确定具体类型
private var webSocketClient: Any? = null
// MediaProjection相关变量
private var mediaProjection: MediaProjection? = null
private var virtualDisplay: VirtualDisplay? = null
private var imageReader: ImageReader? = null
// 截图请求队列
private val screenshotRequestQueue = java.util.concurrent.ConcurrentLinkedQueue<() -> Unit>()
// 用于表示截图请求正在处理中的标志
private val isProcessingScreenshot = AtomicBoolean(false)
// 用于处理截图队列的Handler
private var screenshotHandler: Handler? = null
// 截图请求处理Runnable
private val screenshotRunnable = object : Runnable {
override fun run() {
try {
// 检查是否正在处理截图
if (isProcessingScreenshot.get()) {
// 如果正在处理,稍后再检查
screenshotHandler?.postDelayed(this, 100)
return
}
// 尝试从队列中获取截图请求
val request = screenshotRequestQueue.poll()
if (request != null) {
// 标记为正在处理
isProcessingScreenshot.set(true)
try {
// 执行截图请求
request.invoke()
} catch (e: Exception) {
Log.e(TAG, "Error executing screenshot request", e)
} finally {
// 标记为处理完成
isProcessingScreenshot.set(false)
}
}
} catch (e: Exception) {
Log.e(TAG, "Error in screenshot runnable", e)
} finally {
// 如果队列不为空,继续处理
if (!screenshotRequestQueue.isEmpty()) {
screenshotHandler?.postDelayed(this, 100)
}
}
}
}
// 设置MediaProjection实例的方法
private fun setMediaProjection(resultCode: Int, resultData: Intent) {
try {
val mediaProjectionManager = getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
val newMediaProjection = mediaProjectionManager.getMediaProjection(resultCode, resultData)
// 释放旧的MediaProjection
releaseMediaProjection()
// 设置新的MediaProjection
mediaProjection = newMediaProjection
// 设置回调当MediaProjection被系统终止时收到通知
mediaProjection?.registerCallback(object : MediaProjection.Callback() {
override fun onStop() {
Log.d(TAG, "MediaProjection stopped")
// 发送广播通知MainActivity需要重新请求权限
sendMediaProjectionPermissionRequiredBroadcast()
}
}, null)
Log.d(TAG, "MediaProjection set successfully")
} catch (e: Exception) {
Log.e(TAG, "Error setting MediaProjection", e)
}
}
// 发送MediaProjection权限请求广播
private fun sendMediaProjectionPermissionRequiredBroadcast() {
try {
val intent = Intent("com.joyd.autobot.MEDIA_PROJECTION_PERMISSION_REQUIRED")
intent.setPackage(packageName) // 指定包名解决UnsafeImplicitIntentLaunch错误
sendBroadcast(intent)
Log.d(TAG, "Sent MEDIA_PROJECTION_PERMISSION_REQUIRED broadcast")
} catch (e: Exception) {
Log.e(TAG, "Error sending broadcast", e)
}
}
// 释放MediaProjection资源
private fun releaseMediaProjection() {
try {
if (virtualDisplay != null) {
virtualDisplay?.release()
virtualDisplay = null
}
if (imageReader != null) {
imageReader?.close()
imageReader = null
}
if (mediaProjection != null) {
mediaProjection?.stop()
mediaProjection = null
}
} catch (e: Exception) {
Log.w(TAG, "Error stopping MediaProjection", e)
} finally {
mediaProjection = null
}
}
// 创建通知渠道Android O及以上版本需要
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
"WebSocket服务",
NotificationManager.IMPORTANCE_DEFAULT
)
channel.description = "保持WebSocket连接活跃"
channel.enableVibration(false)
channel.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
val notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.createNotificationChannel(channel)
}
}
// 创建前台服务通知
private fun createForegroundNotification(): Notification {
val builder = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("WebSocket服务")
.setContentText("保持WebSocket连接活跃")
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setOngoing(true)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder.setChannelId(CHANNEL_ID)
}
return builder.build()
}
// 安全地重建MediaProjection实例
private fun recreateMediaProjection(): Boolean {
try {
// 释放旧的MediaProjection
releaseMediaProjection()
// 创建新的MediaProjection但这需要用户授权所以这里只是清理工作
// 实际的MediaProjection创建需要通过MainActivity请求权限
Log.d(TAG, "MediaProjection has been cleared, ready for recreation")
// 发送广播通知MainActivity需要重新请求权限
sendMediaProjectionPermissionRequiredBroadcast()
return true
} catch (e: Exception) {
Log.e(TAG, "Error recreating MediaProjection", e)
return false
}
}
// 安全检查方法
private fun performSecurityChecks(): Boolean {
// 这里可以添加一些安全检查逻辑
// 例如检查应用是否在前台,是否有权限等
return true
}
// 安全地请求截图
fun requestScreenshotSafely() {
try {
// 先进行安全检查
if (!performSecurityChecks()) {
Log.w(TAG, "Security checks failed, cannot perform screenshot")
return
}
// 检查MediaProjection是否有效
if (mediaProjection == null) {
Log.w(TAG, "MediaProjection is null, cannot perform screenshot")
// 尝试重新创建MediaProjection
if (!recreateMediaProjection()) {
return
}
// 由于重新创建MediaProjection需要用户授权这里不能立即执行截图
return
}
// 将截图请求添加到队列
screenshotRequestQueue.add {
// 执行实际的截图操作
takeScreenshot()
}
// 确保Handler已初始化
if (screenshotHandler == null) {
screenshotHandler = Handler(Looper.getMainLooper())
}
// 启动截图队列处理
screenshotHandler?.removeCallbacks(screenshotRunnable)
screenshotHandler?.post(screenshotRunnable)
} catch (e: Exception) {
Log.e(TAG, "Error in requestScreenshotSafely", e)
}
}
// 执行实际的截图操作
private fun takeScreenshot() {
try {
Log.d(TAG, "Starting screenshot process")
// 获取屏幕参数
val metrics = resources.displayMetrics
val screenWidth = metrics.widthPixels
val screenHeight = metrics.heightPixels
val screenDensity = metrics.densityDpi
Log.d(TAG, "Screen parameters: $screenWidth x $screenHeight, density: $screenDensity")
// 创建ImageReader
imageReader = ImageReader.newInstance(screenWidth, screenHeight, IMAGE_READER_FORMAT, IMAGE_READER_MAX_IMAGES)
// 创建VirtualDisplay
virtualDisplay = mediaProjection?.createVirtualDisplay(
"ScreenshotDisplay",
screenWidth,
screenHeight,
screenDensity,
VIRTUAL_DISPLAY_FLAGS,
imageReader?.surface,
null,
null
)
if (virtualDisplay == null) {
Log.e(TAG, "Failed to create VirtualDisplay")
return
}
Log.d(TAG, "VirtualDisplay created successfully")
// 设置ImageReader的回调
val imageAvailableListener = ImageReader.OnImageAvailableListener { reader ->
try {
// 获取Image对象
val image = reader.acquireLatestImage()
if (image == null) {
Log.e(TAG, "Failed to acquire image")
return@OnImageAvailableListener
}
try {
// 处理Image对象
val bitmap = imageToBitmap(image)
if (bitmap != null) {
// 将Bitmap转换为字节数组
val screenshotData = bitmapToByteArray(bitmap)
if (screenshotData != null) {
// 发送截图数据广播
sendScreenshotDataBroadcast(screenshotData)
} else {
Log.e(TAG, "Failed to convert bitmap to byte array")
}
} else {
Log.e(TAG, "Failed to convert image to bitmap")
}
} finally {
// 释放Image对象
image.close()
// 释放VirtualDisplay
if (virtualDisplay != null) {
virtualDisplay?.release()
virtualDisplay = null
}
// 关闭ImageReader
if (imageReader != null) {
imageReader?.close()
imageReader = null
}
}
} catch (e: Exception) {
Log.e(TAG, "Error in imageAvailableListener", e)
}
}
imageReader?.setOnImageAvailableListener(imageAvailableListener, null)
// 添加延迟确保VirtualDisplay已经稳定
Thread.sleep(100)
} catch (e: Exception) {
Log.e(TAG, "Error taking screenshot", e)
// 清理资源
try {
if (virtualDisplay != null) {
virtualDisplay?.release()
virtualDisplay = null
}
if (imageReader != null) {
imageReader?.close()
imageReader = null
}
} catch (cleanupException: Exception) {
Log.e(TAG, "Error during resource cleanup", cleanupException)
}
}
}
// 将Image对象转换为Bitmap
private fun imageToBitmap(image: Image): Bitmap? {
try {
// 获取Image的平面
val planes = image.planes
val buffer = planes[0].buffer
val pixelStride = planes[0].pixelStride
val rowStride = planes[0].rowStride
val rowPadding = rowStride - pixelStride * image.width
// 创建Bitmap
val bitmap = Bitmap.createBitmap(
image.width + rowPadding / pixelStride,
image.height,
Bitmap.Config.ARGB_8888
)
bitmap.copyPixelsFromBuffer(buffer)
// 裁剪Bitmap以移除额外的行填充
return Bitmap.createBitmap(bitmap, 0, 0, image.width, image.height)
} catch (e: Exception) {
Log.e(TAG, "Error converting image to bitmap", e)
return null
}
}
// 将Bitmap转换为字节数组
private fun bitmapToByteArray(bitmap: Bitmap?): ByteArray? {
if (bitmap == null) {
Log.e(TAG, "bitmapToByteArray: bitmap is null")
return null
}
try {
val outputStream = java.io.ByteArrayOutputStream()
// 使用JPEG格式替代PNG提高兼容性和转换成功率
val compressResult = bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream)
val byteArray = outputStream.toByteArray()
outputStream.close()
// 验证压缩结果和字节数组是否有效
if (!compressResult || byteArray.isEmpty()) {
Log.e(TAG, "bitmapToByteArray: compression failed or result is empty")
return null
}
Log.d(TAG, "bitmapToByteArray: conversion successful, size: ${byteArray.size} bytes")
return byteArray
} catch (e: Exception) {
Log.e(TAG, "Error converting bitmap to byte array", e)
return null
}
}
// 发送截图数据广播
private fun sendScreenshotDataBroadcast(data: ByteArray) {
try {
val intent = Intent("com.joyd.autobot.SCREENSHOT_DATA")
intent.putExtra("screenshot_data", data)
intent.setPackage(packageName) // 指定包名解决UnsafeImplicitIntentLaunch错误
sendBroadcast(intent)
Log.d(TAG, "Sent screenshot data broadcast, size: ${data.size} bytes")
} catch (e: Exception) {
Log.e(TAG, "Error sending screenshot data broadcast", e)
}
}
// 获取当前的MainActivity实例
// 注意此方法目前返回null因为我们已经移除了对MainActivity私有属性的直接访问
private fun getCurrentMainActivity(): Activity? {
try {
// 在现代Android版本中getRunningTasks已被弃用且不可用
// 因此此方法简化为直接返回null
Log.d(TAG, "MainActivity instance is not available through this method")
} catch (e: Exception) {
Log.e(TAG, "Error getting MainActivity", e)
}
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(TAG, "onStartCommand() called with action: ${intent?.action}")
// 初始化服务实例引用
instance = WeakReference(this)
// 处理不同的动作
when (intent?.action) {
ACTION_START -> {
// 启动前台服务
createNotificationChannel()
val notification = createForegroundNotification()
startForeground(NOTIFICATION_ID, notification)
// 如果有回调设置,通知服务已准备就绪
if (serviceReadyCallback != null) {
serviceReadyCallback?.onServiceReady()
serviceReadyCallback = null // 只回调一次
}
Log.d(TAG, "WebSocket foreground service started")
}
ACTION_STOP -> {
// 停止前台服务
stopForeground(true)
stopSelf()
Log.d(TAG, "WebSocket foreground service stopped")
}
else -> {
Log.w(TAG, "Unknown action received: ${intent?.action}")
}
}
// 如果服务被终止,不自动重启
return START_NOT_STICKY
}
override fun onDestroy() {
super.onDestroy()
// 清理资源
releaseMediaProjection()
// 注意这里不调用webSocketClient的close方法因为无法确定其具体类型
WebSocketForegroundService.clearInstance()
Log.d(TAG, "Service destroyed")
}
override fun onBind(intent: Intent?): IBinder? {
// 不支持绑定
return null
}
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/errorColor" />
</shape>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/successColor" />
</shape>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_focused="true">
<shape android:shape="rectangle">
<corners android:radius="8dp" />
<solid android:color="#FFFFFF" />
<stroke
android:width="2dp"
android:color="@color/primaryColor" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<corners android:radius="8dp" />
<solid android:color="#FFFFFF" />
<stroke
android:width="1dp"
android:color="#CCCCCC" />
</shape>
</item>
</selector>

View File

@@ -0,0 +1,51 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<!-- 主背景 -->
<path
android:fillColor="#1976D2"
android:pathData="M0,0h108v108h-108z" />
<!-- 装饰性网格线 - 科技感 -->
<path
android:fillColor="#00000000"
android:pathData="M0,36h108M0,72h108M36,0v108M72,0v108"
android:strokeWidth="0.5"
android:strokeColor="#33FFFFFF" />
<!-- 中心发光效果 -->
<path
android:pathData="M54,54m-30,0a30,30 0 1,0 60,0a30,30 0 1,0 -60,0">
<aapt:attr name="android:fillColor">
<radialGradient
android:centerX="54"
android:centerY="54"
android:gradientRadius="30"
android:type="radial">
<item
android:color="#20FFFFFF"
android:offset="0.0" />
<item
android:color="#00FFFFFF"
android:offset="1.0" />
</radialGradient>
</aapt:attr>
</path>
<!-- 装饰性圆点 -->
<path
android:fillColor="#20FFFFFF"
android:pathData="M18,18m-2,0a2,2 0 1,1 4,0a2,2 0 1,1 -4,0" />
<path
android:fillColor="#20FFFFFF"
android:pathData="M90,18m-2,0a2,2 0 1,1 4,0a2,2 0 1,1 -4,0" />
<path
android:fillColor="#20FFFFFF"
android:pathData="M18,90m-2,0a2,2 0 1,1 4,0a2,2 0 1,1 -4,0" />
<path
android:fillColor="#20FFFFFF"
android:pathData="M90,90m-2,0a2,2 0 1,1 4,0a2,2 0 1,1 -4,0" />
</vector>

View File

@@ -0,0 +1,70 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<!-- 阴影效果 -->
<path android:pathData="M28,70l52,0l0,38l-52,0z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="60"
android:endY="100"
android:startX="60"
android:startY="70"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<!-- 机器人头部 -->
<path
android:fillColor="#2196F3"
android:pathData="M30,30h48v32c0,2.2 -1.8,4 -4,4H34c-2.2,0 -4,-1.8 -4,-4v-32z" />
<!-- 天线 -->
<path
android:fillColor="#2196F3"
android:pathData="M42,22h4v8h-4zM62,22h4v8h-4z" />
<!-- 眼睛 -->
<path
android:fillColor="#FFFFFF"
android:pathData="M40,40h8v8h-8zM60,40h8v8h-8z" />
<!-- 机器人身体 -->
<path
android:fillColor="#1976D2"
android:pathData="M34,66h40c1.1,0 2,0.9 2,2v28c0,1.1 -0.9,2 -2,2H34c-1.1,0 -2,-0.9 -2,-2v-28c0,-1.1 0.9,-2 2,-2z" />
<!-- 自动化符号 - 循环箭头 -->
<path
android:fillColor="#FFFFFF"
android:pathData="M48,78c-2.2,0 -4,1.8 -4,4s1.8,4 4,4s4,-1.8 4,-4s-1.8,-4 -4,-4zM72,78c-2.2,0 -4,1.8 -4,4s1.8,4 4,4s4,-1.8 4,-4s-1.8,-4 -4,-4z" />
<path
android:strokeColor="#FFFFFF"
android:strokeWidth="2"
android:strokeLineCap="round"
android:pathData="M52,82l8,0M72,82l-8,0" />
<path
android:strokeColor="#FFFFFF"
android:strokeWidth="2"
android:strokeLineCap="round"
android:pathData="M56,78v-8c0,-2.2 1.8,-4 4,-4s4,1.8 4,4v4" />
<path
android:strokeColor="#FFFFFF"
android:strokeWidth="2"
android:strokeLineCap="round"
android:pathData="M64,82v4c0,2.2 -1.8,4 -4,4s-4,-1.8 -4,-4v-8" />
<!-- 底部装饰 -->
<path
android:fillColor="#0D47A1"
android:pathData="M34,96h40v4h-40z" />
</vector>

View File

@@ -0,0 +1,37 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<!-- 单色机器人图标 - 简化版本 -->
<path
android:fillColor="#FFFFFF"
android:pathData="M30,30h48v32c0,2.2 -1.8,4 -4,4H34c-2.2,0 -4,-1.8 -4,-4v-32z" />
<!-- 天线 -->
<path
android:fillColor="#FFFFFF"
android:pathData="M42,22h4v8h-4zM62,22h4v8h-4z" />
<!-- 眼睛 -->
<path
android:fillColor="#000000"
android:pathData="M42,42h4v4h-4zM62,42h4v4h-4z" />
<!-- 机器人身体 -->
<path
android:fillColor="#FFFFFF"
android:pathData="M34,66h40c1.1,0 2,0.9 2,2v28c0,1.1 -0.9,2 -2,2H34c-1.1,0 -2,-0.9 -2,-2v-28c0,-1.1 0.9,-2 2,-2z" />
<!-- 简化的自动化符号 -->
<path
android:fillColor="#000000"
android:pathData="M48,78c-2.2,0 -4,1.8 -4,4s1.8,4 4,4s4,-1.8 4,-4s-1.8,-4 -4,-4zM72,78c-2.2,0 -4,1.8 -4,4s1.8,4 4,4s4,-1.8 4,-4s-1.8,-4 -4,-4z" />
<!-- 连接线条 -->
<path
android:strokeColor="#000000"
android:strokeWidth="2"
android:strokeLineCap="round"
android:pathData="M56,78v-4M64,82v4" />
</vector>

View File

@@ -0,0 +1,221 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background_material_light"
tools:context=".MainActivity">
<!-- 顶部标题区域 -->
<LinearLayout
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp"
android:gravity="center"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/app_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="智动助手"
android:textSize="28sp"
android:textStyle="bold"
android:textColor="@color/primary_text_default_material_light" />
<TextView
android:id="@+id/app_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="安卓自动化脚本工具"
android:textSize="16sp"
android:textColor="@color/secondary_text_default_material_light"
android:layout_marginTop="8dp" />
</LinearLayout>
<!-- 连接状态区域 -->
<LinearLayout
android:id="@+id/connection_status_area"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
android:gravity="center_vertical"
android:background="@color/cardBackgroundColor"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:elevation="2dp"
app:layout_constraintTop_toBottomOf="@id/header"
app:layout_constraintBottom_toTopOf="@id/feature_cards">
<View
android:id="@+id/connection_status_indicator"
android:layout_width="16dp"
android:layout_height="16dp"
android:background="@drawable/connection_status_circle" />
<TextView
android:id="@+id/connection_status_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="连接状态: 未连接"
android:textSize="16sp"
android:textColor="@color/textPrimary"
android:layout_marginLeft="12dp" />
<Button
android:id="@+id/connect_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="连接"
android:textAllCaps="false"
android:backgroundTint="@color/primaryColor" />
</LinearLayout>
<!-- 功能卡片区域 -->
<LinearLayout
android:id="@+id/feature_cards"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"
android:gravity="center"
app:layout_constraintTop_toBottomOf="@id/connection_status_area"
app:layout_constraintBottom_toTopOf="@id/action_buttons">
<androidx.cardview.widget.CardView
android:id="@+id/script_list_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:elevation="4dp"
app:cardBackgroundColor="@color/cardview_light_background"
app:cardCornerRadius="12dp"
app:cardUseCompatPadding="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="脚本列表"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/primary_text_default_material_light" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="管理和运行您的自动化脚本"
android:textSize="14sp"
android:textColor="@color/secondary_text_default_material_light"
android:layout_marginTop="4dp" />
<Button
android:id="@+id/manage_scripts_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="管理脚本"
android:layout_marginTop="16dp"
android:textAllCaps="false"
android:backgroundTint="@color/colorAccent" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/quick_actions_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="4dp"
app:cardBackgroundColor="@color/cardview_light_background"
app:cardCornerRadius="12dp"
app:cardUseCompatPadding="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="快捷操作"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/primary_text_default_material_light" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="快速启动常用功能"
android:textSize="14sp"
android:textColor="@color/secondary_text_default_material_light"
android:layout_marginTop="4dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:layout_marginTop="16dp">
<Button
android:id="@+id/start_recording_btn"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="录制脚本"
android:layout_marginRight="8dp"
android:textAllCaps="false" />
<Button
android:id="@+id/run_last_script_btn"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="运行上次脚本"
android:layout_marginLeft="8dp"
android:textAllCaps="false" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
<!-- 底部操作按钮 -->
<LinearLayout
android:id="@+id/action_buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="parent">
<Button
android:id="@+id/settings_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="设置"
android:textAllCaps="false"
android:layout_marginRight="16dp" />
<Button
android:id="@+id/about_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="关于"
android:textAllCaps="false" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background_material_light"
tools:context=".SettingsActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="设置"
android:textSize="28sp"
android:textStyle="bold"
android:textColor="@color/primary_text_default_material_light"
android:layout_marginBottom="24dp" />
<!-- WebSocket地址设置 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="WebSocket地址"
android:textSize="16sp"
android:textColor="@color/primary_text_default_material_light"
android:layout_marginBottom="8dp" />
<EditText
android:id="@+id/web_socket_url_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri"
android:hint="ws://192.168.2.236:8805"
android:textSize="16sp"
android:padding="12dp"
android:background="@drawable/edit_text_background"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="示例: ws://192.168.2.236:8805"
android:textSize="12sp"
android:textColor="@color/secondary_text_default_material_light" />
</LinearLayout>
<!-- 保存按钮 -->
<Button
android:id="@+id/save_settings_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="保存"
android:textSize="16sp"
android:padding="12dp"
android:textAllCaps="false"
android:backgroundTint="@color/primaryColor" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon>

View File

@@ -0,0 +1,7 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.AutoBot" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your dark theme here. -->
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
</style>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="primaryColor">#1E88E5</color>
<color name="primaryDarkColor">#1565C0</color>
<color name="secondaryColor">#FFC107</color>
<color name="colorAccent">#FF4081</color>
<color name="successColor">#4CAF50</color>
<color name="warningColor">#FF9800</color>
<color name="errorColor">#F44336</color>
<color name="textPrimary">#212121</color>
<color name="textSecondary">#757575</color>
<color name="backgroundColor">#F5F5F5</color>
<color name="cardBackgroundColor">#FFFFFF</color>
</resources>

View File

@@ -0,0 +1,9 @@
<resources>
<string name="app_name">智动助手</string>
<string name="pref_key_web_socket_url">web_socket_url</string>
<string name="web_socket_url">ws://localhost:8080/ws</string>
<string name="connect">连接</string>
<string name="disconnect">断开</string>
<string name="status_connected">已连接</string>
<string name="status_disconnected">未连接</string>
</resources>

View File

@@ -0,0 +1,9 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.AutoBot" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
</style>
<style name="Theme.AutoBot" parent="Base.Theme.AutoBot" />
</resources>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</base-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">192.168.2.236</domain>
<domain includeSubdomains="true">localhost</domain>
</domain-config>
</network-security-config>

View File

@@ -0,0 +1,17 @@
package com.joyd.autobot
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}