Add AutoRobot directory with Windows line endings
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
57
AutoRobot/Android/Robot/app/src/main/AndroidManifest.xml
Normal file
57
AutoRobot/Android/Robot/app/src/main/AndroidManifest.xml
Normal 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>
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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://")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
16
AutoRobot/Android/Robot/app/src/main/res/values/colors.xml
Normal file
16
AutoRobot/Android/Robot/app/src/main/res/values/colors.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user