fix: double back quitting

连接时导致界面无响应的问题没修好
This commit is contained in:
Miuzarte
2026-03-21 21:18:36 +08:00
parent cf3f6080d8
commit 07fa81dbb9
4 changed files with 160 additions and 74 deletions

View File

@@ -26,6 +26,12 @@
- 组件排序动画一坨 - 组件排序动画一坨
- 退出全屏时的横竖屏状态可能不对,断开 scrcpy 重连就好 - 退出全屏时的横竖屏状态可能不对,断开 scrcpy 重连就好
- 如果受控机在锁屏时处理网络连接较慢,会导致应用界面无响应过长时间被系统杀掉(点名你米)
## 建议搭配模块
- 密码锁屏无法捕获: [LSPosed/DisableFlagSecure](https://github.com/LSPosed/DisableFlagSecure)
- 开机自动启用 adb: [gist/906291](https://gist.github.com/Miuzarte/9062915f1615d5eebd363c759fda496c)
## 构建 ## 构建

View File

@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android">
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@@ -26,6 +25,9 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<!-- <profileable android:shell="true" /> -->
</application> </application>
</manifest> </manifest>

View File

@@ -1,9 +1,7 @@
package io.github.miuzarte.scrcpyforandroid.pages package io.github.miuzarte.scrcpyforandroid.pages
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.util.Log import android.util.Log
import android.view.WindowManager
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@@ -61,6 +59,7 @@ import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonCard import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonCard
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -73,6 +72,7 @@ import java.net.Socket
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import java.util.concurrent.Executors
import kotlin.math.roundToInt import kotlin.math.roundToInt
private const val ADB_CONNECT_TIMEOUT_MS = 3_000L private const val ADB_CONNECT_TIMEOUT_MS = 3_000L
@@ -125,7 +125,6 @@ fun DeviceTabScreen(
nativeCore: NativeCoreFacade, nativeCore: NativeCoreFacade,
snack: SnackbarHostState, snack: SnackbarHostState,
scrollBehavior: ScrollBehavior, scrollBehavior: ScrollBehavior,
keepScreenOnWhenStreamingEnabled: Boolean,
virtualButtonsOutside: List<String>, virtualButtonsOutside: List<String>,
virtualButtonsInMore: List<String>, virtualButtonsInMore: List<String>,
previewCardHeightDp: Int, previewCardHeightDp: Int,
@@ -222,9 +221,19 @@ fun DeviceTabScreen(
val virtualButtonLayout = remember(virtualButtonsOutside, virtualButtonsInMore) { val virtualButtonLayout = remember(virtualButtonsOutside, virtualButtonsInMore) {
VirtualButtonActions.resolveLayout(virtualButtonsOutside, virtualButtonsInMore) VirtualButtonActions.resolveLayout(virtualButtonsOutside, virtualButtonsInMore)
} }
val activity = remember(context) { context as? Activity }
val initialSettings = remember(context) { loadDevicePageSettings(context) } val initialSettings = remember(context) { loadDevicePageSettings(context) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val adbWorkerDispatcher = remember {
Executors.newSingleThreadExecutor { runnable ->
Thread(runnable, "adb-connect-worker").apply { isDaemon = true }
}.asCoroutineDispatcher()
}
DisposableEffect(adbWorkerDispatcher) {
onDispose {
adbWorkerDispatcher.close()
}
}
var busy by rememberSaveable { mutableStateOf(false) } var busy by rememberSaveable { mutableStateOf(false) }
var statusLine by rememberSaveable { mutableStateOf("未连接") } var statusLine by rememberSaveable { mutableStateOf("未连接") }
@@ -315,6 +324,46 @@ fun DeviceTabScreen(
} }
} }
suspend fun disconnectAdbConnection(
clearQuickOnlineForTarget: ConnectionTarget? = currentTarget,
logMessage: String? = null,
showSnackMessage: String? = null,
) {
withContext(adbWorkerDispatcher) {
runCatching { nativeCore.scrcpyStop() }
runCatching { nativeCore.adbDisconnect() }
}
adbConnected = false
currentTargetHost = ""
currentTargetPort = AppDefaults.ADB_PORT
audioForwardingSupported = true
cameraMirroringSupported = true
sessionInfo = null
statusLine = "未连接"
connectedDeviceLabel = "未连接"
clearQuickOnlineForTarget?.let { target ->
if (target.host.isNotBlank()) {
upsertQuickDevice(context, quickDevices, target.host, target.port, false)
}
}
logMessage?.let { logEvent(it) }
if (!showSnackMessage.isNullOrBlank()) {
scope.launch {
snack.showSnackbar(showSnackMessage)
}
}
}
suspend fun disconnectCurrentTargetBeforeConnecting(newHost: String, newPort: Int) {
val current = currentTarget
if (!adbConnected || current == null) return
if (current.host == newHost && current.port == newPort) return
sessionReconnectBlacklistHosts += current.host
logEvent("切换连接目标,先断开当前设备: ${current.host}:${current.port}")
disconnectAdbConnection(clearQuickOnlineForTarget = current)
}
fun applyConnectedDeviceCapabilities(sdkInt: Int, release: String) { fun applyConnectedDeviceCapabilities(sdkInt: Int, release: String) {
val audioSupported = sdkInt !in 0..<30 val audioSupported = sdkInt !in 0..<30
audioForwardingSupported = audioSupported audioForwardingSupported = audioSupported
@@ -337,7 +386,7 @@ fun DeviceTabScreen(
} }
suspend fun connectWithTimeout(host: String, port: Int): Boolean { suspend fun connectWithTimeout(host: String, port: Int): Boolean {
return withContext(Dispatchers.IO) { return withContext(adbWorkerDispatcher) {
withTimeout(ADB_CONNECT_TIMEOUT_MS) { withTimeout(ADB_CONNECT_TIMEOUT_MS) {
nativeCore.adbConnect(host, port) nativeCore.adbConnect(host, port)
} }
@@ -345,7 +394,7 @@ fun DeviceTabScreen(
} }
suspend fun keepAliveCheck(host: String, port: Int): Boolean { suspend fun keepAliveCheck(host: String, port: Int): Boolean {
return withContext(Dispatchers.IO) { return withContext(adbWorkerDispatcher) {
withTimeout(ADB_KEEPALIVE_TIMEOUT_MS) { withTimeout(ADB_KEEPALIVE_TIMEOUT_MS) {
val connected = nativeCore.adbIsConnected() val connected = nativeCore.adbIsConnected()
if (!connected) { if (!connected) {
@@ -381,7 +430,9 @@ fun DeviceTabScreen(
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
val detail = e.message?.takeIf { it.isNotBlank() } ?: e.javaClass.simpleName val detail = e.message?.takeIf { it.isNotBlank() } ?: e.javaClass.simpleName
logEvent("$label 参数错误: $detail", Log.WARN, e) logEvent("$label 参数错误: $detail", Log.WARN, e)
snack.showSnackbar("$label 参数错误: $detail") scope.launch {
snack.showSnackbar("$label 参数错误: $detail")
}
} catch (e: Exception) { } catch (e: Exception) {
val detail = e.message?.takeIf { it.isNotBlank() } ?: e.javaClass.simpleName val detail = e.message?.takeIf { it.isNotBlank() } ?: e.javaClass.simpleName
logEvent("$label 失败: $detail", Log.ERROR, e) logEvent("$label 失败: $detail", Log.ERROR, e)
@@ -403,7 +454,9 @@ fun DeviceTabScreen(
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
val detail = e.message?.takeIf { it.isNotBlank() } ?: e.javaClass.simpleName val detail = e.message?.takeIf { it.isNotBlank() } ?: e.javaClass.simpleName
logEvent("$label 参数错误: $detail", Log.WARN, e) logEvent("$label 参数错误: $detail", Log.WARN, e)
snack.showSnackbar("$label 参数错误: $detail") scope.launch {
snack.showSnackbar("$label 参数错误: $detail")
}
} catch (e: Exception) { } catch (e: Exception) {
val detail = e.message?.takeIf { it.isNotBlank() } ?: e.javaClass.simpleName val detail = e.message?.takeIf { it.isNotBlank() } ?: e.javaClass.simpleName
logEvent("$label 失败: $detail", Log.ERROR, e) logEvent("$label 失败: $detail", Log.ERROR, e)
@@ -414,6 +467,16 @@ fun DeviceTabScreen(
} }
} }
suspend fun runAutoAdbConnect(host: String, port: Int): Boolean {
return runCatching {
connectWithTimeout(host, port)
}.getOrElse { error ->
val detail = error.message?.takeIf { it.isNotBlank() } ?: error.javaClass.simpleName
logEvent("自动重连失败: $host:$port ($detail)", Log.WARN)
false
}
}
fun refreshEncoderLists() { fun refreshEncoderLists() {
if (!adbConnected) return if (!adbConnected) return
val remotePath = serverRemotePath.trim().ifBlank { AppDefaults.SERVER_REMOTE_PATH } val remotePath = serverRemotePath.trim().ifBlank { AppDefaults.SERVER_REMOTE_PATH }
@@ -627,10 +690,8 @@ fun DeviceTabScreen(
snack.showSnackbar("ADB 自动重连成功") snack.showSnackbar("ADB 自动重连成功")
} }
} else { } else {
runCatching { nativeCore.adbDisconnect() } disconnectAdbConnection()
statusLine = "ADB 连接断开" statusLine = "ADB 连接断开"
connectedDeviceLabel = "未连接"
sessionInfo = null
logEvent("ADB 自动重连失败: $host:$port", Log.ERROR) logEvent("ADB 自动重连失败: $host:$port", Log.ERROR)
scope.launch { scope.launch {
snack.showSnackbar("ADB 自动重连失败") snack.showSnackbar("ADB 自动重连失败")
@@ -662,20 +723,19 @@ fun DeviceTabScreen(
if (!portReachable) continue if (!portReachable) continue
quickConnectTriedOnce += targetKey quickConnectTriedOnce += targetKey
runAdbConnect("快速设备端口可达,尝试连接一次") { val ok = runAutoAdbConnect(target.host, target.port)
val ok = connectWithTimeout(target.host, target.port) adbConnected = ok
adbConnected = ok upsertQuickDevice(
upsertQuickDevice( context,
context, quickDevices,
quickDevices, target.host,
target.host, target.port,
target.port, ok
ok )
) if (ok) {
if (ok) { handleAdbConnected(target.host, target.port)
handleAdbConnected(target.host, target.port) logEvent("ADB 快速探测连接成功: ${target.host}:${target.port}")
logEvent("ADB 快速探测连接成功: ${target.host}:${target.port}") break
}
} }
} }
if (adbConnected) break if (adbConnected) break
@@ -727,22 +787,20 @@ fun DeviceTabScreen(
continue continue
} }
runAdbConnect("自动重连 ADB") { val ok = runAutoAdbConnect(discoveredHost, discoveredPort)
val ok = connectWithTimeout(discoveredHost, discoveredPort) adbConnected = ok
adbConnected = ok upsertQuickDevice(
upsertQuickDevice( context,
context, quickDevices,
quickDevices, discoveredHost,
discoveredHost, discoveredPort,
discoveredPort, ok
ok )
) if (ok) {
if (ok) { handleAdbConnected(discoveredHost, discoveredPort)
handleAdbConnected(discoveredHost, discoveredPort) logEvent("ADB 自动重连成功: $discoveredHost:$discoveredPort")
logEvent("ADB 自动重连成功: $discoveredHost:$discoveredPort") } else {
} else { logEvent("ADB 自动重连失败: $discoveredHost:$discoveredPort", Log.WARN)
logEvent("ADB 自动重连失败: $discoveredHost:$discoveredPort", Log.WARN)
}
} }
delay(ADB_AUTO_RECONNECT_RETRY_INTERVAL_MS) delay(ADB_AUTO_RECONNECT_RETRY_INTERVAL_MS)
@@ -759,19 +817,6 @@ fun DeviceTabScreen(
} }
} }
DisposableEffect(activity, keepScreenOnWhenStreamingEnabled, sessionInfo != null) {
val window = activity?.window
val shouldKeepScreenOn = keepScreenOnWhenStreamingEnabled && sessionInfo != null
if (window != null && shouldKeepScreenOn) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
onDispose {
if (window != null && shouldKeepScreenOn) {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
}
}
LaunchedEffect(sessionInfo) { LaunchedEffect(sessionInfo) {
if (sessionInfo != null) { if (sessionInfo != null) {
sessionInfoWidth = sessionInfo?.width ?: 0 sessionInfoWidth = sessionInfo?.width ?: 0
@@ -808,7 +853,6 @@ fun DeviceTabScreen(
onClearLogsActionChange(null) onClearLogsActionChange(null)
onCanClearLogsChange(false) onCanClearLogsChange(false)
onOpenReorderDevicesActionChange(null) onOpenReorderDevicesActionChange(null)
onSessionStartedChange(false)
} }
} }
@@ -915,26 +959,18 @@ fun DeviceTabScreen(
haptics.press() haptics.press()
if (isConnectedTarget) { if (isConnectedTarget) {
activeDeviceActionId = device.id activeDeviceActionId = device.id
runBusy("断开 ADB", onFinished = { activeDeviceActionId = null }) { runAdbConnect("断开 ADB", onFinished = { activeDeviceActionId = null }) {
nativeCore.adbDisconnect()
sessionReconnectBlacklistHosts += host sessionReconnectBlacklistHosts += host
adbConnected = false disconnectAdbConnection(
currentTargetHost = "" clearQuickOnlineForTarget = ConnectionTarget(host, port),
currentTargetPort = AppDefaults.ADB_PORT logMessage = "ADB 已断开: ${device.name}",
audioForwardingSupported = true showSnackMessage = "ADB 已断开",
cameraMirroringSupported = true )
sessionInfo = null
statusLine = "未连接"
connectedDeviceLabel = "未连接"
upsertQuickDevice(context, quickDevices, host, port, false)
logEvent("ADB 已断开: ${device.name}")
scope.launch {
snack.showSnackbar("ADB 已断开")
}
} }
} else { } else {
activeDeviceActionId = device.id activeDeviceActionId = device.id
runAdbConnect("连接 ADB", onFinished = { activeDeviceActionId = null }) { runAdbConnect("连接 ADB", onFinished = { activeDeviceActionId = null }) {
disconnectCurrentTargetBeforeConnecting(host, port)
val ok = connectWithTimeout(host, port) val ok = connectWithTimeout(host, port)
adbConnected = ok adbConnected = ok
upsertQuickDevice(context, quickDevices, host, port, ok) upsertQuickDevice(context, quickDevices, host, port, ok)
@@ -975,6 +1011,7 @@ fun DeviceTabScreen(
onConnect = { onConnect = {
val target = parseQuickTarget(quickConnectInput) ?: return@QuickConnectCard val target = parseQuickTarget(quickConnectInput) ?: return@QuickConnectCard
runAdbConnect("连接 ADB") { runAdbConnect("连接 ADB") {
disconnectCurrentTargetBeforeConnecting(target.host, target.port)
val ok = connectWithTimeout(target.host, target.port) val ok = connectWithTimeout(target.host, target.port)
adbConnected = ok adbConnected = ok
upsertQuickDevice(context, quickDevices, target.host, target.port, ok) upsertQuickDevice(context, quickDevices, target.host, target.port, ok)

View File

@@ -4,6 +4,10 @@ import android.app.Activity
import android.content.Intent import android.content.Intent
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.net.Uri import android.net.Uri
import android.os.SystemClock
import android.view.WindowManager
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.activity.compose.PredictiveBackHandler import androidx.activity.compose.PredictiveBackHandler
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
@@ -23,6 +27,7 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -51,7 +56,9 @@ import io.github.miuzarte.scrcpyforandroid.services.loadMainSettings
import io.github.miuzarte.scrcpyforandroid.services.saveMainSettings import io.github.miuzarte.scrcpyforandroid.services.saveMainSettings
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import top.yukonga.miuix.kmp.basic.DropdownImpl import top.yukonga.miuix.kmp.basic.DropdownImpl
import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.IconButton import top.yukonga.miuix.kmp.basic.IconButton
@@ -189,6 +196,7 @@ fun MainPage() {
var openReorderDevicesAction by remember { mutableStateOf<(() -> Unit)?>(null) } var openReorderDevicesAction by remember { mutableStateOf<(() -> Unit)?>(null) }
var canClearLogs by remember { mutableStateOf(false) } var canClearLogs by remember { mutableStateOf(false) }
var showDeviceMenu by rememberSaveable { mutableStateOf(false) } var showDeviceMenu by rememberSaveable { mutableStateOf(false) }
var lastExitBackPressAtMs by rememberSaveable { mutableLongStateOf(0L) }
var fullscreenOrientation by rememberSaveable { var fullscreenOrientation by rememberSaveable {
mutableIntStateOf(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) mutableIntStateOf(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
} }
@@ -201,6 +209,19 @@ fun MainPage() {
} }
} }
DisposableEffect(activity, keepScreenOnWhenStreamingEnabled, sessionStarted) {
val window = activity?.window
val shouldKeepScreenOn = keepScreenOnWhenStreamingEnabled && sessionStarted
if (window != null && shouldKeepScreenOn) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
onDispose {
if (window != null && shouldKeepScreenOn) {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
}
}
LaunchedEffect(activity, currentRootScreen, fullscreenOrientation) { LaunchedEffect(activity, currentRootScreen, fullscreenOrientation) {
val targetOrientation = when (currentRootScreen) { val targetOrientation = when (currentRootScreen) {
is RootScreen.Fullscreen -> fullscreenOrientation is RootScreen.Fullscreen -> fullscreenOrientation
@@ -275,13 +296,34 @@ fun MainPage() {
), ),
) )
} }
} else {
val now = SystemClock.elapsedRealtime()
if (now - lastExitBackPressAtMs > 2_000L) {
lastExitBackPressAtMs = now
Toast.makeText(context, "再按一次返回键退出", Toast.LENGTH_SHORT).show()
return
}
lastExitBackPressAtMs = 0L
scope.launch {
withContext(Dispatchers.IO) {
runCatching { nativeCore.scrcpyStop() }
runCatching { nativeCore.adbDisconnect() }
}
activity?.finish()
}
} }
} }
val canNavigateBack = rootBackStack.size > 1 || val canNavigateBack = rootBackStack.size > 1 ||
pagerState.currentPage != MainTabDestination.Device.ordinal pagerState.currentPage != MainTabDestination.Device.ordinal
PredictiveBackHandler(enabled = canNavigateBack) { progress -> BackHandler(enabled = currentRootScreen !is RootScreen.Fullscreen) {
handleBackNavigation()
}
PredictiveBackHandler(
enabled = canNavigateBack && currentRootScreen !is RootScreen.Fullscreen
) { progress ->
try { try {
progress.collect { } progress.collect { }
handleBackNavigation() handleBackNavigation()
@@ -380,7 +422,6 @@ fun MainPage() {
nativeCore = nativeCore, nativeCore = nativeCore,
snack = snackHostState, snack = snackHostState,
scrollBehavior = deviceScrollBehavior, scrollBehavior = deviceScrollBehavior,
keepScreenOnWhenStreamingEnabled = keepScreenOnWhenStreamingEnabled,
virtualButtonsOutside = virtualButtonsOutside, virtualButtonsOutside = virtualButtonsOutside,
virtualButtonsInMore = virtualButtonsInMore, virtualButtonsInMore = virtualButtonsInMore,
customServerUri = customServerUri, customServerUri = customServerUri,