diff --git a/README.md b/README.md index d2d0faf..23348be 100644 --- a/README.md +++ b/README.md @@ -23,15 +23,16 @@ - 利用 mDNS 服务实现自动连接启用无线调试的设备、自动发现等待配对设备的IP与端口 - 自动横竖屏切换(算吗 -## 已知问题 / TODO +## 已知问题 - 退出全屏时的横竖屏状态可能不对,断开 scrcpy 重连就好 -- 如果受控机在锁屏时处理网络连接较慢,会导致应用界面无响应过长时间被系统杀掉(点名你米) - 虚拟按键的截图实现方式为发送 `keycode 120`,安卓官方([keycodes.h#349](https://android.googlesource.com/platform/frameworks/native/+/master/include/android/keycodes.h#349))的定义为 `System Request / Print Screen key.`,不同的厂商有不同的实现,在某些类原生(`AxionOS`) 上的行为是软重启 -- I18N -- More: [TODO.md](TODO.md) + +## [TODO](TODO.md) + +\> [TODO.md](TODO.md) ## 建议搭配模块 diff --git a/TODO.md b/TODO.md index 5581187..da2609f 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,8 @@ # TODO +- miuix v0.9 +- 在线更新检查 + ## PARAMS - orientation locking @@ -18,3 +21,4 @@ - [音频延迟](https://developer.android.com/ndk/guides/audio/audio-latency) - 横屏布局 - 原生悬浮窗 [Petterpx/FloatingX](https://github.com/Petterpx/FloatingX) +- I18N 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 e54427d..e6b0862 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 @@ -16,7 +16,6 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -36,7 +35,6 @@ 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.Codec import io.github.miuzarte.scrcpyforandroid.services.EventLogger import io.github.miuzarte.scrcpyforandroid.services.EventLogger.logEvent import io.github.miuzarte.scrcpyforandroid.services.SnackbarController @@ -97,7 +95,7 @@ fun DeviceTabScreen( onOpenVirtualButtonOrder: () -> Unit, onOpenReorderDevices: () -> Unit, onOpenAdvancedPage: () -> Unit, - onOpenFullscreenPage: (Scrcpy.Session.SessionInfo) -> Unit, + onOpenFullscreenPage: () -> Unit, ) { var showThreePointMenu by rememberSaveable { mutableStateOf(false) } @@ -159,7 +157,7 @@ fun DeviceTabPage( snackbar: SnackbarController, scrollBehavior: ScrollBehavior, onOpenAdvancedPage: () -> Unit, - onOpenFullscreenPage: (Scrcpy.Session.SessionInfo) -> Unit, + onOpenFullscreenPage: () -> Unit, ) { val context = LocalContext.current val scope = rememberCoroutineScope() @@ -241,39 +239,9 @@ fun DeviceTabPage( var adbConnected by rememberSaveable { mutableStateOf(false) } var isQuickConnected by rememberSaveable { mutableStateOf(false) } var currentTargetHost by rememberSaveable { mutableStateOf("") } - var currentTargetPort by rememberSaveable { mutableIntStateOf(Defaults.ADB_PORT) } + var currentTargetPort by rememberSaveable { mutableStateOf(Defaults.ADB_PORT) } var connectedDeviceLabel by rememberSaveable { mutableStateOf("未连接") } - var sessionInfoWidth by rememberSaveable { mutableIntStateOf(0) } - var sessionInfoHeight by rememberSaveable { mutableIntStateOf(0) } - var sessionInfoDeviceName by rememberSaveable { mutableStateOf("") } - var sessionInfoCodec by rememberSaveable { mutableStateOf(null) } - var sessionInfoControlEnabled by rememberSaveable { mutableStateOf(false) } - var sessionInfo by remember { - mutableStateOf(null) - } - LaunchedEffect( - sessionInfoWidth, - sessionInfoHeight, - sessionInfoDeviceName, - sessionInfoCodec, - sessionInfoControlEnabled - ) { - sessionInfo = if (sessionInfoDeviceName.isNotBlank()) { - Scrcpy.Session.SessionInfo( - width = sessionInfoWidth, - height = sessionInfoHeight, - deviceName = sessionInfoDeviceName, - codecId = 0, - codec = sessionInfoCodec, - audioCodecId = 0, - controlEnabled = sessionInfoControlEnabled, - host = currentTargetHost, - port = currentTargetPort, - ) - } else { - null - } - } + val sessionInfo by scrcpy.currentSessionState.collectAsState() var previewControlsVisible by rememberSaveable { mutableStateOf(false) } var editingDeviceId by rememberSaveable { mutableStateOf(null) } var activeDeviceActionId by rememberSaveable { mutableStateOf(null) } @@ -349,7 +317,6 @@ fun DeviceTabPage( currentTargetPort = Defaults.ADB_PORT audioForwardingSupported = true cameraMirroringSupported = true - sessionInfo = null statusLine = "未连接" connectedDeviceLabel = "未连接" clearQuickOnlineForTarget?.let { target -> @@ -702,32 +669,6 @@ fun DeviceTabPage( } } - DisposableEffect(nativeCore) { - val listener: (Int, Int) -> Unit = { width, height -> - sessionInfo = sessionInfo?.copy(width = width, height = height) - } - nativeCore.addVideoSizeListener(listener) - onDispose { - nativeCore.removeVideoSizeListener(listener) - } - } - - LaunchedEffect(sessionInfo) { - if (sessionInfo != null) { - sessionInfoWidth = sessionInfo?.width ?: 0 - sessionInfoHeight = sessionInfo?.height ?: 0 - sessionInfoDeviceName = sessionInfo?.deviceName.orEmpty() - sessionInfoCodec = sessionInfo?.codec - sessionInfoControlEnabled = sessionInfo?.controlEnabled == true - } else { - sessionInfoWidth = 0 - sessionInfoHeight = 0 - sessionInfoDeviceName = "" - sessionInfoCodec = null - sessionInfoControlEnabled = false - } - } - fun sendVirtualButtonAction(action: VirtualButtonAction) { val keycode = action.keycode ?: return runBusy("发送 ${action.title}") { @@ -942,10 +883,6 @@ fun DeviceTabPage( if (options.disableScreensaver) { setKeepScreenOn(true) } - sessionInfo = session.copy( - host = currentTargetHost, - port = currentTargetPort - ) statusLine = "scrcpy 运行中" @SuppressLint("DefaultLocale") val videoDetail = @@ -972,7 +909,6 @@ fun DeviceTabPage( runBusy("停止 scrcpy") { scrcpy.stop() setKeepScreenOn(false) - sessionInfo = null statusLine = "${currentTarget!!.host}:${currentTarget.port}" logEvent("scrcpy 已停止") snackbar.show("scrcpy 已停止") @@ -1013,8 +949,9 @@ fun DeviceTabPage( previewControlsVisible = !previewControlsVisible }, onOpenFullscreen = { - val info = sessionInfo ?: return@PreviewCard - onOpenFullscreenPage(info) + if (sessionInfo != null) { + onOpenFullscreenPage() + } }, ) } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlScreen.kt index ac35d74..60e8d9d 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlScreen.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlScreen.kt @@ -43,14 +43,12 @@ import top.yukonga.miuix.kmp.basic.Scaffold @Composable fun FullscreenControlScreen( onBack: () -> Unit, - session: Scrcpy.Session.SessionInfo, + scrcpy: Scrcpy, nativeCore: NativeCoreFacade, onVideoSizeChanged: (width: Int, height: Int) -> Unit, ) { - // Disable predictive back handler temporarily to avoid decoding issues. BackHandler(enabled = true, onBack = onBack) - val context = LocalContext.current val haptics = rememberAppHaptics() @@ -58,6 +56,7 @@ fun FullscreenControlScreen( val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) } val activity = remember(context) { context as? Activity } + val currentSession by scrcpy.currentSessionState.collectAsState() val asBundleShared by appSettings.bundleState.collectAsState() val asBundleSharedLatest by rememberUpdatedState(asBundleShared) @@ -119,14 +118,9 @@ fun FullscreenControlScreen( } } - DisposableEffect(nativeCore) { - val listener: (Int, Int) -> Unit = { w, h -> - onVideoSizeChanged(w, h) - } - nativeCore.addVideoSizeListener(listener) - onDispose { - nativeCore.removeVideoSizeListener(listener) - } + LaunchedEffect(currentSession?.width, currentSession?.height) { + val session = currentSession ?: return@LaunchedEffect + onVideoSizeChanged(session.width, session.height) } DisposableEffect(nativeCore) { @@ -156,6 +150,7 @@ fun FullscreenControlScreen( .fillMaxSize() .padding(contentPadding), ) { + val session = currentSession ?: return@Box FullscreenControlScreen( session = session, nativeCore = nativeCore, diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/MainScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/MainScreen.kt index 82753dc..a8bc02a 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/MainScreen.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/MainScreen.kt @@ -82,7 +82,7 @@ sealed interface RootScreen : NavKey { data object Home : RootScreen data object Advanced : RootScreen data object VirtualButtonOrder : RootScreen - data class Fullscreen(val session: Scrcpy.Session.SessionInfo) : RootScreen + data object Fullscreen : RootScreen } @Composable @@ -215,6 +215,7 @@ fun MainScreen() { serverRemotePath = serverRemotePath, ) } + val currentSession by scrcpy.currentSessionState.collectAsState() fun handleBackNavigation() { if (rootBackStack.size > 1) { @@ -247,6 +248,11 @@ fun MainScreen() { } } + var showReorderDevices by rememberSaveable { mutableStateOf(false) } + var fullscreenOrientation by rememberSaveable { + mutableIntStateOf(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) + } + val canNavigateBack = rootBackStack.size > 1 || pagerState.currentPage != MainBottomTabDestination.Device.ordinal @@ -265,11 +271,6 @@ fun MainScreen() { } } - var showReorderDevices by rememberSaveable { mutableStateOf(false) } - var fullscreenOrientation by rememberSaveable { - mutableIntStateOf(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) - } - // Fullscreen route can force orientation based on stream ratio; all other routes are portrait. LaunchedEffect(activity, currentRootScreen, fullscreenOrientation) { val targetOrientation = when (currentRootScreen) { @@ -279,6 +280,28 @@ fun MainScreen() { activity?.requestedOrientation = targetOrientation } + DisposableEffect(nativeCore, scrcpy) { + val listener: (Int, Int) -> Unit = { width, height -> + scrcpy.updateCurrentSessionSize(width, height) + } + nativeCore.addVideoSizeListener(listener) + onDispose { + nativeCore.removeVideoSizeListener(listener) + } + } + + LaunchedEffect(currentRootScreen, currentSession?.width, currentSession?.height) { + if (currentRootScreen is RootScreen.Fullscreen) { + val session = currentSession ?: return@LaunchedEffect + fullscreenOrientation = + if (session.width >= session.height) { + ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + } else { + ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + } + } + } + LaunchedEffect(asBundle.adbKeyName) { adbService.keyName = asBundle.adbKeyName.ifBlank { AppSettings.ADB_KEY_NAME.defaultValue } } @@ -341,16 +364,16 @@ fun MainScreen() { onOpenVirtualButtonOrder = { rootBackStack.add(RootScreen.VirtualButtonOrder) }, onOpenReorderDevices = { showReorderDevices = true }, onOpenAdvancedPage = { rootBackStack.add(RootScreen.Advanced) }, - onOpenFullscreenPage = { session -> - fullscreenOrientation = - if (session.width >= session.height) { - ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE - } else { - ActivityInfo.SCREEN_ORIENTATION_PORTRAIT - } - rootBackStack.add( - RootScreen.Fullscreen(session), - ) + onOpenFullscreenPage = { + currentSession?.let { session -> + fullscreenOrientation = + if (session.width >= session.height) { + ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + } else { + ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + } + rootBackStack.add(RootScreen.Fullscreen) + } }, ) @@ -396,10 +419,10 @@ fun MainScreen() { ) } - entry { screen -> + entry(RootScreen.Fullscreen) { FullscreenControlScreen( onBack = ::popRoot, - session = screen.session, + scrcpy = scrcpy, nativeCore = nativeCore, onVideoSizeChanged = { width, height -> fullscreenOrientation = 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 f8ec94b..22c50b8 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 @@ -16,6 +16,9 @@ import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.EncoderType import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.ListOptions import io.github.miuzarte.scrcpyforandroid.services.EventLogger.logEvent import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext @@ -58,8 +61,8 @@ class Scrcpy( private val session = Session(adbService) private val nativeCore: NativeCoreFacade = NativeCoreFacade.get(appContext) - @Volatile - private var currentSession: Session.SessionInfo? = null + private val _currentSessionState = MutableStateFlow(null) + val currentSessionState: StateFlow = _currentSessionState.asStateFlow() @Volatile private var isRunning: Boolean = false @@ -136,7 +139,7 @@ class Scrcpy( } // Create session info - currentSession = info + _currentSessionState.value = info isRunning = true // Setup video consumer (notify NativeCoreFacade to setup decoders) @@ -175,7 +178,7 @@ class Scrcpy( } catch (e: Exception) { Log.e(TAG, "start(): Failed to start scrcpy session", e) isRunning = false - currentSession = null + _currentSessionState.value = null throw e } } @@ -196,7 +199,7 @@ class Scrcpy( audioPlayer?.release() audioPlayer = null isRunning = false - currentSession = null + _currentSessionState.value = null Log.i(TAG, "stop(): Session stopped successfully") true } catch (e: Exception) { @@ -212,7 +215,13 @@ class Scrcpy( fun isStarted(): Boolean = isRunning && session.isStarted() - fun getCurrentSession(): Session.SessionInfo? = currentSession + fun getCurrentSession(): Session.SessionInfo? = currentSessionState.value + + fun updateCurrentSessionSize(width: Int, height: Int) { + val current = _currentSessionState.value ?: return + if (current.width == width && current.height == height) return + _currentSessionState.value = current.copy(width = width, height = height) + } sealed class ListResult { data class Encoders(