feat(android): add device readiness checks

This commit is contained in:
QiuSW
2026-07-25 18:41:16 +08:00
parent 04fa4a0994
commit d013ac8c84
19 changed files with 884 additions and 28 deletions
@@ -1,5 +1,6 @@
package com.roubao.autopilot
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.widget.Toast
@@ -30,6 +31,9 @@ import com.roubao.autopilot.agent.MobileAgent
import com.roubao.autopilot.controller.AppScanner
import com.roubao.autopilot.controller.DeviceController
import com.roubao.autopilot.data.*
import com.roubao.autopilot.readiness.DeviceReadinessChecker
import com.roubao.autopilot.readiness.DeviceReadinessSnapshot
import com.roubao.autopilot.readiness.PINDUODUO_PACKAGE
import com.roubao.autopilot.ui.screens.*
import com.roubao.autopilot.ui.theme.*
import androidx.compose.ui.graphics.toArgb
@@ -45,8 +49,8 @@ import android.util.Log
private const val TAG = "MainActivity"
sealed class Screen(val route: String, val title: String, val icon: ImageVector, val selectedIcon: ImageVector) {
object Device : Screen("device", "设备", Icons.Outlined.Build, Icons.Filled.Build)
object Home : Screen("home", "肉包", Icons.Outlined.Home, Icons.Filled.Home)
object Capabilities : Screen("capabilities", "能力", Icons.Outlined.Star, Icons.Filled.Star)
object History : Screen("history", "记录", Icons.Outlined.List, Icons.Filled.List)
object Settings : Screen("settings", "设置", Icons.Outlined.Settings, Icons.Filled.Settings)
}
@@ -56,9 +60,11 @@ class MainActivity : ComponentActivity() {
private lateinit var deviceController: DeviceController
private lateinit var settingsManager: SettingsManager
private lateinit var executionRepository: ExecutionRepository
private lateinit var readinessChecker: DeviceReadinessChecker
private val mobileAgent = mutableStateOf<MobileAgent?>(null)
private var shizukuAvailable = mutableStateOf(false)
private val readinessSnapshot = mutableStateOf(DeviceReadinessSnapshot.empty())
// 当前执行的协程 Job(用于停止任务)
private var currentExecutionJob: kotlinx.coroutines.Job? = null
@@ -113,6 +119,8 @@ class MainActivity : ComponentActivity() {
deviceController.setCacheDir(cacheDir)
settingsManager = SettingsManager(this)
executionRepository = ExecutionRepository(this)
readinessChecker = DeviceReadinessChecker(this)
refreshReadiness()
// 加载执行记录
lifecycleScope.launch {
@@ -164,10 +172,9 @@ class MainActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainApp() {
var currentScreen by remember { mutableStateOf<Screen>(Screen.Home) }
var currentScreen by remember { mutableStateOf<Screen>(Screen.Device) }
var selectedRecord by remember { mutableStateOf<ExecutionRecord?>(null) }
var showShizukuHelpDialog by remember { mutableStateOf(false) }
var hasShownShizukuHelp by remember { mutableStateOf(false) }
val settings by settingsManager.settings.collectAsState()
val colors = BaoziTheme.colors
@@ -179,6 +186,7 @@ class MainActivity : ComponentActivity() {
val executing by remember { isExecuting }
val navigateToRecord by remember { shouldNavigateToRecord }
val recordId by remember { currentRecordId }
val readiness by remember { readinessSnapshot }
// 监听跳转事件
LaunchedEffect(navigateToRecord, recordId) {
@@ -193,14 +201,6 @@ class MainActivity : ComponentActivity() {
}
}
// 首次进入且 Shizuku 未连接时,显示帮助引导(只显示一次)
LaunchedEffect(Unit) {
if (!isShizukuAvailable && settings.hasSeenOnboarding && !hasShownShizukuHelp) {
hasShownShizukuHelp = true
showShizukuHelpDialog = true
}
}
Scaffold(
modifier = Modifier.background(colors.background),
containerColor = colors.background,
@@ -211,7 +211,7 @@ class MainActivity : ComponentActivity() {
contentColor = colors.textPrimary,
tonalElevation = 0.dp
) {
listOf(Screen.Home, Screen.Capabilities, Screen.History, Screen.Settings).forEach { screen ->
listOf(Screen.Device, Screen.Home, Screen.History, Screen.Settings).forEach { screen ->
val selected = currentScreen == screen
NavigationBarItem(
icon = {
@@ -262,6 +262,14 @@ class MainActivity : ComponentActivity() {
label = "screen"
) { screen ->
when (screen) {
Screen.Device -> DeviceReadinessScreen(
snapshot = readiness,
onRefresh = { refreshReadiness() },
onOpenAccessibilitySettings = {
startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
},
onOpenPinduoduo = { openPinduoduo() }
)
Screen.Home -> {
// 每次进入首页都检测 Shizuku 状态
LaunchedEffect(Unit) {
@@ -291,7 +299,6 @@ class MainActivity : ComponentActivity() {
isExecuting = executing
)
}
Screen.Capabilities -> CapabilitiesScreen()
Screen.History -> HistoryScreen(
records = records,
onRecordClick = { record -> selectedRecord = record },
@@ -346,6 +353,13 @@ class MainActivity : ComponentActivity() {
}
}
override fun onResume() {
super.onResume()
if (::readinessChecker.isInitialized) {
refreshReadiness()
}
}
override fun onDestroy() {
super.onDestroy()
Shizuku.removeBinderReceivedListener(binderReceivedListener)
@@ -354,6 +368,20 @@ class MainActivity : ComponentActivity() {
deviceController.unbindService()
}
private fun refreshReadiness() {
readinessSnapshot.value = readinessChecker.snapshot()
}
private fun openPinduoduo() {
val launchIntent = packageManager.getLaunchIntentForPackage(PINDUODUO_PACKAGE)
if (launchIntent == null) {
Toast.makeText(this, "未安装拼多多", Toast.LENGTH_SHORT).show()
refreshReadiness()
return
}
startActivity(launchIntent)
}
private fun checkShizukuPermission(): Boolean {
return try {
val granted = Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED
@@ -365,7 +393,7 @@ class MainActivity : ComponentActivity() {
}
}
private fun checkAndUpdateShizukuStatus() {
private fun checkAndUpdateShizukuStatus(requestPermission: Boolean = false) {
Log.d(TAG, "checkAndUpdateShizukuStatus called")
try {
val binderAlive = Shizuku.pingBinder()
@@ -379,7 +407,7 @@ class MainActivity : ComponentActivity() {
if (hasPermission) {
Log.d(TAG, "Binding Shizuku service")
deviceController.bindService()
} else {
} else if (requestPermission) {
Log.d(TAG, "Requesting Shizuku permission")
requestShizukuPermission()
}
@@ -396,7 +424,7 @@ class MainActivity : ComponentActivity() {
private fun refreshShizukuStatus() {
Log.d(TAG, "refreshShizukuStatus called by user")
Toast.makeText(this, "正在检查 Shizuku 状态...", Toast.LENGTH_SHORT).show()
checkAndUpdateShizukuStatus()
checkAndUpdateShizukuStatus(requestPermission = true)
if (shizukuAvailable.value && checkShizukuPermission()) {
Toast.makeText(this, "Shizuku 已连接", Toast.LENGTH_SHORT).show()
@@ -0,0 +1,92 @@
package com.roubao.autopilot.accessibility
import android.accessibilityservice.AccessibilityService
import android.os.SystemClock
import android.util.Log
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import com.roubao.autopilot.readiness.DeviceObservationStore
import com.roubao.autopilot.readiness.LoginBlockerDetector
import com.roubao.autopilot.readiness.PINDUODUO_PACKAGE
import java.util.ArrayDeque
class BuyerAccessibilityService : AccessibilityService() {
private var lastPinduoduoScanAt = 0L
override fun onServiceConnected() {
super.onServiceConnected()
DeviceObservationStore.setAccessibilityConnected(true)
Log.i(TAG, "Buyer accessibility service connected")
}
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
event ?: return
val packageName = event.packageName?.toString() ?: return
if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
DeviceObservationStore.recordForeground(packageName)
}
if (packageName != PINDUODUO_PACKAGE || !shouldInspect(event.eventType)) {
return
}
val now = SystemClock.elapsedRealtime()
if (now - lastPinduoduoScanAt < MIN_SCAN_INTERVAL_MS) {
return
}
lastPinduoduoScanAt = now
val root = rootInActiveWindow ?: event.source ?: return
val visibleTexts = collectVisibleTexts(root)
val blocker = LoginBlockerDetector.detect(visibleTexts)
DeviceObservationStore.recordPinduoduoPage(
blocker = blocker,
observedAtMillis = System.currentTimeMillis()
)
Log.d(TAG, "Pinduoduo page classified as ${blocker.name}")
}
override fun onInterrupt() {
Log.w(TAG, "Buyer accessibility service interrupted")
}
override fun onDestroy() {
DeviceObservationStore.setAccessibilityConnected(false)
super.onDestroy()
}
private fun shouldInspect(eventType: Int): Boolean =
eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED ||
eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED ||
eventType == AccessibilityEvent.TYPE_VIEW_SCROLLED
private fun collectVisibleTexts(root: AccessibilityNodeInfo): List<String> {
val texts = ArrayList<String>()
val queue = ArrayDeque<AccessibilityNodeInfo>()
queue.add(root)
var visited = 0
while (queue.isNotEmpty() && visited < MAX_NODES) {
val node = queue.removeFirst()
visited += 1
node.text?.toString()?.takeIf(String::isNotBlank)?.let(texts::add)
node.contentDescription
?.toString()
?.takeIf(String::isNotBlank)
?.let(texts::add)
for (index in 0 until node.childCount) {
node.getChild(index)?.let(queue::addLast)
}
}
return texts
}
companion object {
private const val TAG = "BuyerAccessibility"
private const val MAX_NODES = 300
private const val MIN_SCAN_INTERVAL_MS = 300L
}
}
@@ -0,0 +1,33 @@
package com.roubao.autopilot.readiness
data class DeviceObservation(
val accessibilityConnected: Boolean = false,
val foregroundPackage: String? = null,
val loginBlocker: LoginBlocker = LoginBlocker.UNKNOWN,
val pinduoduoObservedAtMillis: Long? = null
)
object DeviceObservationStore {
@Volatile
private var current = DeviceObservation()
fun snapshot(): DeviceObservation = current
@Synchronized
fun setAccessibilityConnected(connected: Boolean) {
current = current.copy(accessibilityConnected = connected)
}
@Synchronized
fun recordForeground(packageName: String) {
current = current.copy(foregroundPackage = packageName)
}
@Synchronized
fun recordPinduoduoPage(blocker: LoginBlocker, observedAtMillis: Long) {
current = current.copy(
loginBlocker = blocker,
pinduoduoObservedAtMillis = observedAtMillis
)
}
}
@@ -0,0 +1,70 @@
package com.roubao.autopilot.readiness
import android.accessibilityservice.AccessibilityServiceInfo
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Build
import android.view.accessibility.AccessibilityManager
import com.roubao.autopilot.accessibility.BuyerAccessibilityService
class DeviceReadinessChecker(context: Context) {
private val appContext = context.applicationContext
private val packageManager = appContext.packageManager
fun snapshot(): DeviceReadinessSnapshot {
val observation = DeviceObservationStore.snapshot()
return DeviceReadinessSnapshot(
manufacturer = Build.MANUFACTURER,
model = Build.MODEL,
androidVersion = Build.VERSION.RELEASE,
androidApi = Build.VERSION.SDK_INT,
buyerApp = readVersion(appContext.packageName),
pinduoduo = readVersion(PINDUODUO_PACKAGE),
accessibilityEnabled = isBuyerAccessibilityEnabled(),
accessibilityConnected = observation.accessibilityConnected,
foregroundPackage = observation.foregroundPackage,
loginBlocker = observation.loginBlocker,
pinduoduoObservedAtMillis = observation.pinduoduoObservedAtMillis
)
}
private fun isBuyerAccessibilityEnabled(): Boolean {
val manager = appContext.getSystemService(AccessibilityManager::class.java)
val expectedService = BuyerAccessibilityService::class.java.name
return manager
.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK)
.any { info ->
info.resolveInfo.serviceInfo.packageName == appContext.packageName &&
info.resolveInfo.serviceInfo.name == expectedService
}
}
private fun readVersion(packageName: String): InstalledAppVersion =
try {
val packageInfo = getPackageInfo(packageName)
InstalledAppVersion(
installed = true,
versionName = packageInfo.versionName,
versionCode = packageInfo.longVersionCodeCompat()
)
} catch (_: PackageManager.NameNotFoundException) {
InstalledAppVersion(installed = false)
}
private fun getPackageInfo(packageName: String): PackageInfo =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0))
} else {
@Suppress("DEPRECATION")
packageManager.getPackageInfo(packageName, 0)
}
private fun PackageInfo.longVersionCodeCompat(): Long =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
longVersionCode
} else {
@Suppress("DEPRECATION")
versionCode.toLong()
}
}
@@ -0,0 +1,124 @@
package com.roubao.autopilot.readiness
const val PINDUODUO_PACKAGE = "com.xunmeng.pinduoduo"
data class InstalledAppVersion(
val installed: Boolean,
val versionName: String? = null,
val versionCode: Long? = null
)
enum class LoginBlocker {
NONE,
LOGIN_REQUIRED,
VERIFICATION_REQUIRED,
RISK_CONTROL,
UNKNOWN
}
enum class ReadinessBlocker {
PINDUODUO_NOT_INSTALLED,
ACCESSIBILITY_DISABLED,
ACCESSIBILITY_DISCONNECTED,
LOGIN_REQUIRED,
VERIFICATION_REQUIRED,
RISK_CONTROL
}
data class DeviceReadinessSnapshot(
val manufacturer: String,
val model: String,
val androidVersion: String,
val androidApi: Int,
val buyerApp: InstalledAppVersion,
val pinduoduo: InstalledAppVersion,
val accessibilityEnabled: Boolean,
val accessibilityConnected: Boolean,
val foregroundPackage: String?,
val loginBlocker: LoginBlocker,
val pinduoduoObservedAtMillis: Long?
) {
val blockers: List<ReadinessBlocker>
get() = buildList {
if (!pinduoduo.installed) {
add(ReadinessBlocker.PINDUODUO_NOT_INSTALLED)
}
if (!accessibilityEnabled) {
add(ReadinessBlocker.ACCESSIBILITY_DISABLED)
} else if (!accessibilityConnected) {
add(ReadinessBlocker.ACCESSIBILITY_DISCONNECTED)
}
when (loginBlocker) {
LoginBlocker.LOGIN_REQUIRED -> add(ReadinessBlocker.LOGIN_REQUIRED)
LoginBlocker.VERIFICATION_REQUIRED -> add(ReadinessBlocker.VERIFICATION_REQUIRED)
LoginBlocker.RISK_CONTROL -> add(ReadinessBlocker.RISK_CONTROL)
LoginBlocker.NONE,
LoginBlocker.UNKNOWN -> Unit
}
}
val canStartProbe: Boolean
get() = blockers.isEmpty()
companion object {
fun empty() = DeviceReadinessSnapshot(
manufacturer = "",
model = "",
androidVersion = "",
androidApi = 0,
buyerApp = InstalledAppVersion(installed = false),
pinduoduo = InstalledAppVersion(installed = false),
accessibilityEnabled = false,
accessibilityConnected = false,
foregroundPackage = null,
loginBlocker = LoginBlocker.UNKNOWN,
pinduoduoObservedAtMillis = null
)
}
}
object LoginBlockerDetector {
private val riskMarkers = listOf(
"安全验证",
"账号异常",
"操作频繁",
"存在风险",
"风险验证"
)
private val verificationMarkers = listOf(
"请输入验证码",
"获取验证码",
"短信验证码",
"滑块验证",
"拖动滑块"
)
private val loginMarkers = listOf(
"请先登录",
"登录后继续",
"手机号登录",
"密码登录",
"本机号码一键登录",
"登录/注册"
)
fun detect(visibleTexts: Collection<String>): LoginBlocker {
val normalized = visibleTexts
.asSequence()
.map(::normalize)
.filter(String::isNotEmpty)
.toList()
return when {
normalized.containsAny(riskMarkers) -> LoginBlocker.RISK_CONTROL
normalized.containsAny(verificationMarkers) -> LoginBlocker.VERIFICATION_REQUIRED
normalized.containsAny(loginMarkers) -> LoginBlocker.LOGIN_REQUIRED
else -> LoginBlocker.NONE
}
}
private fun normalize(value: String): String =
value.lowercase().replace(Regex("\\s+"), "")
private fun Collection<String>.containsAny(markers: Collection<String>): Boolean =
any { text -> markers.any(text::contains) }
}
@@ -0,0 +1,252 @@
package com.roubao.autopilot.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.ShoppingCart
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Divider
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.roubao.autopilot.readiness.DeviceReadinessSnapshot
import com.roubao.autopilot.readiness.LoginBlocker
import com.roubao.autopilot.readiness.PINDUODUO_PACKAGE
import com.roubao.autopilot.readiness.ReadinessBlocker
import com.roubao.autopilot.ui.theme.BaoziTheme
@Composable
fun DeviceReadinessScreen(
snapshot: DeviceReadinessSnapshot,
onRefresh: () -> Unit,
onOpenAccessibilitySettings: () -> Unit,
onOpenPinduoduo: () -> Unit
) {
val colors = BaoziTheme.colors
val ready = snapshot.canStartProbe
LazyColumn(
modifier = Modifier
.fillMaxSize()
.background(colors.background),
contentPadding = PaddingValues(horizontal = 20.dp, vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(0.dp)
) {
item {
Text(
text = "设备就绪检查",
fontSize = 28.sp,
fontWeight = FontWeight.Bold,
color = colors.textPrimary
)
Text(
text = if (ready) "自动化探针可以开始" else "请先处理阻塞项",
fontSize = 14.sp,
color = if (ready) colors.success else colors.error
)
Spacer(modifier = Modifier.height(20.dp))
}
item {
ReadinessRow(
title = "测试设备",
value = "${snapshot.manufacturer} ${snapshot.model} · Android ${snapshot.androidVersion} / API ${snapshot.androidApi}",
state = RowState.INFO
)
ReadinessRow(
title = "肉包 App",
value = snapshot.buyerApp.versionLabel(),
state = if (snapshot.buyerApp.installed) RowState.READY else RowState.BLOCKED
)
ReadinessRow(
title = "拼多多",
value = snapshot.pinduoduo.versionLabel(),
state = if (snapshot.pinduoduo.installed) RowState.READY else RowState.BLOCKED
)
ReadinessRow(
title = "肉包无障碍",
value = when {
!snapshot.accessibilityEnabled -> "未启用"
!snapshot.accessibilityConnected -> "已启用,服务未连接"
else -> "已启用并连接"
},
state = when {
!snapshot.accessibilityEnabled -> RowState.BLOCKED
!snapshot.accessibilityConnected -> RowState.WARNING
else -> RowState.READY
}
)
ReadinessRow(
title = "当前前台 App",
value = snapshot.foregroundPackage ?: "尚未观察",
state = if (snapshot.foregroundPackage == PINDUODUO_PACKAGE) {
RowState.READY
} else {
RowState.INFO
}
)
ReadinessRow(
title = "拼多多登录检查",
value = snapshot.loginBlocker.label(),
state = when (snapshot.loginBlocker) {
LoginBlocker.NONE -> RowState.READY
LoginBlocker.UNKNOWN -> RowState.INFO
else -> RowState.BLOCKED
}
)
}
if (snapshot.blockers.isNotEmpty()) {
item {
Spacer(modifier = Modifier.height(20.dp))
Text(
text = "阻塞原因",
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
color = colors.textPrimary
)
Spacer(modifier = Modifier.height(6.dp))
snapshot.blockers.forEach { blocker ->
Text(
text = "· ${blocker.label()}",
fontSize = 14.sp,
color = colors.error,
modifier = Modifier.padding(vertical = 3.dp)
)
}
}
}
item {
Spacer(modifier = Modifier.height(24.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(
onClick = onRefresh,
modifier = Modifier.weight(1f),
colors = ButtonDefaults.buttonColors(containerColor = colors.primary)
) {
Icon(Icons.Default.Refresh, contentDescription = null)
Spacer(modifier = Modifier.size(8.dp))
Text("重新检查")
}
OutlinedButton(
onClick = onOpenAccessibilitySettings,
modifier = Modifier.weight(1f)
) {
Icon(Icons.Default.Settings, contentDescription = null)
Spacer(modifier = Modifier.size(8.dp))
Text("无障碍设置")
}
}
Spacer(modifier = Modifier.height(8.dp))
OutlinedButton(
onClick = onOpenPinduoduo,
enabled = snapshot.pinduoduo.installed,
modifier = Modifier.fillMaxWidth()
) {
Icon(Icons.Default.ShoppingCart, contentDescription = null)
Spacer(modifier = Modifier.size(8.dp))
Text("打开拼多多并检查页面")
}
}
}
}
private enum class RowState {
READY,
WARNING,
BLOCKED,
INFO
}
@Composable
private fun ReadinessRow(title: String, value: String, state: RowState) {
val colors = BaoziTheme.colors
val (icon, tint) = when (state) {
RowState.READY -> Icons.Default.CheckCircle to colors.success
RowState.WARNING -> Icons.Default.Info to colors.warning
RowState.BLOCKED -> Icons.Default.Warning to colors.error
RowState.INFO -> Icons.Default.Info to colors.textSecondary
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 13.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = tint,
modifier = Modifier.size(22.dp)
)
Column(
modifier = Modifier
.weight(1f)
.padding(start = 12.dp)
) {
Text(
text = title,
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = colors.textPrimary
)
Text(
text = value,
fontSize = 13.sp,
color = colors.textSecondary
)
}
}
Divider(color = colors.surfaceVariant)
}
private fun com.roubao.autopilot.readiness.InstalledAppVersion.versionLabel(): String =
if (!installed) {
"未安装"
} else {
"${versionName ?: "未知版本"} (${versionCode ?: "?"})"
}
private fun LoginBlocker.label(): String = when (this) {
LoginBlocker.NONE -> "未发现阻塞(不代表已登录)"
LoginBlocker.LOGIN_REQUIRED -> "检测到登录页"
LoginBlocker.VERIFICATION_REQUIRED -> "检测到验证码"
LoginBlocker.RISK_CONTROL -> "检测到风控或安全验证"
LoginBlocker.UNKNOWN -> "尚未检查"
}
private fun ReadinessBlocker.label(): String = when (this) {
ReadinessBlocker.PINDUODUO_NOT_INSTALLED -> "未安装拼多多"
ReadinessBlocker.ACCESSIBILITY_DISABLED -> "未启用肉包无障碍服务"
ReadinessBlocker.ACCESSIBILITY_DISCONNECTED -> "肉包无障碍服务尚未连接"
ReadinessBlocker.LOGIN_REQUIRED -> "拼多多要求登录"
ReadinessBlocker.VERIFICATION_REQUIRED -> "拼多多要求验证码"
ReadinessBlocker.RISK_CONTROL -> "拼多多出现风控或安全验证"
}
@@ -144,7 +144,13 @@ class CrashHandler private constructor() : Thread.UncaughtExceptionHandler {
private fun getAppVersion(context: Context): String {
return try {
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
"${pInfo.versionName} (${pInfo.longVersionCode})"
val versionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
pInfo.longVersionCode
} else {
@Suppress("DEPRECATION")
pInfo.versionCode.toLong()
}
"${pInfo.versionName} ($versionCode)"
} catch (e: Exception) {
"Unknown"
}