feat():实现命令行启动

This commit is contained in:
2026-07-16 21:58:53 +08:00
parent 491badd2aa
commit db6c4b9bb8
5 changed files with 146 additions and 5 deletions

View File

@@ -10,6 +10,7 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.core.content.edit import androidx.core.content.edit
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import io.github.miuzarte.scrcpyforandroid.models.AdbLaunchParams
import io.github.miuzarte.scrcpyforandroid.pages.MainScreen import io.github.miuzarte.scrcpyforandroid.pages.MainScreen
import io.github.miuzarte.scrcpyforandroid.password.BiometricGate import io.github.miuzarte.scrcpyforandroid.password.BiometricGate
import io.github.miuzarte.scrcpyforandroid.password.PasswordRepository import io.github.miuzarte.scrcpyforandroid.password.PasswordRepository
@@ -44,6 +45,11 @@ class MainActivity: FragmentActivity() {
AppRuntime.init(applicationContext) AppRuntime.init(applicationContext)
AppScreenOn.register(window) AppScreenOn.register(window)
// 解析命令行启动参数adb shell am start --es host ...
AdbLaunchParams.fromIntent(intent)?.let {
AppRuntime.adbLaunchParams = it
}
runBlocking { runBlocking {
PasswordRepository.refresh() PasswordRepository.refresh()
val cached = getAppLanguageTag(applicationContext) val cached = getAppLanguageTag(applicationContext)

View File

@@ -0,0 +1,72 @@
package io.github.miuzarte.scrcpyforandroid.models
import android.content.Intent
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
/**
* 从命令行 Intent extras 中解析的启动参数。
*
* 使用方式adb shell
* ```
* adb shell am start -n io.github.miuzarte.scrcpyforandroid/.MainActivity \
* --es host "192.168.1.100" \
* --ei port 5555 \
* --ei maxSize 1920 \
* --ei videoBitRate 8000000 \
* --es maxFps "60" \
* --ez fullscreen true \
* --es startApp "com.example.app"
* ```
*/
@Parcelize
data class AdbLaunchParams(
/** 目标设备 IP/主机名(必填) */
val host: String,
/** ADB 端口,默认 5555 */
val port: Int = 5555,
/** 视频最大分辨率0 表示不限制 */
val maxSize: Int = 0,
/** 视频码率bps0 表示使用默认值 */
val videoBitRate: Int = 0,
/** 最大帧率,空字符串表示不限制 */
val maxFps: String = "",
/** 启动后自动进入全屏 */
val fullscreen: Boolean = false,
/** 在远程设备上启动指定应用包名 */
val startApp: String = "",
/** 显示触摸点 */
val showTouches: Boolean = false,
/** 启动后关闭设备屏幕 */
val turnScreenOff: Boolean = false,
/** 启用触控操作,默认 true */
val control: Boolean = true,
/** 启用视频流,默认 true */
val video: Boolean = true,
/** 启用音频流,默认 true */
val audio: Boolean = true,
/** 关闭后断掉 ADB 连接 */
val killAdbOnClose: Boolean = false,
) : Parcelable {
companion object {
fun fromIntent(intent: Intent?): AdbLaunchParams? {
intent ?: return null
val host = intent.getStringExtra("host")?.takeIf { it.isNotBlank() } ?: return null
return AdbLaunchParams(
host = host,
port = intent.getIntExtra("port", 5555),
maxSize = intent.getIntExtra("maxSize", 0),
videoBitRate = intent.getIntExtra("videoBitRate", 0),
maxFps = intent.getStringExtra("maxFps") ?: "",
fullscreen = intent.getBooleanExtra("fullscreen", false),
startApp = intent.getStringExtra("startApp") ?: "",
showTouches = intent.getBooleanExtra("showTouches", false),
turnScreenOff = intent.getBooleanExtra("turnScreenOff", false),
control = intent.getBooleanExtra("control", true),
video = intent.getBooleanExtra("video", true),
audio = intent.getBooleanExtra("audio", true),
killAdbOnClose = intent.getBooleanExtra("killAdbOnClose", false),
)
}
}
}

View File

@@ -230,6 +230,7 @@ internal fun DeviceTabPage(
LaunchedEffect(Unit) { viewModel.startAutoReconnectLoop() } LaunchedEffect(Unit) { viewModel.startAutoReconnectLoop() }
LaunchedEffect(Unit) { viewModel.startProfileIdSync() } LaunchedEffect(Unit) { viewModel.startProfileIdSync() }
LaunchedEffect(Unit) { viewModel.startRecentTasksAutoRefresh() } LaunchedEffect(Unit) { viewModel.startRecentTasksAutoRefresh() }
LaunchedEffect(Unit) { viewModel.processLaunchParams() }
fun openFullscreenControl() { fun openFullscreenControl() {
if (viewModel.shouldOpenFullscreenCompat()) if (viewModel.shouldOpenFullscreenCompat())

View File

@@ -7,9 +7,11 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import io.github.miuzarte.scrcpyforandroid.R import io.github.miuzarte.scrcpyforandroid.R
import io.github.miuzarte.scrcpyforandroid.StreamActivity import io.github.miuzarte.scrcpyforandroid.StreamActivity
import io.github.miuzarte.scrcpyforandroid.models.AdbLaunchParams
import io.github.miuzarte.scrcpyforandroid.models.ConnectionTarget import io.github.miuzarte.scrcpyforandroid.models.ConnectionTarget
import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut
import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcuts import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcuts
import io.github.miuzarte.scrcpyforandroid.scrcpy.ClientOptions
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
import io.github.miuzarte.scrcpyforandroid.services.* import io.github.miuzarte.scrcpyforandroid.services.*
import io.github.miuzarte.scrcpyforandroid.services.EventLogger.logEvent import io.github.miuzarte.scrcpyforandroid.services.EventLogger.logEvent
@@ -512,6 +514,7 @@ internal class DeviceTabViewModel(
autoStartScrcpy: Boolean = false, autoStartScrcpy: Boolean = false,
autoEnterFullScreen: Boolean = false, autoEnterFullScreen: Boolean = false,
scrcpyProfileId: String = ScrcpyOptions.GLOBAL_PROFILE_ID, scrcpyProfileId: String = ScrcpyOptions.GLOBAL_PROFILE_ID,
scrcpyOptionsOverride: ((ClientOptions) -> ClientOptions)? = null,
) { ) {
val connected = connectionController.handleAdbConnected(host, port, scrcpyProfileId) val connected = connectionController.handleAdbConnected(host, port, scrcpyProfileId)
val info = connected.info val info = connected.info
@@ -551,7 +554,10 @@ internal class DeviceTabViewModel(
if (autoStartScrcpy && sessionInfo.value == null) { if (autoStartScrcpy && sessionInfo.value == null) {
runBusy(EventLogMessage.Resource(R.string.vm_start_scrcpy)) { runBusy(EventLogMessage.Resource(R.string.vm_start_scrcpy)) {
startScrcpySession(openFullscreen = autoStartScrcpy && autoEnterFullScreen) startScrcpySession(
openFullscreen = autoStartScrcpy && autoEnterFullScreen,
extraOptions = scrcpyOptionsOverride,
)
} }
} }
} }
@@ -559,13 +565,21 @@ internal class DeviceTabViewModel(
suspend fun startScrcpySession( suspend fun startScrcpySession(
openFullscreen: Boolean = false, openFullscreen: Boolean = false,
startAppOverride: String? = null, startAppOverride: String? = null,
extraOptions: ((ClientOptions) -> ClientOptions)? = null,
) { ) {
val activeBundle = resolveScrcpyBundle(connectedScrcpyProfileId.value) val activeBundle = resolveScrcpyBundle(connectedScrcpyProfileId.value)
val options = scrcpyOptions.toClientOptions(activeBundle).fix() val options = scrcpyOptions.toClientOptions(activeBundle).fix()
val resolvedOptions = startAppOverride val resolvedOptions = extraOptions?.invoke(
?.takeIf { it.isNotBlank() } startAppOverride
?.let { options.copy(startApp = it) } ?.takeIf { it.isNotBlank() }
?: options ?.let { options.copy(startApp = it) }
?: options
) ?: (
startAppOverride
?.takeIf { it.isNotBlank() }
?.let { options.copy(startApp = it) }
?: options
)
val session = scrcpy.start(resolvedOptions) val session = scrcpy.start(resolvedOptions)
_pendingScrollToPreview.value = resolvedOptions.video && resolvedOptions.videoPlayback _pendingScrollToPreview.value = resolvedOptions.video && resolvedOptions.videoPlayback
@@ -786,6 +800,52 @@ internal class DeviceTabViewModel(
} }
} }
/**
* 处理命令行启动参数:连接到指定设备并自动启动 scrcpy。
* 从 AppRuntime.adbLaunchParams 读取参数,用后即清。
*/
fun processLaunchParams() {
val params = AppRuntime.adbLaunchParams ?: return
AppRuntime.adbLaunchParams = null // consume
runAdbConnect(
label = EventLogMessage.Raw("Auto-connect"),
) {
disconnectCurrentTargetBeforeConnecting(params.host, params.port)
try {
connectWithTimeout(params.host, params.port)
handleAdbConnected(
host = params.host,
port = params.port,
autoStartScrcpy = true,
autoEnterFullScreen = params.fullscreen,
scrcpyOptionsOverride = { options ->
options.copy(
maxSize =
if (params.maxSize > 0) params.maxSize.toUShort() else options.maxSize,
videoBitRate =
if (params.videoBitRate > 0) params.videoBitRate else options.videoBitRate,
maxFps = params.maxFps.ifBlank { options.maxFps },
showTouches = params.showTouches,
fullscreen = params.fullscreen,
control = params.control,
video = params.video,
audio = params.audio,
turnScreenOff = params.turnScreenOff,
killAdbOnClose = params.killAdbOnClose,
startApp = params.startApp.ifBlank { options.startApp },
)
},
)
connectionController.updateQuickConnected(false)
} catch (error: Exception) {
connectionController.markConnectionFailed(error)
logEvent(R.string.vm_adb_connection_failed, level = Log.ERROR, error = error)
AppRuntime.snackbar(R.string.vm_adb_connection_failed)
}
}
}
fun onDisconnectCurrent(target: ConnectionTarget?) { fun onDisconnectCurrent(target: ConnectionTarget?) {
runAdbConnect(EventLogMessage.Resource(R.string.vm_disconnect_adb)) { runAdbConnect(EventLogMessage.Resource(R.string.vm_disconnect_adb)) {
target?.let { target?.let {

View File

@@ -3,6 +3,7 @@ package io.github.miuzarte.scrcpyforandroid.services
import android.content.Context import android.content.Context
import androidx.annotation.StringRes import androidx.annotation.StringRes
import io.github.miuzarte.scrcpyforandroid.models.ConnectionTarget import io.github.miuzarte.scrcpyforandroid.models.ConnectionTarget
import io.github.miuzarte.scrcpyforandroid.models.AdbLaunchParams
import io.github.miuzarte.scrcpyforandroid.nativecore.AdbMdnsDiscoverer import io.github.miuzarte.scrcpyforandroid.nativecore.AdbMdnsDiscoverer
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -28,6 +29,7 @@ object AppRuntime {
var scrcpy: Scrcpy? = null var scrcpy: Scrcpy? = null
var currentConnectionTarget: ConnectionTarget? = null var currentConnectionTarget: ConnectionTarget? = null
var currentConnectedDevice: ConnectedDeviceInfo? = null var currentConnectedDevice: ConnectedDeviceInfo? = null
var adbLaunchParams: AdbLaunchParams? = null
private val snackbarHostStateLock = Any() private val snackbarHostStateLock = Any()
private val snackbarHostStateStack = mutableListOf<SnackbarHostState>() private val snackbarHostStateStack = mutableListOf<SnackbarHostState>()