feat: adb connect following with scrcpy auto start
This commit is contained in:
@@ -11,9 +11,9 @@ class Preset<T : Comparable<T>>(val values: List<T>) {
|
|||||||
val lastIndex: Int get() = values.lastIndex
|
val lastIndex: Int get() = values.lastIndex
|
||||||
val size: Int get() = values.size
|
val size: Int get() = values.size
|
||||||
val indices: IntRange get() = values.indices
|
val indices: IntRange get() = values.indices
|
||||||
|
|
||||||
operator fun get(index: Int): T = values[index]
|
operator fun get(index: Int): T = values[index]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the index of the exact value or the nearest preset.
|
* Find the index of the exact value or the nearest preset.
|
||||||
* For numeric types, finds the closest value by absolute difference.
|
* For numeric types, finds the closest value by absolute difference.
|
||||||
@@ -21,13 +21,14 @@ class Preset<T : Comparable<T>>(val values: List<T>) {
|
|||||||
fun indexOfOrNearest(value: T): Int {
|
fun indexOfOrNearest(value: T): Int {
|
||||||
val exact = values.indexOf(value)
|
val exact = values.indexOf(value)
|
||||||
if (exact >= 0) return exact
|
if (exact >= 0) return exact
|
||||||
|
|
||||||
// For numeric types, find nearest by comparing
|
// For numeric types, find nearest by comparing
|
||||||
return values.withIndex().minByOrNull { (_, preset) ->
|
return values.withIndex().minByOrNull { (_, preset) ->
|
||||||
when {
|
when {
|
||||||
preset is Number && value is Number -> {
|
preset is Number && value is Number -> {
|
||||||
kotlin.math.abs(preset.toDouble() - value.toDouble())
|
kotlin.math.abs(preset.toDouble() - value.toDouble())
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> if (preset > value) 1.0 else -1.0
|
else -> if (preset > value) 1.0 else -1.0
|
||||||
}
|
}
|
||||||
}?.index ?: 0
|
}?.index ?: 0
|
||||||
@@ -40,8 +41,8 @@ class Preset<T : Comparable<T>>(val values: List<T>) {
|
|||||||
fun Preset<Int>.indexOfOrNearest(raw: Int): Int {
|
fun Preset<Int>.indexOfOrNearest(raw: Int): Int {
|
||||||
val exact = values.indexOf(raw)
|
val exact = values.indexOf(raw)
|
||||||
if (exact >= 0) return exact
|
if (exact >= 0) return exact
|
||||||
val nearest = values.withIndex().minByOrNull { (_, preset) ->
|
val nearest = values.withIndex().minByOrNull { (_, preset) ->
|
||||||
kotlin.math.abs(preset - raw)
|
kotlin.math.abs(preset - raw)
|
||||||
}
|
}
|
||||||
return nearest?.index ?: 0
|
return nearest?.index ?: 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ class DeviceShortcuts(val devices: List<DeviceShortcut>) : List<DeviceShortcut>
|
|||||||
host: String? = null,
|
host: String? = null,
|
||||||
port: Int? = null,
|
port: Int? = null,
|
||||||
name: String? = null,
|
name: String? = null,
|
||||||
|
startScrcpyOnConnect: Boolean? = null,
|
||||||
newPort: Int? = null,
|
newPort: Int? = null,
|
||||||
updateNameOnlyWhenEmpty: Boolean = false,
|
updateNameOnlyWhenEmpty: Boolean = false,
|
||||||
): DeviceShortcuts {
|
): DeviceShortcuts {
|
||||||
@@ -57,9 +58,15 @@ class DeviceShortcuts(val devices: List<DeviceShortcut>) : List<DeviceShortcut>
|
|||||||
}
|
}
|
||||||
val finalHost = if (updateById) host ?: old.host else old.host
|
val finalHost = if (updateById) host ?: old.host else old.host
|
||||||
val finalPort = if (updateById) port ?: old.port else newPort ?: old.port
|
val finalPort = if (updateById) port ?: old.port else newPort ?: old.port
|
||||||
|
val finalStartScrcpyOnConnect = startScrcpyOnConnect ?: old.startScrcpyOnConnect
|
||||||
|
|
||||||
// 若无任何变化,返回原实例
|
// 若无任何变化,返回原实例
|
||||||
if (finalName == old.name && finalHost == old.host && finalPort == old.port)
|
if (
|
||||||
|
finalName == old.name
|
||||||
|
&& finalHost == old.host
|
||||||
|
&& finalPort == old.port
|
||||||
|
&& finalStartScrcpyOnConnect == old.startScrcpyOnConnect
|
||||||
|
)
|
||||||
return this
|
return this
|
||||||
|
|
||||||
val newList = devices.toMutableList().apply {
|
val newList = devices.toMutableList().apply {
|
||||||
@@ -67,6 +74,7 @@ class DeviceShortcuts(val devices: List<DeviceShortcut>) : List<DeviceShortcut>
|
|||||||
name = finalName,
|
name = finalName,
|
||||||
host = finalHost,
|
host = finalHost,
|
||||||
port = finalPort,
|
port = finalPort,
|
||||||
|
startScrcpyOnConnect = finalStartScrcpyOnConnect,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return DeviceShortcuts(
|
return DeviceShortcuts(
|
||||||
@@ -119,13 +127,14 @@ data class DeviceShortcut(
|
|||||||
val name: String = "",
|
val name: String = "",
|
||||||
val host: String,
|
val host: String,
|
||||||
val port: Int = Defaults.ADB_PORT,
|
val port: Int = Defaults.ADB_PORT,
|
||||||
|
val startScrcpyOnConnect: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val id: String get() = "$host:$port"
|
val id: String get() = "$host:$port"
|
||||||
|
|
||||||
fun marshalToString(
|
fun marshalToString(
|
||||||
separator: String = DEFAULT_SEPARATOR,
|
separator: String = DEFAULT_SEPARATOR,
|
||||||
): String = listOf(
|
): String = listOf(
|
||||||
name.trim(), host.trim(), port.toString()
|
name.trim(), host.trim(), port.toString(), if (startScrcpyOnConnect) "1" else "0"
|
||||||
).joinToString(
|
).joinToString(
|
||||||
separator = separator
|
separator = separator
|
||||||
)
|
)
|
||||||
@@ -136,16 +145,18 @@ data class DeviceShortcut(
|
|||||||
s: String,
|
s: String,
|
||||||
separator: String = DEFAULT_SEPARATOR,
|
separator: String = DEFAULT_SEPARATOR,
|
||||||
): DeviceShortcut? {
|
): DeviceShortcut? {
|
||||||
val parts = s.split(separator, limit = 3)
|
val parts = s.split(separator)
|
||||||
return when (parts.size) {
|
return when (parts.size) {
|
||||||
3 -> {
|
3, 4 -> {
|
||||||
val name = parts[0].trim()
|
val name = parts[0].trim()
|
||||||
val host = parts[1].trim()
|
val host = parts[1].trim()
|
||||||
val port = parts[2].trim().toIntOrNull() ?: Defaults.ADB_PORT
|
val port = parts[2].trim().toIntOrNull() ?: Defaults.ADB_PORT
|
||||||
|
val startScrcpyOnConnect = parts.getOrNull(3)?.trim() == "1"
|
||||||
if (host.isNotBlank()) DeviceShortcut(
|
if (host.isNotBlank()) DeviceShortcut(
|
||||||
name = name,
|
name = name,
|
||||||
host = host,
|
host = host,
|
||||||
port = port,
|
port = port,
|
||||||
|
startScrcpyOnConnect = startScrcpyOnConnect,
|
||||||
)
|
)
|
||||||
else null
|
else null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,7 +87,10 @@ class PersistentVideoRenderer {
|
|||||||
|
|
||||||
fun detachDisplaySurface(surface: Surface? = null, releaseSurface: Boolean = false) {
|
fun detachDisplaySurface(surface: Surface? = null, releaseSurface: Boolean = false) {
|
||||||
val requestId = surface?.let { System.identityHashCode(it) }
|
val requestId = surface?.let { System.identityHashCode(it) }
|
||||||
Log.i(tag, "detachDisplaySurface(): request surfaceId=$requestId releaseSurface=$releaseSurface current=${displaySurfaceId}")
|
Log.i(
|
||||||
|
tag,
|
||||||
|
"detachDisplaySurface(): request surfaceId=$requestId releaseSurface=$releaseSurface current=${displaySurfaceId}"
|
||||||
|
)
|
||||||
handler.post {
|
handler.post {
|
||||||
if (released) return@post
|
if (released) return@post
|
||||||
if (requestId != null && requestId != displaySurfaceId) return@post
|
if (requestId != null && requestId != displaySurfaceId) return@post
|
||||||
@@ -190,7 +193,10 @@ class PersistentVideoRenderer {
|
|||||||
setOnFrameAvailableListener({
|
setOnFrameAvailableListener({
|
||||||
val n = frameAvailableCount.incrementAndGet()
|
val n = frameAvailableCount.incrementAndGet()
|
||||||
if (n == 1L || n % 120L == 0L) {
|
if (n == 1L || n % 120L == 0L) {
|
||||||
Log.i(tag, "onFrameAvailable(): available=$n consumed=${frameConsumedCount.get()} rendered=${frameRenderedCount.get()} display=${displaySurfaceId != null}")
|
Log.i(
|
||||||
|
tag,
|
||||||
|
"onFrameAvailable(): available=$n consumed=${frameConsumedCount.get()} rendered=${frameRenderedCount.get()} display=${displaySurfaceId != null}"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
drawFrame()
|
drawFrame()
|
||||||
}, handler)
|
}, handler)
|
||||||
@@ -222,7 +228,10 @@ class PersistentVideoRenderer {
|
|||||||
.onSuccess {
|
.onSuccess {
|
||||||
val consumed = frameConsumedCount.incrementAndGet()
|
val consumed = frameConsumedCount.incrementAndGet()
|
||||||
if (consumed == 1L || consumed % 120L == 0L) {
|
if (consumed == 1L || consumed % 120L == 0L) {
|
||||||
Log.i(tag, "drawFrame(): consumed=$consumed available=${frameAvailableCount.get()} rendered=${frameRenderedCount.get()} display=${displaySurfaceId != null}")
|
Log.i(
|
||||||
|
tag,
|
||||||
|
"drawFrame(): consumed=$consumed available=${frameAvailableCount.get()} rendered=${frameRenderedCount.get()} display=${displaySurfaceId != null}"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onFailure { Log.w(tag, "updateTexImage failed", it) }
|
.onFailure { Log.w(tag, "updateTexImage failed", it) }
|
||||||
@@ -258,7 +267,10 @@ class PersistentVideoRenderer {
|
|||||||
EGL14.eglSwapBuffers(eglDisplay, displayEglSurface)
|
EGL14.eglSwapBuffers(eglDisplay, displayEglSurface)
|
||||||
val rendered = frameRenderedCount.incrementAndGet()
|
val rendered = frameRenderedCount.incrementAndGet()
|
||||||
if (rendered == 1L || rendered % 120L == 0L) {
|
if (rendered == 1L || rendered % 120L == 0L) {
|
||||||
Log.i(tag, "drawFrame(): rendered=$rendered consumed=${frameConsumedCount.get()} available=${frameAvailableCount.get()} viewport=${width[0]}x${height[0]}")
|
Log.i(
|
||||||
|
tag,
|
||||||
|
"drawFrame(): rendered=$rendered consumed=${frameConsumedCount.get()} available=${frameAvailableCount.get()} viewport=${width[0]}x${height[0]}"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -503,6 +503,63 @@ fun DeviceTabPage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(adbConnected, currentTargetHost, currentTargetPort) {
|
||||||
|
if (!adbConnected || currentTargetHost.isBlank()) return@LaunchedEffect
|
||||||
|
|
||||||
|
// Keep-alive loop for current target.
|
||||||
|
// On failure: try to reconnect once; if failed, fully disconnect and reset UI state.
|
||||||
|
val host = currentTargetHost
|
||||||
|
val port = currentTargetPort
|
||||||
|
while (adbConnected && currentTargetHost == host && currentTargetPort == port) {
|
||||||
|
delay(ADB_KEEPALIVE_INTERVAL_MS)
|
||||||
|
val alive = runCatching { keepAliveCheck(host, port) }.getOrElse { false }
|
||||||
|
if (alive) continue
|
||||||
|
|
||||||
|
logEvent("ADB 长连接中断,尝试自动重连: $host:$port", Log.WARN)
|
||||||
|
try {
|
||||||
|
connectWithTimeout(host, port)
|
||||||
|
adbConnected = true
|
||||||
|
statusLine = "$host:$port"
|
||||||
|
logEvent("ADB 自动重连成功: $host:$port")
|
||||||
|
snackbar.show("ADB 自动重连成功")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
disconnectAdbConnection()
|
||||||
|
statusLine = "ADB 连接断开"
|
||||||
|
logEvent("ADB 自动重连失败: $e", Log.ERROR)
|
||||||
|
snackbar.show("ADB 自动重连失败")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun startScrcpySession() {
|
||||||
|
val options = scrcpyOptions.toClientOptions(soBundleShared).fix()
|
||||||
|
val session = scrcpy.start(options)
|
||||||
|
if (options.disableScreensaver) {
|
||||||
|
setKeepScreenOn(true)
|
||||||
|
}
|
||||||
|
statusLine = "scrcpy 运行中"
|
||||||
|
@SuppressLint("DefaultLocale")
|
||||||
|
val videoDetail =
|
||||||
|
if (!options.video) "off"
|
||||||
|
else if (soBundleShared.videoBitRate <= 0) "${session.codec?.string ?: "null"} ${session.width}x${session.height} @default"
|
||||||
|
else "${session.codec?.string ?: "null"} ${session.width}x${session.height} " +
|
||||||
|
"@${String.format("%.1f", soBundleShared.videoBitRate / 1_000_000f)}Mbps"
|
||||||
|
|
||||||
|
val audioDetail =
|
||||||
|
if (!soBundleShared.audio) "off"
|
||||||
|
else if (soBundleShared.audioBitRate <= 0) "${options.audioCodec} default source=${options.audioSource}"
|
||||||
|
else "${options.audioCodec} ${soBundleShared.audioBitRate / 1_000f}Kbps source=${options.audioSource}${if (!options.audioPlayback) "(no-playback)" else ""}"
|
||||||
|
|
||||||
|
logEvent(
|
||||||
|
"scrcpy 已启动: device=${session.deviceName}" +
|
||||||
|
", video=$videoDetail, audio=$audioDetail" +
|
||||||
|
", control=${options.control}, turnScreenOff=${options.turnScreenOff}" +
|
||||||
|
", maxSize=${options.maxSize}, maxFps=${options.maxFps}"
|
||||||
|
)
|
||||||
|
snackbar.show("scrcpy 已启动")
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun handleAdbConnected(host: String, port: Int) {
|
suspend fun handleAdbConnected(host: String, port: Int) {
|
||||||
currentTargetHost = host
|
currentTargetHost = host
|
||||||
currentTargetPort = port
|
currentTargetPort = port
|
||||||
@@ -534,48 +591,28 @@ fun DeviceTabPage(
|
|||||||
"sdk=${info.sdkInt}"
|
"sdk=${info.sdkInt}"
|
||||||
)
|
)
|
||||||
snackbar.show("ADB 已连接")
|
snackbar.show("ADB 已连接")
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(adbConnected, currentTargetHost, currentTargetPort) {
|
if (
|
||||||
if (!adbConnected || currentTargetHost.isBlank()) return@LaunchedEffect
|
savedShortcuts.get(host, port)?.startScrcpyOnConnect == true &&
|
||||||
|
sessionInfo == null
|
||||||
// Keep-alive loop for current target.
|
) {
|
||||||
// On failure: try to reconnect once; if failed, fully disconnect and reset UI state.
|
runBusy("启动 scrcpy") {
|
||||||
val host = currentTargetHost
|
startScrcpySession()
|
||||||
val port = currentTargetPort
|
|
||||||
while (adbConnected && currentTargetHost == host && currentTargetPort == port) {
|
|
||||||
delay(ADB_KEEPALIVE_INTERVAL_MS)
|
|
||||||
val alive = runCatching { keepAliveCheck(host, port) }.getOrElse { false }
|
|
||||||
if (alive) continue
|
|
||||||
|
|
||||||
logEvent("ADB 长连接中断,尝试自动重连: $host:$port", Log.WARN)
|
|
||||||
try {
|
|
||||||
connectWithTimeout(host, port)
|
|
||||||
adbConnected = true
|
|
||||||
statusLine = "$host:$port"
|
|
||||||
logEvent("ADB 自动重连成功: $host:$port")
|
|
||||||
snackbar.show("ADB 自动重连成功")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
disconnectAdbConnection()
|
|
||||||
statusLine = "ADB 连接断开"
|
|
||||||
logEvent("ADB 自动重连失败: $e", Log.ERROR)
|
|
||||||
snackbar.show("ADB 自动重连失败")
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
LaunchedEffect(
|
||||||
val adbPairingAutoDiscoverOnDialogOpen = asBundle.adbPairingAutoDiscoverOnDialogOpen
|
adbConnected,
|
||||||
val adbAutoReconnectPairedDevice = asBundle.adbAutoReconnectPairedDevice
|
asBundle.adbAutoReconnectPairedDevice,
|
||||||
val adbMdnsLanDiscovery = asBundle.adbMdnsLanDiscovery
|
asBundle.adbMdnsLanDiscovery
|
||||||
LaunchedEffect(adbConnected, adbAutoReconnectPairedDevice, adbMdnsLanDiscovery) {
|
) {
|
||||||
if (adbConnected || !adbAutoReconnectPairedDevice) return@LaunchedEffect
|
if (adbConnected || !asBundle.adbAutoReconnectPairedDevice) return@LaunchedEffect
|
||||||
|
|
||||||
// Background auto reconnect pipeline:
|
// Background auto reconnect pipeline:
|
||||||
// 1) try quick list targets with reachable TCP ports
|
// 1) try quick list targets with reachable TCP ports
|
||||||
// 2) fallback to mDNS discovery
|
// 2) fallback to mDNS discovery
|
||||||
val quickConnectTriedOnce = mutableSetOf<String>()
|
val quickConnectTriedOnce = mutableSetOf<String>()
|
||||||
while (!adbConnected && adbAutoReconnectPairedDevice) {
|
while (!adbConnected) {
|
||||||
if (busy || adbConnecting || sessionInfo != null) {
|
if (busy || adbConnecting || sessionInfo != null) {
|
||||||
delay(ADB_AUTO_RECONNECT_RETRY_INTERVAL_MS)
|
delay(ADB_AUTO_RECONNECT_RETRY_INTERVAL_MS)
|
||||||
continue
|
continue
|
||||||
@@ -611,7 +648,7 @@ fun DeviceTabPage(
|
|||||||
val discovered = withContext(Dispatchers.IO) {
|
val discovered = withContext(Dispatchers.IO) {
|
||||||
adbService.discoverConnectService(
|
adbService.discoverConnectService(
|
||||||
timeoutMs = ADB_AUTO_RECONNECT_DISCOVER_TIMEOUT_MS,
|
timeoutMs = ADB_AUTO_RECONNECT_DISCOVER_TIMEOUT_MS,
|
||||||
includeLanDevices = adbMdnsLanDiscovery,
|
includeLanDevices = asBundle.adbMdnsLanDiscovery,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -673,12 +710,6 @@ fun DeviceTabPage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val devicePreviewCardHeightDp = asBundle.devicePreviewCardHeightDp
|
|
||||||
val previewVirtualButtonShowText = asBundle.previewVirtualButtonShowText
|
|
||||||
|
|
||||||
val audioBitRate = soBundleShared.audioBitRate
|
|
||||||
val videoBitRate = soBundleShared.videoBitRate
|
|
||||||
|
|
||||||
// 设备
|
// 设备
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
@@ -708,7 +739,9 @@ fun DeviceTabPage(
|
|||||||
adbConnecting && activeDeviceActionId == device.id
|
adbConnecting && activeDeviceActionId == device.id
|
||||||
},
|
},
|
||||||
editingDeviceId = editingDeviceId,
|
editingDeviceId = editingDeviceId,
|
||||||
onClick = {},
|
onClick = {
|
||||||
|
snackbar.show("长按可编辑")
|
||||||
|
},
|
||||||
onLongClick = { device ->
|
onLongClick = { device ->
|
||||||
val connected = adbConnected
|
val connected = adbConnected
|
||||||
&& currentTarget?.host == device.host
|
&& currentTarget?.host == device.host
|
||||||
@@ -716,7 +749,9 @@ fun DeviceTabPage(
|
|||||||
if (connected) {
|
if (connected) {
|
||||||
snackbar.show("无法修改已连接的设备")
|
snackbar.show("无法修改已连接的设备")
|
||||||
} else {
|
} else {
|
||||||
editingDeviceId = device.id
|
editingDeviceId =
|
||||||
|
if (editingDeviceId != device.id) device.id
|
||||||
|
else null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onAction = { device ->
|
onAction = { device ->
|
||||||
@@ -769,8 +804,8 @@ fun DeviceTabPage(
|
|||||||
name = updated.name,
|
name = updated.name,
|
||||||
host = updated.host,
|
host = updated.host,
|
||||||
port = updated.port,
|
port = updated.port,
|
||||||
|
startScrcpyOnConnect = updated.startScrcpyOnConnect,
|
||||||
)
|
)
|
||||||
editingDeviceId = null
|
|
||||||
},
|
},
|
||||||
onEditorDelete = { device ->
|
onEditorDelete = { device ->
|
||||||
savedShortcuts = savedShortcuts.remove(id = device.id)
|
savedShortcuts = savedShortcuts.remove(id = device.id)
|
||||||
@@ -833,10 +868,10 @@ fun DeviceTabPage(
|
|||||||
// "使用配对码配对设备"
|
// "使用配对码配对设备"
|
||||||
PairingCard(
|
PairingCard(
|
||||||
busy = busy,
|
busy = busy,
|
||||||
autoDiscoverOnDialogOpen = adbPairingAutoDiscoverOnDialogOpen,
|
autoDiscoverOnDialogOpen = asBundle.adbPairingAutoDiscoverOnDialogOpen,
|
||||||
onDiscoverTarget = {
|
onDiscoverTarget = {
|
||||||
adbService.discoverPairingService(
|
adbService.discoverPairingService(
|
||||||
includeLanDevices = adbMdnsLanDiscovery,
|
includeLanDevices = asBundle.adbMdnsLanDiscovery,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onPair = { host, port, code ->
|
onPair = { host, port, code ->
|
||||||
@@ -873,31 +908,7 @@ fun DeviceTabPage(
|
|||||||
onStartStopHaptic = { haptics.contextClick() },
|
onStartStopHaptic = { haptics.contextClick() },
|
||||||
onStart = {
|
onStart = {
|
||||||
runBusy("启动 scrcpy") {
|
runBusy("启动 scrcpy") {
|
||||||
val options = scrcpyOptions.toClientOptions(soBundleShared).fix()
|
startScrcpySession()
|
||||||
val session = scrcpy.start(options)
|
|
||||||
if (options.disableScreensaver) {
|
|
||||||
setKeepScreenOn(true)
|
|
||||||
}
|
|
||||||
statusLine = "scrcpy 运行中"
|
|
||||||
@SuppressLint("DefaultLocale")
|
|
||||||
val videoDetail =
|
|
||||||
if (!options.video) "off"
|
|
||||||
else if (videoBitRate <= 0) "${session.codec?.string ?: "null"} ${session.width}x${session.height} @default"
|
|
||||||
else "${session.codec?.string ?: "null"} ${session.width}x${session.height} " +
|
|
||||||
"@${String.format("%.1f", videoBitRate / 1_000_000f)}Mbps"
|
|
||||||
|
|
||||||
val audioDetail =
|
|
||||||
if (!soBundleShared.audio) "off"
|
|
||||||
else if (audioBitRate <= 0) "${options.audioCodec} default source=${options.audioSource}"
|
|
||||||
else "${options.audioCodec} ${audioBitRate / 1_000f}Kbps source=${options.audioSource}${if (!options.audioPlayback) "(no-playback)" else ""}"
|
|
||||||
|
|
||||||
logEvent(
|
|
||||||
"scrcpy 已启动: device=${session.deviceName}" +
|
|
||||||
", video=$videoDetail, audio=$audioDetail" +
|
|
||||||
", control=${options.control}, turnScreenOff=${options.turnScreenOff}" +
|
|
||||||
", maxSize=${options.maxSize}, maxFps=${options.maxFps}"
|
|
||||||
)
|
|
||||||
snackbar.show("scrcpy 已启动")
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onStop = {
|
onStop = {
|
||||||
@@ -938,7 +949,7 @@ fun DeviceTabPage(
|
|||||||
PreviewCard(
|
PreviewCard(
|
||||||
sessionInfo = sessionInfo,
|
sessionInfo = sessionInfo,
|
||||||
nativeCore = nativeCore,
|
nativeCore = nativeCore,
|
||||||
previewHeightDp = devicePreviewCardHeightDp.coerceAtLeast(120),
|
previewHeightDp = asBundle.devicePreviewCardHeightDp.coerceAtLeast(120),
|
||||||
controlsVisible = previewControlsVisible,
|
controlsVisible = previewControlsVisible,
|
||||||
onTapped = {
|
onTapped = {
|
||||||
previewControlsVisible = !previewControlsVisible
|
previewControlsVisible = !previewControlsVisible
|
||||||
@@ -964,7 +975,7 @@ fun DeviceTabPage(
|
|||||||
busy = busy,
|
busy = busy,
|
||||||
outsideActions = virtualButtonLayout.first,
|
outsideActions = virtualButtonLayout.first,
|
||||||
moreActions = virtualButtonLayout.second,
|
moreActions = virtualButtonLayout.second,
|
||||||
showText = previewVirtualButtonShowText,
|
showText = asBundle.previewVirtualButtonShowText,
|
||||||
onAction = ::sendVirtualButtonAction,
|
onAction = ::sendVirtualButtonAction,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package io.github.miuzarte.scrcpyforandroid.pages
|
package io.github.miuzarte.scrcpyforandroid.pages
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.util.Log
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
@@ -144,7 +145,11 @@ fun FullscreenControlScreen(
|
|||||||
nativeCore.session?.injectKeycode(1, keycode)
|
nativeCore.session?.injectKeycode(1, keycode)
|
||||||
}
|
}
|
||||||
}.onFailure { e ->
|
}.onFailure { e ->
|
||||||
android.util.Log.w("FullscreenControlPage", "sendKeycode failed for keycode=$keycode", e)
|
Log.w(
|
||||||
|
"FullscreenControlPage",
|
||||||
|
"sendKeycode failed for keycode=$keycode",
|
||||||
|
e
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package io.github.miuzarte.scrcpyforandroid.pages
|
package io.github.miuzarte.scrcpyforandroid.pages
|
||||||
|
|
||||||
import android.content.pm.ActivityInfo
|
|
||||||
import androidx.compose.runtime.staticCompositionLocalOf
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
|
|
||||||
class RootNavigator(
|
class RootNavigator(
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ data class ClientOptions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun fix(): ClientOptions {
|
fun fix(): ClientOptions {
|
||||||
when(videoSource) {
|
when (videoSource) {
|
||||||
VideoSource.DISPLAY -> {
|
VideoSource.DISPLAY -> {
|
||||||
cameraId = ""
|
cameraId = ""
|
||||||
cameraFacing = CameraFacing.ANY
|
cameraFacing = CameraFacing.ANY
|
||||||
@@ -238,6 +238,7 @@ data class ClientOptions(
|
|||||||
cameraFps = 0u
|
cameraFps = 0u
|
||||||
cameraHighSpeed = false
|
cameraHighSpeed = false
|
||||||
}
|
}
|
||||||
|
|
||||||
VideoSource.CAMERA -> {
|
VideoSource.CAMERA -> {
|
||||||
displayId = 0
|
displayId = 0
|
||||||
maxSize = 0u
|
maxSize = 0u
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package io.github.miuzarte.scrcpyforandroid.services
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||||
import io.github.miuzarte.scrcpyforandroid.constants.Defaults
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
|
|||||||
@@ -97,6 +97,8 @@ import top.yukonga.miuix.kmp.basic.TextButton
|
|||||||
import top.yukonga.miuix.kmp.basic.TextField
|
import top.yukonga.miuix.kmp.basic.TextField
|
||||||
import top.yukonga.miuix.kmp.overlay.OverlayDialog
|
import top.yukonga.miuix.kmp.overlay.OverlayDialog
|
||||||
import top.yukonga.miuix.kmp.preference.ArrowPreference
|
import top.yukonga.miuix.kmp.preference.ArrowPreference
|
||||||
|
import top.yukonga.miuix.kmp.preference.CheckboxLocation
|
||||||
|
import top.yukonga.miuix.kmp.preference.CheckboxPreference
|
||||||
import top.yukonga.miuix.kmp.preference.OverlayDropdownPreference
|
import top.yukonga.miuix.kmp.preference.OverlayDropdownPreference
|
||||||
import top.yukonga.miuix.kmp.preference.SwitchPreference
|
import top.yukonga.miuix.kmp.preference.SwitchPreference
|
||||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||||
@@ -435,23 +437,23 @@ internal fun ConfigPanel(
|
|||||||
enabled = !sessionStarted && soBundle.audio,
|
enabled = !sessionStarted && soBundle.audio,
|
||||||
)
|
)
|
||||||
AnimatedVisibility(audioBitRateVisibility) {
|
AnimatedVisibility(audioBitRateVisibility) {
|
||||||
SuperSlider(
|
SuperSlider(
|
||||||
title = "音频码率",
|
title = "音频码率",
|
||||||
summary = "--audio-bit-rate",
|
summary = "--audio-bit-rate",
|
||||||
value = ScrcpyPresets.AudioBitRate
|
value = ScrcpyPresets.AudioBitRate
|
||||||
.indexOfOrNearest(soBundle.audioBitRate / 1000)
|
.indexOfOrNearest(soBundle.audioBitRate / 1000)
|
||||||
.toFloat(),
|
.toFloat(),
|
||||||
onValueChange = { value ->
|
onValueChange = { value ->
|
||||||
val idx = value.roundToInt()
|
val idx = value.roundToInt()
|
||||||
.coerceIn(0, ScrcpyPresets.AudioBitRate.lastIndex)
|
.coerceIn(0, ScrcpyPresets.AudioBitRate.lastIndex)
|
||||||
soBundle = soBundle.copy(
|
soBundle = soBundle.copy(
|
||||||
audioBitRate = ScrcpyPresets.AudioBitRate[idx] * 1000
|
audioBitRate = ScrcpyPresets.AudioBitRate[idx] * 1000
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
valueRange = 0f..ScrcpyPresets.AudioBitRate.lastIndex.toFloat(),
|
valueRange = 0f..ScrcpyPresets.AudioBitRate.lastIndex.toFloat(),
|
||||||
steps = (ScrcpyPresets.AudioBitRate.size - 2).coerceAtLeast(0),
|
steps = (ScrcpyPresets.AudioBitRate.size - 2).coerceAtLeast(0),
|
||||||
unit = "Kbps",
|
unit = "Kbps",
|
||||||
zeroStateText = "默认",
|
zeroStateText = "默认",
|
||||||
displayText = (soBundle.audioBitRate / 1_000).toString(),
|
displayText = (soBundle.audioBitRate / 1_000).toString(),
|
||||||
inputInitialValue =
|
inputInitialValue =
|
||||||
if (soBundle.audioBitRate <= 0) ""
|
if (soBundle.audioBitRate <= 0) ""
|
||||||
@@ -963,15 +965,37 @@ internal fun DeviceTile(
|
|||||||
onEditorCancel: () -> Unit,
|
onEditorCancel: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val haptics = rememberAppHaptics()
|
val haptics = rememberAppHaptics()
|
||||||
var name by rememberSaveable(device.id) { mutableStateOf(device.name) }
|
|
||||||
var host by rememberSaveable(device.id) { mutableStateOf(device.host) }
|
|
||||||
var port by rememberSaveable(device.id) { mutableStateOf(device.port.toString()) }
|
|
||||||
|
|
||||||
LaunchedEffect(device) {
|
var draft by remember(editing, device.id) {
|
||||||
name = device.name
|
mutableStateOf(if (editing) device else null)
|
||||||
host = device.host
|
|
||||||
port = device.port.toString()
|
|
||||||
}
|
}
|
||||||
|
var originalDraft by remember(editing, device.id) {
|
||||||
|
mutableStateOf(if (editing) device else null)
|
||||||
|
}
|
||||||
|
var draftPortText by remember(editing, device.id) {
|
||||||
|
mutableStateOf(if (editing) device.port.toString() else null)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(editing, draft) {
|
||||||
|
val currentDraft = draft ?: return@LaunchedEffect
|
||||||
|
if (!editing) return@LaunchedEffect
|
||||||
|
delay(Settings.BUNDLE_SAVE_DELAY)
|
||||||
|
val trimmedHost = currentDraft.host.trim()
|
||||||
|
if (trimmedHost.isBlank()) return@LaunchedEffect
|
||||||
|
val updated = DeviceShortcut(
|
||||||
|
name = currentDraft.name.trim(),
|
||||||
|
host = trimmedHost,
|
||||||
|
port = currentDraft.port,
|
||||||
|
startScrcpyOnConnect = currentDraft.startScrcpyOnConnect,
|
||||||
|
)
|
||||||
|
if (updated != device) {
|
||||||
|
onEditorSave(updated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentDraft = draft ?: device
|
||||||
|
val currentOriginalDraft = originalDraft ?: device
|
||||||
|
val currentDraftPortText = draftPortText ?: device.port.toString()
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
colors = CardDefaults.defaultColors(
|
colors = CardDefaults.defaultColors(
|
||||||
@@ -1041,48 +1065,73 @@ internal fun DeviceTile(
|
|||||||
text = if (!isConnected) "连接" else "断开",
|
text = if (!isConnected) "连接" else "断开",
|
||||||
onClick = onAction,
|
onClick = onAction,
|
||||||
enabled = actionEnabled && !actionInProgress,
|
enabled = actionEnabled && !actionInProgress,
|
||||||
|
colors = if (!isConnected && device.startScrcpyOnConnect) {
|
||||||
|
ButtonDefaults.textButtonColorsPrimary()
|
||||||
|
} else {
|
||||||
|
ButtonDefaults.textButtonColors()
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AnimatedVisibility(editing) {
|
AnimatedVisibility(editing) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier.padding(vertical = UiSpacing.Large),
|
||||||
.fillMaxWidth()
|
verticalArrangement = Arrangement.spacedBy(UiSpacing.ContentVertical),
|
||||||
.padding(horizontal = UiSpacing.ContentVertical)
|
|
||||||
.padding(bottom = UiSpacing.ContentVertical),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(UiSpacing.ContentVertical)
|
|
||||||
) {
|
) {
|
||||||
SuperTextField(
|
Column(
|
||||||
value = name,
|
modifier = Modifier.padding(horizontal = UiSpacing.Large),
|
||||||
onValueChange = { name = it },
|
verticalArrangement = Arrangement.spacedBy(UiSpacing.ContentVertical),
|
||||||
label = "设备名称",
|
) {
|
||||||
singleLine = true,
|
SuperTextField(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
value = currentDraft.name,
|
||||||
)
|
onValueChange = { draft = currentDraft.copy(name = it) },
|
||||||
SuperTextField(
|
label = "设备名称",
|
||||||
value = host,
|
singleLine = true,
|
||||||
onValueChange = { host = it },
|
modifier = Modifier.fillMaxWidth(),
|
||||||
label = "IP 地址",
|
)
|
||||||
singleLine = true,
|
SuperTextField(
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
value = currentDraft.host,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
onValueChange = { draft = currentDraft.copy(host = it) },
|
||||||
)
|
label = "IP 地址",
|
||||||
SuperTextField(
|
singleLine = true,
|
||||||
value = port,
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
onValueChange = { port = it.filter(Char::isDigit) },
|
modifier = Modifier.fillMaxWidth(),
|
||||||
label = "端口",
|
)
|
||||||
singleLine = true,
|
SuperTextField(
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
value = currentDraftPortText,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
onValueChange = {
|
||||||
)
|
draftPortText = it.filter(Char::isDigit)
|
||||||
|
draft = currentDraft.copy(
|
||||||
|
port = draftPortText?.toIntOrNull() ?: Defaults.ADB_PORT
|
||||||
|
)
|
||||||
|
},
|
||||||
|
label = "端口",
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
CheckboxPreference(
|
||||||
|
title = "连接后立即启动 scrcpy",
|
||||||
|
checkboxLocation = CheckboxLocation.End,
|
||||||
|
checked = currentDraft.startScrcpyOnConnect,
|
||||||
|
onCheckedChange = {
|
||||||
|
draft = currentDraft.copy(startScrcpyOnConnect = it)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = UiSpacing.Large),
|
||||||
horizontalArrangement = Arrangement.spacedBy(UiSpacing.ContentVertical),
|
horizontalArrangement = Arrangement.spacedBy(UiSpacing.ContentVertical),
|
||||||
) {
|
) {
|
||||||
TextButton(
|
TextButton(
|
||||||
text = "取消",
|
text = "取消",
|
||||||
onClick = onEditorCancel,
|
onClick = {
|
||||||
|
onEditorSave(currentOriginalDraft)
|
||||||
|
onEditorCancel()
|
||||||
|
},
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
TextButton(
|
TextButton(
|
||||||
@@ -1091,19 +1140,8 @@ internal fun DeviceTile(
|
|||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
TextButton(
|
TextButton(
|
||||||
text = "保存",
|
text = "完成",
|
||||||
onClick = {
|
onClick = onEditorCancel,
|
||||||
val trimmedHost = host.trim()
|
|
||||||
if (trimmedHost.isNotBlank()) {
|
|
||||||
onEditorSave(
|
|
||||||
DeviceShortcut(
|
|
||||||
name = name.trim(),
|
|
||||||
host = trimmedHost,
|
|
||||||
port = port.toIntOrNull() ?: Defaults.ADB_PORT,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
colors = ButtonDefaults.textButtonColorsPrimary(),
|
colors = ButtonDefaults.textButtonColorsPrimary(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package io.github.miuzarte.scrcpyforandroid.widgets
|
package io.github.miuzarte.scrcpyforandroid.widgets
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.gestures.detectDragGestures
|
import androidx.compose.foundation.gestures.detectDragGestures
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
|||||||
Reference in New Issue
Block a user