diff --git a/.gitignore b/.gitignore index a77f58c..f966e01 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,5 @@ google-services.json *.hprof .kiro/ + +scrcpy.help* diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/ScrcpyPresets.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/ScrcpyPresets.kt index 8bb5da9..8738851 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/ScrcpyPresets.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/ScrcpyPresets.kt @@ -59,6 +59,7 @@ fun Preset.indexOfOrNearest(raw: String): Int { object ScrcpyPresets { val MaxSize = Preset(listOf(0, 720, 1080, 1280, 1600, 1920, 2160, 2560, 3200, 3840)) // px val MaxFPS = Preset(listOf(0, 24, 30, 45, 60, 90, 120)) // fps + val ScreenOffTimeout = Preset(listOf(0, 15, 30, 60, 120, 300, 600)) // sec val AudioBitRate = Preset(listOf(0, 32, 64, 96, 128, 160, 192, 256, 320, 384, 512)) // Kbps val CameraFps = Preset(listOf(0, 10, 15, 24, 30, 60, 120, 240, 480, 960)) // fps } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/NativeAdbService.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/NativeAdbService.kt index fce50c7..7e15833 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/NativeAdbService.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/NativeAdbService.kt @@ -133,6 +133,41 @@ object NativeAdbService { response } + suspend fun startApp( + packageName: String, + displayId: Int? = null, + forceStop: Boolean = false, + ): String = mutex.withLock { + val normalizedPackageName = packageName.trim() + require(normalizedPackageName.isNotBlank()) { "package name is blank" } + + if (forceStop) { + requireConnection().shell( + "am force-stop ${quoteShellArg(normalizedPackageName)}" + ) + } + + val resolveOutput = requireConnection().shell( + "cmd package resolve-activity --brief ${quoteShellArg(normalizedPackageName)}" + ) + val componentName = resolveOutput + .lineSequence() + .map(String::trim) + .lastOrNull { '/' in it } + ?: throw IllegalStateException( + "Cannot resolve launch activity for $normalizedPackageName" + ) + + val displayArg = displayId + ?.takeIf { it >= 0 } + ?.let { " --display $it" } + .orEmpty() + val command = "am start-activity$displayArg -n ${quoteShellArg(componentName)}" + val response = requireConnection().shell(command) + Log.d(TAG, "startApp(): package=$normalizedPackageName component=$componentName") + response + } + suspend fun openShellStream(command: String): AdbSocketStream = mutex.withLock { requireConnection().openStream("shell:$command") } @@ -161,5 +196,9 @@ object NativeAdbService { ?: throw IllegalStateException("ADB not connected") } + private fun quoteShellArg(value: String): String { + return "'" + value.replace("'", "'\\''") + "'" + } + private const val TAG = "NativeAdbService" } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DeviceTabScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DeviceTabScreen.kt index dcbb678..a74ac7a 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DeviceTabScreen.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DeviceTabScreen.kt @@ -36,6 +36,7 @@ import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcuts import io.github.miuzarte.scrcpyforandroid.nativecore.NativeAdbService import io.github.miuzarte.scrcpyforandroid.scaffolds.LazyColumn import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy +import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.VideoSource import io.github.miuzarte.scrcpyforandroid.services.AppWakeLocks import io.github.miuzarte.scrcpyforandroid.services.EventLogger import io.github.miuzarte.scrcpyforandroid.services.EventLogger.logEvent @@ -89,6 +90,13 @@ private const val ADB_TCP_PROBE_TIMEOUT_MS = 500 private const val PREVIEW_CARD_ITEM_KEY = "preview_card" private const val PREVIEW_CARD_ITEM_INDEX = 3 +private data class StartAppRequest( + val packageName: String, + val displayId: Int?, + val forceStop: Boolean, + val matchedAppLabel: String? = null, +) + @Composable fun DeviceTabScreen( scrollBehavior: ScrollBehavior, @@ -517,11 +525,102 @@ fun DeviceTabPage( } } + suspend fun resolveStartAppRequest( + scrcpy: Scrcpy, + options: io.github.miuzarte.scrcpyforandroid.scrcpy.ClientOptions, + ): StartAppRequest? { + val raw = options.startApp.trim() + if (raw.isBlank()) { + return null + } + + var query = raw + val forceStop = query.startsWith("+") + if (forceStop) { + query = query.drop(1).trimStart() + } + require(query.isNotBlank()) { "应用名或包名不能为空" } + + val displayId = when { + options.videoSource != VideoSource.DISPLAY -> null + options.displayId >= 0 -> options.displayId + else -> null + } + + if (!query.startsWith("?")) { + return StartAppRequest( + packageName = query, + displayId = displayId, + forceStop = forceStop, + ) + } + + val searchName = query.drop(1).trim() + require(searchName.isNotBlank()) { "应用名称不能为空" } + + val apps = scrcpy.listings.getApps(forceRefresh = false) + val matches = apps.filter { it.label.startsWith(searchName, ignoreCase = true) } + + require(matches.isNotEmpty()) { "未找到应用名以 \"$searchName\" 开头的应用" } + require(matches.size == 1) { + "按名称匹配到多个应用: " + + matches.take(5).joinToString { "${it.label} (${it.packageName})" } + } + + return StartAppRequest( + packageName = matches[0].packageName, + displayId = displayId, + forceStop = forceStop, + matchedAppLabel = matches[0].label, + ) + } + suspend fun startScrcpySession(openFullscreen: Boolean = false) { val options = scrcpyOptions.toClientOptions(soBundleShared).fix() val session = scrcpy.start(options) pendingScrollToPreview = true - if (openFullscreen) withContext(Dispatchers.Main) { + val startAppRequest = runCatching { + resolveStartAppRequest(scrcpy, options) + }.getOrElse { error -> + logEvent( + "启动应用请求无效: ${ + error.message?.takeIf { it.isNotBlank() } ?: error.javaClass.simpleName + }", + Log.WARN, + error, + ) + null + } + startAppRequest?.let { request -> + if (options.newDisplay.isNotBlank() && request.displayId == null) { + logEvent( + "当前实现无法获取 new display 的真实 displayId,应用会启动到默认显示", + Log.WARN, + ) + } + runCatching { + NativeAdbService.startApp( + packageName = request.packageName, + displayId = request.displayId, + forceStop = request.forceStop, + ) + }.onSuccess { + val appLabelPart = request.matchedAppLabel?.let { " ($it)" }.orEmpty() + logEvent( + "已启动应用: ${request.packageName}$appLabelPart" + + request.displayId?.let { " @display=$it" }.orEmpty() + ) + }.onFailure { error -> + logEvent( + "启动应用失败: ${request.packageName} (${ + error.message?.takeIf { it.isNotBlank() } ?: error.javaClass.simpleName + })", + Log.WARN, + error, + ) + } + } + if (options.fullscreen || openFullscreen) withContext(Dispatchers.Main) { context.startActivity(StreamActivity.createIntent(context)) } if (options.disableScreensaver) @@ -549,6 +648,15 @@ fun DeviceTabPage( snackbar.show("scrcpy 已启动") } + suspend fun stopScrcpySession() { + val options = scrcpyOptions.toClientOptions(soBundleShared).fix() + scrcpy.stop() + if (options.killAdbOnClose) { + // TODO + disconnectAdbConnection() + } + } + LaunchedEffect(pendingScrollToPreview, isPreviewCardVisible) { if (!pendingScrollToPreview) return@LaunchedEffect if (isPreviewCardVisible) return@LaunchedEffect diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/ScrcpyAllOptionsScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/ScrcpyAllOptionsScreen.kt index 46b9850..b1dcfe5 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/ScrcpyAllOptionsScreen.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/ScrcpyAllOptionsScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -40,8 +41,13 @@ import io.github.miuzarte.scrcpyforandroid.scaffolds.LazyColumn import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperSlider import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperTextField import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy -import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared +import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.AudioSource +import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.CameraFacing import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Codec +import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.DisplayImePolicy +import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.LogLevel +import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Tick +import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.VideoSource import io.github.miuzarte.scrcpyforandroid.services.LocalSnackbarController import io.github.miuzarte.scrcpyforandroid.storage.Settings import io.github.miuzarte.scrcpyforandroid.storage.Storage.scrcpyOptions @@ -61,6 +67,9 @@ import top.yukonga.miuix.kmp.basic.SpinnerEntry import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.basic.TextField import top.yukonga.miuix.kmp.basic.TopAppBar +import top.yukonga.miuix.kmp.icon.MiuixIcons +import top.yukonga.miuix.kmp.icon.extended.Store +import top.yukonga.miuix.kmp.icon.extended.Tune import top.yukonga.miuix.kmp.preference.ArrowPreference import top.yukonga.miuix.kmp.preference.OverlayDropdownPreference import top.yukonga.miuix.kmp.preference.OverlaySpinnerPreference @@ -153,26 +162,50 @@ internal fun ScrcpyAllOptionsPage( .coerceAtLeast(0) } - val videoSourceItems = rememberSaveable { Shared.VideoSource.entries.map { it.string } } + val videoSourceItems = rememberSaveable { VideoSource.entries.map { it.string } } val videoSourceIndex = rememberSaveable(soBundle.videoSource) { - Shared.VideoSource.entries + VideoSource.entries .indexOfFirst { it.string == soBundle.videoSource } .coerceAtLeast(0) } - var displayIdInput by rememberSaveable(soBundle.displayId) { - mutableStateOf( - if (soBundle.displayId == -1) "" - else soBundle.displayId.toString() - ) + val displays = scrcpy.listings.displays + val displayDropdownItems = rememberSaveable(displays, listRefreshVersion) { + listOf("默认") + displays.map { "${it.id} (${it.width}x${it.height})" } + } + val displayDropdownIndex = rememberSaveable( + soBundle.displayId, + displays, + listRefreshVersion, + ) { + (displays.indexOfFirst { it.id == soBundle.displayId } + 1).coerceAtLeast(0) } - var cameraIdInput by rememberSaveable(soBundle.cameraId) { - mutableStateOf(soBundle.cameraId) + val cameras = scrcpy.listings.cameras + val cameraDropdownItems = rememberSaveable(cameras, listRefreshVersion) { + listOf("默认") + cameras.map { info -> + buildString { + append(info.id) + append(" (") + append(info.facing.string) + if (info.activeSize.isNotBlank()) { + append(", ") + append(info.activeSize) + } + append(')') + } + } + } + val cameraDropdownIndex = rememberSaveable( + soBundle.cameraId, + cameras, + listRefreshVersion, + ) { + (cameras.indexOfFirst { it.id == soBundle.cameraId } + 1).coerceAtLeast(0) } val cameraFacingItems = rememberSaveable { - listOf("默认") + Shared.CameraFacing.entries + listOf("默认") + CameraFacing.entries .drop(1) .map { it.string } } @@ -180,7 +213,7 @@ internal fun ScrcpyAllOptionsPage( if (soBundle.cameraFacing.isEmpty()) { 0 } else { - val idx = Shared.CameraFacing.entries + val idx = CameraFacing.entries .indexOfFirst { it.string == soBundle.cameraFacing } if (idx > 0) idx else 0 } @@ -192,24 +225,23 @@ internal fun ScrcpyAllOptionsPage( val cameraSizes = scrcpy.listings.cameraSizes val cameraSizeDropdownItems = rememberSaveable(cameraSizes, listRefreshVersion) { - listOf("自动", "自定义") + cameraSizes + listOf("默认", "自定义") + cameraSizes } - var cameraSizeDropdownIndex by rememberSaveable( + val cameraSizeDropdownIndex = rememberSaveable( soBundle.cameraSize, + soBundle.cameraSizeCustom, soBundle.cameraSizeUseCustom, cameraSizes, listRefreshVersion, ) { - mutableIntStateOf( - when { - soBundle.cameraSizeUseCustom -> 1 // "自定义" - soBundle.cameraSize.isEmpty() -> 0 // "自动" - soBundle.cameraSize in cameraSizes -> - cameraSizes.indexOf(soBundle.cameraSize) + 2 + when { + soBundle.cameraSizeUseCustom -> 1 + soBundle.cameraSize.isEmpty() -> 0 + soBundle.cameraSize in cameraSizes -> + cameraSizes.indexOf(soBundle.cameraSize) + 2 - else -> 0 // 默认自动 - } - ) + else -> 0 + } } var cameraArInput by rememberSaveable(soBundle.cameraAr) { @@ -220,28 +252,19 @@ internal fun ScrcpyAllOptionsPage( ScrcpyPresets.CameraFps.indexOfOrNearest(soBundle.cameraFps) } - val audioSourceItems = rememberSaveable { - Shared.AudioSource.entries.map { it.string } - } - val audioSourceIndex = rememberSaveable(soBundle.audioSource) { - Shared.AudioSource.entries - .indexOfFirst { it.string == soBundle.audioSource } - .coerceAtLeast(0) + val screenOffTimeoutPresetIndex = rememberSaveable(soBundle.screenOffTimeout) { + ScrcpyPresets.ScreenOffTimeout.indexOfOrNearest( + Tick(soBundle.screenOffTimeout).sec.toInt().coerceAtLeast(0) + ) } - val videoEncoderInfos = scrcpy.listings.videoEncoders - val audioEncoderInfos = scrcpy.listings.audioEncoders - val videoEncoders = remember(videoEncoderInfos) { - videoEncoderInfos.map { it.id } + val audioSourceItems = rememberSaveable { + AudioSource.entries.map { it.string } } - val audioEncoders = remember(audioEncoderInfos) { - audioEncoderInfos.map { it.id } - } - val videoEncoderTypes = remember(videoEncoderInfos) { - videoEncoderInfos.associate { it.id to it.type.s } - } - val audioEncoderTypes = remember(audioEncoderInfos) { - audioEncoderInfos.associate { it.id to it.type.s } + val audioSourceIndex = rememberSaveable(soBundle.audioSource) { + AudioSource.entries + .indexOfFirst { it.string == soBundle.audioSource } + .coerceAtLeast(0) } val maxSizePresetIndex = rememberSaveable(soBundle.maxSize) { @@ -260,46 +283,118 @@ internal fun ScrcpyAllOptionsPage( mutableStateOf(soBundle.audioCodecOptions) } - val videoEncoderDropdownItems = rememberSaveable(videoEncoders, listRefreshVersion) { - listOf("") + videoEncoders - } - val videoEncoderIndex = - rememberSaveable(soBundle.videoEncoder, videoEncoders, listRefreshVersion) { - (videoEncoders.indexOf(soBundle.videoEncoder) + 1).coerceAtLeast(0) + val videoEncoders = scrcpy.listings.videoEncoders + val videoEncoderItems by remember(videoEncoders, listRefreshVersion) { + derivedStateOf { + buildList { + add(SpinnerEntry(title = "自动")) + videoEncoders.forEach { info -> + add( + SpinnerEntry( + title = info.id, + summary = info.type.s, + ) + ) + } + } } + } + val videoEncoderIndex = rememberSaveable( + soBundle.videoEncoder, + videoEncoders, + listRefreshVersion, + ) { + (videoEncoders.indexOfFirst { it.id == soBundle.videoEncoder } + 1) + .coerceAtLeast(0) + } - val audioEncoderDropdownItems = rememberSaveable(audioEncoders, listRefreshVersion) { - listOf("") + audioEncoders + val audioEncoders = scrcpy.listings.audioEncoders + val audioEncoderItems by remember(audioEncoders, listRefreshVersion) { + derivedStateOf { + buildList { + add(SpinnerEntry(title = "自动")) + audioEncoders.forEach { info -> + add( + SpinnerEntry( + title = info.id, + summary = info.type.s, + ) + ) + } + } + } } val audioEncoderIndex = rememberSaveable( soBundle.audioEncoder, audioEncoders, - listRefreshVersion + listRefreshVersion, ) { - (audioEncoders.indexOf(soBundle.audioEncoder) + 1).coerceAtLeast(0) + (audioEncoders.indexOfFirst { it.id == soBundle.audioEncoder } + 1) + .coerceAtLeast(0) } - val videoEncoderEntries = videoEncoderDropdownItems.map { encoderName -> - if (encoderName == "") { - SpinnerEntry(title = "自动") + val displayImePolicyItems = rememberSaveable { + listOf("默认") + DisplayImePolicy.entries + .drop(1) + .map { it.string } + } + val displayImePolicyIndex = rememberSaveable(soBundle.displayImePolicy) { + if (soBundle.displayImePolicy.isEmpty()) { + 0 } else { - SpinnerEntry( - title = encoderName, - summary = videoEncoderTypes[encoderName], - ) + val idx = DisplayImePolicy.entries + .indexOfFirst { it.string == soBundle.displayImePolicy } + if (idx > 0) idx else 0 } } - val audioEncoderEntries = audioEncoderDropdownItems.map { encoderName -> - if (encoderName == "") { - SpinnerEntry(title = "自动") - } else { - SpinnerEntry( - title = encoderName, - summary = audioEncoderTypes[encoderName], - ) + val apps = remember(scrcpy.listings.apps, listRefreshVersion) { + scrcpy.listings.apps.sortedBy { it.packageName } + } + val appDropdownItems by remember(apps, listRefreshVersion) { + derivedStateOf { + buildList { + add(SpinnerEntry(title = "无")) + add(SpinnerEntry(title = "自定义")) + apps.forEach { app -> + add( + SpinnerEntry( + icon = { + Icon( + imageVector = + if (app.system) MiuixIcons.Tune + else MiuixIcons.Store, + contentDescription = app.label, + modifier = Modifier.padding(end = UiSpacing.ContentVertical), + ) + }, + title = app.label, + summary = app.packageName, + ) + ) + } + } } } + val appDropdownIndex = rememberSaveable( + soBundle.startApp, + soBundle.startAppCustom, + soBundle.startAppUseCustom, + apps, + listRefreshVersion, + ) { + when { + soBundle.startAppUseCustom -> 1 + soBundle.startApp.isEmpty() -> 0 + apps.any { it.packageName == soBundle.startApp } -> + apps.indexOfFirst { it.packageName == soBundle.startApp } + 2 + + else -> 0 + } + } + var startAppCustomInput by rememberSaveable(soBundle.startAppCustom) { + mutableStateOf(soBundle.startAppCustom) + } // [x][/] val (ndWidth, ndHeight, ndDpi) = remember(soBundle.newDisplay) { @@ -332,6 +427,13 @@ internal fun ScrcpyAllOptionsPage( mutableStateOf(cY?.toString() ?: "") } + val logLevelItems = rememberSaveable { LogLevel.entries.map { it.string } } + val logLevelIndex = rememberSaveable(soBundle.logLevel) { + LogLevel.entries + .indexOfFirst { it.string == soBundle.logLevel } + .coerceAtLeast(0) + } + var serverParamsPreview by rememberSaveable { mutableStateOf("") } // 监听选项变化, 自动更新预览 LaunchedEffect(soBundle) { @@ -351,7 +453,7 @@ internal fun ScrcpyAllOptionsPage( serverParamsPreview = clientOptions .toServerParams(0u) - .toList(simplify = true) + .toList(preview = true) // improve readability using hard line breaks .joinToString("\n") } @@ -374,19 +476,19 @@ internal fun ScrcpyAllOptionsPage( item { Card { SwitchPreference( - title = "启动后关闭屏幕", + title = "scrcpy 启动后熄灭设备屏幕", summary = "--turn-screen-off", checked = soBundle.turnScreenOff, onCheckedChange = { soBundle = soBundle.copy( turnScreenOff = it ) - if (it) { + if (it) snackbar.show( // github.com/Genymobile/scrcpy/issues/3376 // github.com/Genymobile/scrcpy/issues/4587 // github.com/Genymobile/scrcpy/issues/5676 - snackbar.show("注意:大部分设备在关闭屏幕后刷新率会降低/减半") - } + "注意:大部分设备在关闭屏幕后刷新率会降低/减半" + ) }, ) SwitchPreference( @@ -424,38 +526,45 @@ internal fun ScrcpyAllOptionsPage( // enabled = control || video, ) SuperSlider( - title = "投屏过程中受控机的息屏时间", + title = "scrcpy 启动后受控机的息屏时间", summary = "--screen-off-timeout", - value = soBundle.screenOffTimeout.coerceAtLeast(0).toFloat(), - onValueChange = { + value = screenOffTimeoutPresetIndex.toFloat(), + onValueChange = { value -> + val idx = value.roundToInt() + .coerceIn(0, ScrcpyPresets.ScreenOffTimeout.lastIndex) soBundle = soBundle.copy( - screenOffTimeout = it.roundToInt() - .takeIf { value -> value > 0 } - ?.toLong() ?: -1 + screenOffTimeout = ScrcpyPresets.ScreenOffTimeout[idx] + .takeIf { it > 0 } + ?.toLong() + ?.let(Tick::fromSec) + ?.value + ?: -1 ) }, - valueRange = 0f..600f, - steps = 600 - 1, + valueRange = 0f..ScrcpyPresets.ScreenOffTimeout.lastIndex.toFloat(), + steps = (ScrcpyPresets.ScreenOffTimeout.size - 2).coerceAtLeast(0), unit = "s", - zeroStateText = "默认", - displayText = - if (soBundle.screenOffTimeout <= 0) "默认" - else soBundle.screenOffTimeout.toString(), + zeroStateText = "不修改", + showKeyPoints = true, + keyPoints = ScrcpyPresets.ScreenOffTimeout.indices.map { it.toFloat() }, + displayText = Tick(soBundle.screenOffTimeout).sec.toString(), inputInitialValue = if (soBundle.screenOffTimeout <= 0) "" - else soBundle.screenOffTimeout.toString(), + else Tick(soBundle.screenOffTimeout).sec.toString(), inputFilter = { it.filter(Char::isDigit) }, inputValueRange = 0f..86400f, onInputConfirm = { soBundle = soBundle.copy( screenOffTimeout = it.toLongOrNull() ?.takeIf { value -> value > 0 } + ?.let(Tick::fromSec) + ?.value ?: -1 ) }, ) SwitchPreference( - title = "开始投屏时不唤醒屏幕", + title = "scrcpy 启动时不唤醒屏幕", summary = "--no-power-on", checked = !soBundle.powerOn, onCheckedChange = { @@ -465,7 +574,7 @@ internal fun ScrcpyAllOptionsPage( }, ) SwitchPreference( - title = "结束投屏时息屏", + title = "scrcpy 结束后息屏", summary = "--power-off-on-close", checked = soBundle.powerOffOnClose, onCheckedChange = { @@ -474,6 +583,16 @@ internal fun ScrcpyAllOptionsPage( ) }, ) + SwitchPreference( + title = "scrcpy 启动后保持设备唤醒状态(插入电源)", + summary = "--stay-awake", + checked = soBundle.stayAwake, + onCheckedChange = { + soBundle = soBundle.copy( + stayAwake = it + ) + }, + ) SwitchPreference( title = "显示物理触控", summary = "--show-touches", @@ -485,15 +604,42 @@ internal fun ScrcpyAllOptionsPage( }, ) SwitchPreference( - title = "投屏时保持本机屏幕唤醒", + title = "启动后进入全屏", + summary = "--fullscreen", + checked = soBundle.fullscreen, + onCheckedChange = { + soBundle = soBundle.copy( + fullscreen = it + ) + }, + ) + SwitchPreference( + title = "scrcpy 启动后保持本机屏幕唤醒", summary = "--disable-screensaver", checked = soBundle.disableScreensaver, onCheckedChange = { soBundle = soBundle.copy( disableScreensaver = it ) + if (it) snackbar.show( + "不保证可用" + ) }, ) + SwitchPreference( + title = "scrcpy 结束时断开 adb", + summary = "--kill-adb-on-close", + checked = soBundle.killAdbOnClose, + onCheckedChange = { + soBundle = soBundle.copy( + killAdbOnClose = it + ) + if (it) snackbar.show( + "未实现" + ) + }, + enabled = false, + ) } } @@ -610,26 +756,46 @@ internal fun ScrcpyAllOptionsPage( selectedIndex = videoSourceIndex, onSelectedIndexChange = { soBundle = soBundle.copy( - videoSource = Shared.VideoSource.entries[it].string + videoSource = VideoSource.entries[it].string ) }, ) AnimatedVisibility(soBundle.videoSource == "display") { Column { - SuperTextField( - value = displayIdInput, - onValueChange = { displayIdInput = it }, - onFocusLost = { + ArrowPreference( + title = "获取 Displays", + summary = "--list-displays", + onClick = { + if (refreshBusy) return@ArrowPreference + scope.launch { + refreshBusy = true + snackbar.show("获取中") + try { + withContext(Dispatchers.IO) { + scrcpy.listings.getDisplays(forceRefresh = true) + } + listRefreshVersion += 1 + snackbar.show("获取成功") + } catch (e: Exception) { + snackbar.show("刷新失败: ${e.message}") + } finally { + refreshBusy = false + } + } + }, + ) + OverlayDropdownPreference( + title = "监视器 ID", + summary = "--display-id", + items = displayDropdownItems, + selectedIndex = displayDropdownIndex, + onSelectedIndexChange = { soBundle = soBundle.copy( - displayId = displayIdInput.toIntOrNull() ?: -1 + displayId = + if (it == 0) -1 + else displays[it - 1].id ) }, - label = "--display-id", - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier - .fillMaxWidth() - .padding(all = UiSpacing.Large), ) SuperSlider( title = "最大分辨率", @@ -694,19 +860,40 @@ internal fun ScrcpyAllOptionsPage( } AnimatedVisibility(soBundle.videoSource == "camera") { Column { - SuperTextField( - value = cameraIdInput, - onValueChange = { cameraIdInput = it }, - onFocusLost = { + ArrowPreference( + title = "获取 Cameras", + summary = "--list-cameras", + onClick = { + if (refreshBusy) return@ArrowPreference + scope.launch { + refreshBusy = true + snackbar.show("获取中") + try { + withContext(Dispatchers.IO) { + scrcpy.listings.getCameras(forceRefresh = true) + } + listRefreshVersion += 1 + snackbar.show("获取成功") + } catch (e: Exception) { + snackbar.show("刷新失败: ${e.message}") + } finally { + refreshBusy = false + } + } + }, + ) + OverlayDropdownPreference( + title = "摄像头 ID", + summary = "--camera-id", + items = cameraDropdownItems, + selectedIndex = cameraDropdownIndex, + onSelectedIndexChange = { soBundle = soBundle.copy( - cameraId = cameraIdInput + cameraId = + if (it == 0) "" + else cameras[it - 1].id ) }, - label = "--camera-id", - singleLine = true, - modifier = Modifier - .fillMaxWidth() - .padding(all = UiSpacing.Large), ) OverlayDropdownPreference( title = "摄像头朝向", @@ -717,7 +904,7 @@ internal fun ScrcpyAllOptionsPage( soBundle = soBundle.copy( cameraFacing = if (it == 0) "" - else Shared.CameraFacing.entries[it].string + else CameraFacing.entries[it].string ) }, ) @@ -749,7 +936,6 @@ internal fun ScrcpyAllOptionsPage( items = cameraSizeDropdownItems, selectedIndex = cameraSizeDropdownIndex, onSelectedIndexChange = { - cameraSizeDropdownIndex = it when (it) { 0 -> { // "自动" @@ -765,7 +951,7 @@ internal fun ScrcpyAllOptionsPage( soBundle = soBundle.copy( cameraSizeUseCustom = true ) - cameraSizeCustomInput = soBundle.cameraSize + cameraSizeCustomInput = "" } else -> { @@ -785,15 +971,14 @@ internal fun ScrcpyAllOptionsPage( value = cameraSizeCustomInput, onValueChange = { cameraSizeCustomInput = it }, onFocusLost = { - if (cameraSizeCustomInput in cameraSizes) { + soBundle = if (cameraSizeCustomInput in cameraSizes) { // 输入的值存在于列表中, 取消自定义输入 - cameraSizeDropdownIndex = cameraSizes - .indexOf(cameraSizeCustomInput) + 2 - soBundle = soBundle.copy( + soBundle.copy( + cameraSize = cameraSizeCustomInput, cameraSizeUseCustom = false ) } else { - soBundle = soBundle.copy( + soBundle.copy( cameraSizeCustom = cameraSizeCustomInput ) } @@ -859,26 +1044,6 @@ internal fun ScrcpyAllOptionsPage( ) }, ) - SwitchPreference( - title = "关闭虚拟显示器时保留内容", - summary = "--no-vd-destroy-content", - checked = !soBundle.vdDestroyContent, - onCheckedChange = { - soBundle = soBundle.copy( - vdDestroyContent = !it - ) - }, - ) - SwitchPreference( - title = "禁用虚拟显示器系统装饰", - summary = "--no-vd-system-decorations", - checked = !soBundle.vdSystemDecorations, - onCheckedChange = { - soBundle = soBundle.copy( - vdSystemDecorations = !it - ) - }, - ) } } @@ -894,7 +1059,7 @@ internal fun ScrcpyAllOptionsPage( selectedIndex = audioSourceIndex, onSelectedIndexChange = { soBundle = soBundle.copy( - audioSource = Shared.AudioSource.entries[it].string + audioSource = AudioSource.entries[it].string ) }, ) @@ -943,7 +1108,7 @@ internal fun ScrcpyAllOptionsPage( snackbar.show("获取中") try { withContext(Dispatchers.IO) { - scrcpy.listings.getVideoEncoders(forceRefresh = true) + scrcpy.listings.getEncoders(forceRefresh = true) } listRefreshVersion += 1 snackbar.show("获取成功") @@ -959,11 +1124,13 @@ internal fun ScrcpyAllOptionsPage( OverlaySpinnerPreference( title = "视频编码器", summary = "--video-encoder", - items = videoEncoderEntries, + items = videoEncoderItems, selectedIndex = videoEncoderIndex, onSelectedIndexChange = { soBundle = soBundle.copy( - videoEncoder = videoEncoderEntries[it].title ?: "" + videoEncoder = + if (it == 0) "" + else videoEncoders[it - 1].id ) }, ) @@ -984,11 +1151,13 @@ internal fun ScrcpyAllOptionsPage( OverlaySpinnerPreference( title = "音频编码器", summary = "--audio-encoder", - items = audioEncoderEntries, + items = audioEncoderItems, selectedIndex = audioEncoderIndex, onSelectedIndexChange = { soBundle = soBundle.copy( - audioEncoder = audioEncoderEntries[it].title ?: "" + audioEncoder = + if (it == 0) "" + else audioEncoders[it - 1].id ) }, ) @@ -1009,6 +1178,160 @@ internal fun ScrcpyAllOptionsPage( } } + if (soBundle.videoSource == "display") item { + Card { + OverlayDropdownPreference( + title = "输入法显示策略", + summary = "--display-ime-policy", + items = displayImePolicyItems, + selectedIndex = displayImePolicyIndex, + onSelectedIndexChange = { + soBundle = soBundle.copy( + displayImePolicy = + if (it == 0) "" + else DisplayImePolicy.entries[it].string + ) + }, + ) + SwitchPreference( + title = "关闭虚拟显示器时保留内容", + summary = "--no-vd-destroy-content", + checked = !soBundle.vdDestroyContent, + onCheckedChange = { + soBundle = soBundle.copy( + vdDestroyContent = !it + ) + }, + ) + SwitchPreference( + title = "禁用虚拟显示器系统装饰", + summary = "--no-vd-system-decorations", + checked = !soBundle.vdSystemDecorations, + onCheckedChange = { + soBundle = soBundle.copy( + vdSystemDecorations = !it + ) + }, + ) + SwitchPreference( + title = "禁用结束后清理", + summary = "--no-downsize-on-error", + checked = !soBundle.downsizeOnError, + onCheckedChange = { + soBundle = soBundle.copy( + downsizeOnError = !it + ) + if (it) snackbar.show( + "默认情况下,在 MediaCodec 出错时," + + "scrcpy 会自动尝试使用更低的分辨率重新开始。" + + "\n此选项将禁用此行为。" + ) + }, + ) + SwitchPreference( + title = "禁用结束后清理", + summary = "--no-cleanup", + checked = !soBundle.cleanup, + onCheckedChange = { + soBundle = soBundle.copy( + cleanup = !it + ) + if (it) snackbar.show( + "默认情况下,scrcpy 会从设备中移除服务器二进制文件," + + "并在退出时恢复设备状态(显示触摸、保持唤醒和电源模式)" + + "\n此选项将禁用此清理操作" + ) + }, + ) + } + } + + if (soBundle.videoSource == "display") item { + Card { + ArrowPreference( + title = "获取应用列表", + summary = "--list-apps", + onClick = { + if (refreshBusy) return@ArrowPreference + scope.launch { + refreshBusy = true + snackbar.show("获取中") + try { + withContext(Dispatchers.IO) { + scrcpy.listings.getApps(forceRefresh = true) + } + listRefreshVersion += 1 + snackbar.show("获取成功") + } catch (e: Exception) { + snackbar.show("刷新失败: ${e.message}") + } finally { + refreshBusy = false + } + } + }, + ) + OverlaySpinnerPreference( + title = "scrcpy 启动后打开应用", + summary = "--start-app\nTODO: 未实现虚拟屏配合", + items = appDropdownItems, + selectedIndex = appDropdownIndex, + onSelectedIndexChange = { + when (it) { + 0 -> { + soBundle = soBundle.copy( + startApp = "", + startAppUseCustom = false, + ) + startAppCustomInput = "" + } + + 1 -> { + soBundle = soBundle.copy( + startAppUseCustom = true + ) + startAppCustomInput = "" + } + + else -> { + val selectedApp = apps[it - 2] + soBundle = soBundle.copy( + startApp = selectedApp.packageName, + startAppUseCustom = false, + ) + startAppCustomInput = "" + } + } + }, + ) + AnimatedVisibility(soBundle.startAppUseCustom) { + SuperTextField( + value = startAppCustomInput, + onValueChange = { startAppCustomInput = it }, + onFocusLost = { + val matchedAppIndex = apps + .indexOfFirst { it.packageName == startAppCustomInput } + soBundle = if (matchedAppIndex >= 0) { + soBundle.copy( + startApp = apps[matchedAppIndex].packageName, + startAppUseCustom = false, + ) + } else { + soBundle.copy( + startAppCustom = startAppCustomInput + ) + } + }, + label = "--start-app", + useLabelAsPlaceholder = true, + singleLine = true, + modifier = Modifier + .fillMaxWidth() + .padding(all = UiSpacing.Large), + ) + } + } + } + if (soBundle.videoSource == "display") item { Card { Column( @@ -1243,6 +1566,22 @@ internal fun ScrcpyAllOptionsPage( } } + item { + Card { + OverlayDropdownPreference( + title = "日志等级", + summary = "--verbosity", + items = logLevelItems, + selectedIndex = logLevelIndex, + onSelectedIndexChange = { + soBundle = soBundle.copy( + logLevel = LogLevel.entries[it].string + ) + }, + ) + } + } + item { Spacer(Modifier.height(UiSpacing.PageBottom)) } } } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/ClientOptions.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/ClientOptions.kt index e3ae058..bd9a4c6 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/ClientOptions.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/ClientOptions.kt @@ -164,7 +164,7 @@ data class ClientOptions( // var clipboardAutosync: Boolean = true, // to server // --no-downsize-on-error - // var downsizeOnError: Boolean = true, // to server + var downsizeOnError: Boolean = true, // to server // var tcpip: Boolean, // to server // var tcpipDst: String = "", // to server @@ -172,7 +172,7 @@ data class ClientOptions( // var selectTcpip: Boolean, // to server // --no-cleanup - var cleanUp: Boolean = true, // to server + var cleanup: Boolean = true, // to server // var startFpsCounter: Boolean, @@ -226,6 +226,12 @@ data class ClientOptions( M4A, MKA, OPUS, AAC, FLAC, WAV -> true else -> false } + + companion object { + fun fromString(value: String) = + entries.find { it.string.equals(value, ignoreCase = true) } + ?: AUTO + } } fun fix(): ClientOptions { @@ -552,7 +558,9 @@ data class ClientOptions( powerOffOnClose = powerOffOnClose, - cleanUp = cleanUp, + downsizeOnError = downsizeOnError, + + cleanUp = cleanup, powerOn = powerOn, // killAdbOnClose == killAdbOnClose, // client side diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/Scrcpy.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/Scrcpy.kt index 814e64d..26651f6 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/Scrcpy.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/Scrcpy.kt @@ -344,7 +344,7 @@ class Scrcpy( return getEncoders(forceRefresh).second } - private suspend fun getEncoders(forceRefresh: Boolean = false) + suspend fun getEncoders(forceRefresh: Boolean = false) : Pair, List> { if (!forceRefresh && cachedVideoEncoders != null && cachedAudioEncoders != null) return cachedVideoEncoders.orEmpty() to cachedAudioEncoders.orEmpty() @@ -455,7 +455,7 @@ class Scrcpy( video = false, audio = false, control = false, - cleanUp = false, + cleanup = false, list = list, ) val serverParams = options.toServerParams(scid) @@ -1065,6 +1065,7 @@ class Scrcpy( } serverLogBuffer.addLast(line) } + logEvent(line) Log.i(TAG, "[server:$socketName] $line") } } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/ServerParams.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/ServerParams.kt index 0097b4b..8fc1c47 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/ServerParams.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/ServerParams.kt @@ -73,7 +73,7 @@ data class ServerParams( var powerOffOnClose: Boolean, - // var downsizeOnError: Boolean, + var downsizeOnError: Boolean, // var tcpip: Boolean, // var tcpipDst: String, // var selectUsb: Boolean, @@ -107,13 +107,12 @@ data class ServerParams( return (extraArgs.toList() + this.toList()).joinToString(SEPARATOR) } - fun toList(simplify: Boolean = false): MutableList { + fun toList(preview: Boolean = false): MutableList { val cmd = mutableListOf() - if (!simplify) { - cmd.add("scid=${scid.toString(16)}") - cmd.add("log_level=${logLevel.string}") - } + if (!preview) cmd.add("scid=${scid.toString(16)}") + + cmd.add("log_level=${logLevel.string}") if (!video) { cmd.add("video=false") @@ -216,7 +215,7 @@ data class ServerParams( require(screenOffTimeout >= 0) { "screen_off_timeout must be >= 0" } - cmd.add("screen_off_timeout=${screenOffTimeout.toMs()}") + cmd.add("screen_off_timeout=${screenOffTimeout.ms}") } if (videoCodecOptions.isNotBlank()) { validate(videoCodecOptions) @@ -242,8 +241,6 @@ data class ServerParams( if (!clipBoardAutosync) { cmd.add("clipboard_autosync=false") } - // not implemented - val downsizeOnError = false if (!downsizeOnError) { cmd.add("downsize_on_error=false") } @@ -285,4 +282,4 @@ data class ServerParams( } return cmd } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/Shared.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/Shared.kt index 07d4fcb..9590ae0 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/Shared.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/Shared.kt @@ -15,6 +15,11 @@ class Shared { fun toMs(): Long = value / 1000 fun toSec(): Long = value / 1_000_000 fun toSecDouble(): Double = value / 1_000_000.0 + val ns: Long get() = toNs() + val us: Long get() = toUs() + val ms: Long get() = toMs() + val sec: Long get() = toSec() + val secDouble: Double get() = toSecDouble() operator fun plus(other: Tick): Tick = Tick(value + other.value) operator fun minus(other: Tick): Tick = Tick(value - other.value) @@ -38,17 +43,23 @@ class Shared { fun Duration.toTick(): Tick = Tick(this.inWholeMicroseconds) } - enum class ListOptions(val value: Int) { - NULL(0x0), - ENCODERS(0x1), // --list-encoders - DISPLAYS(0x2), // --list-displays - CAMERAS(0x4), // --list-cameras - CAMERA_SIZES(0x8), // --list-camera-sizes - APPS(0x10); // --list-apps + enum class ListOptions(val string: String, val value: Int) { + NULL("null", 0x0), + ENCODERS("encoders", 0x1), // --list-encoders + DISPLAYS("displays", 0x2), // --list-displays + CAMERAS("cameras", 0x4), // --list-cameras + CAMERA_SIZES("camera_sizes", 0x8), // --list-camera-sizes + APPS("apps", 0x10); // --list-apps infix fun or(other: ListOptions) = value or other.value infix fun and(other: ListOptions) = value and other.value infix fun has(other: ListOptions) = this and other != 0 + + companion object { + fun fromString(value: String) = + entries.find { it.string.equals(value, ignoreCase = true) } + ?: NULL + } } enum class EncoderType(val s: String) { @@ -62,6 +73,12 @@ class Shared { INFO("info"), WARN("warn"), ERROR("error"); + + companion object { + fun fromString(value: String) = + entries.find { it.string.equals(value, ignoreCase = true) } + ?: VERBOSE + } } enum class Codec( @@ -177,6 +194,12 @@ class Shared { UNLOCKED("unlocked"), // ignore LOCKED_VALUE("locked_value"), // "@${orientation.string}" LOCKED_INITIAL("locked_initial"); // "@" + + companion object { + fun fromString(value: String) = + entries.find { it.string.equals(value, ignoreCase = true) } + ?: UNLOCKED + } } enum class DisplayImePolicy(val string: String) { @@ -184,5 +207,11 @@ class Shared { LOCAL("local"), FALLBACK("fallback"), HIDE("hide"); + + companion object { + fun fromString(value: String) = + entries.find { it.string.equals(value, ignoreCase = true) } + ?: UNDEFINED + } } } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/ScrcpyOptions.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/ScrcpyOptions.kt index 4075e9e..5fca1a8 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/ScrcpyOptions.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/ScrcpyOptions.kt @@ -8,6 +8,7 @@ import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import io.github.miuzarte.scrcpyforandroid.scrcpy.ClientOptions +import io.github.miuzarte.scrcpyforandroid.scrcpy.ClientOptions.RecordFormat import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.AudioSource import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.CameraFacing import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Codec @@ -74,11 +75,11 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") { ) val LOG_LEVEL = Pair( stringPreferencesKey("log_level"), - "info", + LogLevel.INFO.string, ) val VIDEO_CODEC = Pair( stringPreferencesKey("video_codec"), - "h264", + Codec.H264.string, ) val AUDIO_CODEC = Pair( stringPreferencesKey("audio_codec"), @@ -138,7 +139,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") { ) val DISPLAY_IME_POLICY = Pair( stringPreferencesKey("display_ime_policy"), - "undefined", + DisplayImePolicy.UNDEFINED.string, ) val DISPLAY_ID = Pair( intPreferencesKey("display_id"), @@ -184,6 +185,10 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") { booleanPreferencesKey("power_off_on_close"), false, ) + val DOWNSIZE_ON_ERROR = Pair( + booleanPreferencesKey("downsize_on_error"), + true, + ) val CLEANUP = Pair( booleanPreferencesKey("cleanup"), true, @@ -228,6 +233,14 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") { stringPreferencesKey("start_app"), "", ) + val START_APP_CUSTOM = Pair( + stringPreferencesKey("start_app_custom"), + "", + ) + val START_APP_USE_CUSTOM = Pair( + booleanPreferencesKey("start_app_use_custom"), + false, + ) val VD_DESTROY_CONTENT = Pair( booleanPreferencesKey("vd_destroy_content"), true, @@ -334,6 +347,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") { val stayAwake: Boolean, val disableScreensaver: Boolean, val powerOffOnClose: Boolean, + val downsizeOnError: Boolean, val cleanup: Boolean, val powerOn: Boolean, val video: Boolean, @@ -345,6 +359,8 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") { val audioDup: Boolean, val newDisplay: String, val startApp: String, + val startAppCustom: String, + val startAppUseCustom: Boolean, val vdDestroyContent: Boolean, val vdSystemDecorations: Boolean ) : Parcelable { @@ -449,6 +465,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") { stayAwake = preferences.read(STAY_AWAKE), disableScreensaver = preferences.read(DISABLE_SCREENSAVER), powerOffOnClose = preferences.read(POWER_OFF_ON_CLOSE), + downsizeOnError = preferences.read(DOWNSIZE_ON_ERROR), cleanup = preferences.read(CLEANUP), powerOn = preferences.read(POWER_ON), video = preferences.read(VIDEO), @@ -460,6 +477,8 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") { audioDup = preferences.read(AUDIO_DUP), newDisplay = preferences.read(NEW_DISPLAY), startApp = preferences.read(START_APP), + startAppCustom = preferences.read(START_APP_CUSTOM), + startAppUseCustom = preferences.read(START_APP_USE_CUSTOM), vdDestroyContent = preferences.read(VD_DESTROY_CONTENT), vdSystemDecorations = preferences.read(VD_SYSTEM_DECORATIONS), ) @@ -493,12 +512,12 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") { cameraSize = if (!bundle.cameraSizeUseCustom) bundle.cameraSize else bundle.cameraSizeCustom, cameraAr = bundle.cameraAr, cameraFps = bundle.cameraFps.toUShort(), - logLevel = LogLevel.valueOf(bundle.logLevel.uppercase()), + logLevel = LogLevel.fromString(bundle.logLevel), videoCodec = Codec.fromString(bundle.videoCodec), audioCodec = Codec.fromString(bundle.audioCodec), videoSource = VideoSource.fromString(bundle.videoSource), audioSource = AudioSource.fromString(bundle.audioSource), - recordFormat = ClientOptions.RecordFormat.valueOf(bundle.recordFormat.uppercase()), + recordFormat = RecordFormat.fromString(bundle.recordFormat), cameraFacing = CameraFacing.fromString(bundle.cameraFacing), maxSize = bundle.maxSize.toUShort(), videoBitRate = bundle.videoBitRate, @@ -506,12 +525,10 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") { maxFps = bundle.maxFps, angle = bundle.angle, captureOrientation = Orientation.fromInt(bundle.captureOrientation), - captureOrientationLock = OrientationLock.valueOf( - bundle.captureOrientationLock.uppercase() - ), + captureOrientationLock = OrientationLock.fromString(bundle.captureOrientationLock), displayOrientation = Orientation.fromInt(bundle.displayOrientation), recordOrientation = Orientation.fromInt(bundle.recordOrientation), - displayImePolicy = DisplayImePolicy.valueOf(bundle.displayImePolicy.uppercase()), + displayImePolicy = DisplayImePolicy.fromString(bundle.displayImePolicy), displayId = bundle.displayId, screenOffTimeout = Tick(bundle.screenOffTimeout), showTouches = bundle.showTouches, @@ -523,17 +540,18 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") { stayAwake = bundle.stayAwake, disableScreensaver = bundle.disableScreensaver, powerOffOnClose = bundle.powerOffOnClose, - cleanUp = bundle.cleanup, + downsizeOnError = bundle.downsizeOnError, + cleanup = bundle.cleanup, powerOn = bundle.powerOn, video = bundle.video, audio = bundle.audio, requireAudio = bundle.requireAudio, killAdbOnClose = bundle.killAdbOnClose, cameraHighSpeed = bundle.cameraHighSpeed, - list = ListOptions.valueOf(bundle.list.uppercase()), + list = ListOptions.fromString(bundle.list), audioDup = bundle.audioDup, newDisplay = bundle.newDisplay, - startApp = bundle.startApp, + startApp = if (!bundle.startAppUseCustom) bundle.startApp else bundle.startAppCustom, vdDestroyContent = bundle.vdDestroyContent, vdSystemDecorations = bundle.vdSystemDecorations )