diff --git a/ScrcpyForAndroid.code-workspace b/ScrcpyForAndroid.code-workspace index 5ed02f7..5a1c1c5 100644 --- a/ScrcpyForAndroid.code-workspace +++ b/ScrcpyForAndroid.code-workspace @@ -20,6 +20,10 @@ "name": "shizuku", "path": "../shizuku" }, + { + "name": "moonlight-android", + "path": "../moonlight-android" + }, { "name": "KernelSU", "path": "../KernelSU" @@ -27,6 +31,9 @@ { "name": "adblib", "path": "../adblib" + }, + { + "path": "../socialite" } ], } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 784e834..30a4cbe 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,10 +19,6 @@ android:supportsRtl="true" android:theme="@style/Theme.ScrcpyForAndroid"> - - = _pipModeState val pipStopAction: RemoteAction by lazy { - val intent = Intent(this, PictureInPictureActionReceiver::class.java) - .apply { - action = PictureInPictureActionReceiver.ACTION_STOP_SCRCPY - `package` = packageName - } - val pendingIntent = PendingIntent.getBroadcast( - this, - 1, - intent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, - ) RemoteAction( Icon.createWithResource(this, android.R.drawable.ic_menu_close_clear_cancel), "停止投屏", "停止投屏", - pendingIntent, + PictureInPictureActionReceiver.createPendingIntent(this), ) } @@ -53,6 +40,8 @@ class StreamActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + registerPipActionReceiver() + // 声明要画中画 basicPip.setEnabled(true) @@ -91,6 +80,11 @@ class StreamActivity : ComponentActivity() { basicPip.setPictureInPictureParams(params) } + override fun onDestroy() { + unregisterPipActionReceiver() + super.onDestroy() + } + /* // 回到全屏也会停止, 暂时不做 override fun onDestroy() { @@ -110,22 +104,37 @@ class StreamActivity : ComponentActivity() { //+ onPictureInPictureUiStateChanged //- onUserLeaveHint - @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + // @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) override fun onPictureInPictureUiStateChanged(pipState: PictureInPictureUiState) { super.onPictureInPictureUiStateChanged(pipState) + _pipModeState.value = true + + /* when { // 进入画中画 - pipState.isTransitioningToPip -> if (!_pipModeState.value) { - _pipModeState.value = true - VideoOutputTargetState.set(VideoOutputTarget.PICTURE_IN_PICTURE) - } + pipState.isTransitioningToPip -> {} // 收进边缘 - pipState.isStashed -> if (!_pipModeState.value) { - _pipModeState.value = true - VideoOutputTargetState.set(VideoOutputTarget.PICTURE_IN_PICTURE) - } + pipState.isStashed -> {} } + */ + } + + private fun registerPipActionReceiver() { + if (isPipActionReceiverRegistered) return + ContextCompat.registerReceiver( + this, + pipActionReceiver, + PictureInPictureActionReceiver.createIntentFilter(), + ContextCompat.RECEIVER_NOT_EXPORTED, + ) + isPipActionReceiverRegistered = true + } + + private fun unregisterPipActionReceiver() { + if (!isPipActionReceiverRegistered) return + unregisterReceiver(pipActionReceiver) + isPipActionReceiverRegistered = false } companion object { 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 446bf8f..366d366 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,6 +16,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.mutableStateOf import androidx.compose.runtime.remember @@ -86,6 +87,8 @@ private const val ADB_KEEPALIVE_TIMEOUT_MS = 1_500L private const val ADB_AUTO_RECONNECT_DISCOVER_TIMEOUT_MS = 2_000L private const val ADB_AUTO_RECONNECT_RETRY_INTERVAL_MS = 2_000L 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 @Composable fun DeviceTabScreen( @@ -241,6 +244,11 @@ fun DeviceTabPage( var cameraMirroringSupported by rememberSaveable { mutableStateOf(true) } var pendingScrollToPreview by rememberSaveable { mutableStateOf(false) } val listState = rememberLazyListState() + val isPreviewCardVisible by remember(listState) { + derivedStateOf { + listState.layoutInfo.visibleItemsInfo.any { it.key == PREVIEW_CARD_ITEM_KEY } + } + } val currentTarget = if (currentTargetHost.isNotBlank()) @@ -557,13 +565,10 @@ fun DeviceTabPage( snackbar.show("scrcpy 已启动") } - LaunchedEffect(pendingScrollToPreview, sessionInfo) { + LaunchedEffect(pendingScrollToPreview, isPreviewCardVisible) { if (!pendingScrollToPreview) return@LaunchedEffect - val session = sessionInfo ?: return@LaunchedEffect - if (session.width <= 0 || session.height <= 0) return@LaunchedEffect - // status/device list/scrcpy panel are above the preview card - listState.animateScrollToItem(index = 3) - pendingScrollToPreview = false + if (isPreviewCardVisible) return@LaunchedEffect + listState.animateScrollToItem(PREVIEW_CARD_ITEM_INDEX) } suspend fun handleAdbConnected(host: String, port: Int) { @@ -952,8 +957,9 @@ fun DeviceTabPage( sessionInfo!!.width > 0 && sessionInfo!!.height > 0 ) { - item { + item(key = PREVIEW_CARD_ITEM_KEY) { PreviewCard( + modifier = Modifier, sessionInfo = sessionInfo, previewHeightDp = asBundle.devicePreviewCardHeightDp.coerceAtLeast(120), controlsVisible = previewControlsVisible, @@ -963,6 +969,8 @@ fun DeviceTabPage( onOpenFullscreen = { context.startActivity(StreamActivity.createIntent(context)) }, + autoBringIntoView = pendingScrollToPreview, + onAutoBringIntoViewConsumed = { pendingScrollToPreview = false }, ) } 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 5df23f0..be81c34 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 @@ -333,7 +333,6 @@ fun FullscreenControlPage( ) }, session = session, - target = if (interactive) VideoOutputTarget.FULLSCREEN else VideoOutputTarget.PICTURE_IN_PICTURE, ) } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/StreamScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/StreamScreen.kt index a4e666c..3b3d7ce 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/StreamScreen.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/StreamScreen.kt @@ -45,12 +45,6 @@ fun StreamScreen(activity: StreamActivity) { } DisposableEffect(isInPip) { - VideoOutputTargetState.set( - if (isInPip) - VideoOutputTarget.PICTURE_IN_PICTURE - else - VideoOutputTarget.FULLSCREEN - ) onDispose { VideoOutputTargetState.set(VideoOutputTarget.PREVIEW) } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/PictureInPictureActionReceiver.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/PictureInPictureActionReceiver.kt index 392b33f..f754af5 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/PictureInPictureActionReceiver.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/PictureInPictureActionReceiver.kt @@ -1,14 +1,16 @@ package io.github.miuzarte.scrcpyforandroid.services +import android.app.PendingIntent import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.content.IntentFilter import android.os.Build import androidx.annotation.RequiresApi import io.github.miuzarte.scrcpyforandroid.storage.Storage import kotlinx.coroutines.runBlocking -// MIUI 不进 +// not working in MIUI class PictureInPictureActionReceiver : BroadcastReceiver() { @RequiresApi(Build.VERSION_CODES.R) override fun onReceive(context: Context, intent: Intent?) { @@ -31,5 +33,18 @@ class PictureInPictureActionReceiver : BroadcastReceiver() { companion object { const val ACTION_STOP_SCRCPY = "io.github.miuzarte.scrcpyforandroid.action.STOP_SCRCPY_FROM_PIP" + private const val REQUEST_CODE_STOP_SCRCPY = 1 + + fun createIntentFilter(): IntentFilter = IntentFilter(ACTION_STOP_SCRCPY) + + fun createPendingIntent(context: Context): PendingIntent { + val intent = Intent(ACTION_STOP_SCRCPY).setPackage(context.packageName) + return PendingIntent.getBroadcast( + context, + REQUEST_CODE_STOP_SCRCPY, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + } } } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt index 4e00329..69faff0 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt @@ -23,6 +23,8 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.relocation.BringIntoViewRequester +import androidx.compose.foundation.relocation.bringIntoViewRequester import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape @@ -245,15 +247,25 @@ internal fun PairingCard( @Composable internal fun PreviewCard( + modifier: Modifier, sessionInfo: Scrcpy.Session.SessionInfo?, previewHeightDp: Int, controlsVisible: Boolean, onTapped: () -> Unit, onOpenFullscreen: () -> Unit, + autoBringIntoView: Boolean = false, + onAutoBringIntoViewConsumed: () -> Unit = {}, ) { val haptics = rememberAppHaptics() val alpha by animateFloatAsState(if (controlsVisible) 1f else 0f, label = "preview-controls") val lifecycleOwner = LocalLifecycleOwner.current + val bringIntoViewRequester = remember { BringIntoViewRequester() } + + LaunchedEffect(autoBringIntoView) { + if (!autoBringIntoView) return@LaunchedEffect + bringIntoViewRequester.bringIntoView() + onAutoBringIntoViewConsumed() + } DisposableEffect(lifecycleOwner) { val observer = LifecycleEventObserver { _, event -> @@ -275,7 +287,7 @@ internal fun PreviewCard( } } - Card { + Card(modifier = Modifier.bringIntoViewRequester(bringIntoViewRequester).then(modifier)) { Box( modifier = Modifier .fillMaxWidth() @@ -283,11 +295,11 @@ internal fun PreviewCard( .pointerInput(sessionInfo) { detectTapGestures(onTap = { onTapped() }) }, ) { BoxWithConstraints(modifier = Modifier.fillMaxSize()) { - val sessionAspect = if (sessionInfo == null || sessionInfo.height == 0) { - 16f / 9f - } else { - sessionInfo.width.toFloat() / sessionInfo.height.toFloat() - } + val sessionAspect = + if (sessionInfo == null || sessionInfo.height == 0) + 16f / 9f + else sessionInfo.width.toFloat() / sessionInfo.height.toFloat() + val containerAspect = maxWidth.value / maxHeight.value val fittedModifier = if (sessionAspect > containerAspect) { Modifier @@ -307,7 +319,6 @@ internal fun PreviewCard( ScrcpyVideoSurface( modifier = Modifier.fillMaxSize(), session = sessionInfo, - target = VideoOutputTarget.PREVIEW, ) } } @@ -762,7 +773,6 @@ private fun PairingDialog( fun ScrcpyVideoSurface( modifier: Modifier, session: Scrcpy.Session.SessionInfo?, - target: VideoOutputTarget, ) { val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) } @@ -772,37 +782,29 @@ fun ScrcpyVideoSurface( var currentSurface by remember { mutableStateOf(null) } var currentSurfaceView by remember { mutableStateOf(null) } - val currentTarget by VideoOutputTargetState.current.collectAsState() - val latestCurrentTarget by rememberUpdatedState(currentTarget) val latestSession by rememberUpdatedState(session) - val latestRequestedTarget by rememberUpdatedState(target) - LaunchedEffect(session?.width, session?.height, currentSurfaceView, target) { - val surfaceView = currentSurfaceView ?: return@LaunchedEffect - if (target == VideoOutputTarget.PICTURE_IN_PICTURE) { - // In PiP, let SurfaceView buffer follow viewport to avoid stale portrait frame crop. - surfaceView.holder.setSizeFromLayout() - return@LaunchedEffect + LaunchedEffect(session, currentSurface) { + val surface = currentSurface ?: return@LaunchedEffect + if (session != null && surface.isValid) { + NativeCoreFacade.attachVideoSurface(surface) } + } + LaunchedEffect(session?.width, session?.height, currentSurfaceView) { + val surfaceView = currentSurfaceView ?: return@LaunchedEffect val currentSession = session ?: return@LaunchedEffect + if (currentSession.width > 0 && currentSession.height > 0) { surfaceView.holder.setFixedSize(currentSession.width, currentSession.height) } } - LaunchedEffect(session, currentSurface, currentTarget, target) { - val surface = currentSurface ?: return@LaunchedEffect - if (currentTarget == target && session != null && surface.isValid) { - NativeCoreFacade.attachVideoSurface(surface) - } - } - - DisposableEffect(lifecycleOwner, session, currentSurface, currentTarget, target) { + DisposableEffect(lifecycleOwner, session, currentSurface) { val observer = LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_START) { val surface = currentSurface - if (currentTarget == target && session != null && surface != null && surface.isValid) { + if (session != null && surface != null && surface.isValid) { scope.launch { NativeCoreFacade.attachVideoSurface(surface) } @@ -820,10 +822,7 @@ fun ScrcpyVideoSurface( val surface = currentSurface if (surface != null) { taskScope.launch { - NativeCoreFacade.detachVideoSurface( - surface, - releaseDecoder = false - ) + NativeCoreFacade.detachVideoSurface(surface) } } } @@ -840,7 +839,7 @@ fun ScrcpyVideoSurface( if (!newSurface.isValid) return currentSurface = newSurface // Register immediately when surface becomes available - if (latestCurrentTarget == target && latestSession != null) { + if (latestSession != null) { scope.launch { NativeCoreFacade.attachVideoSurface(newSurface) } @@ -857,7 +856,7 @@ fun ScrcpyVideoSurface( if (!holder.surface.isValid) return val surface = holder.surface currentSurface = surface - if (latestCurrentTarget == latestRequestedTarget && latestSession != null) { + if (latestSession != null) { scope.launch { NativeCoreFacade.attachVideoSurface(surface) } @@ -868,7 +867,7 @@ fun ScrcpyVideoSurface( val surface = currentSurface if (surface != null) { taskScope.launch { - NativeCoreFacade.detachVideoSurface(surface, releaseDecoder = false) + NativeCoreFacade.detachVideoSurface(surface) } currentSurface = null } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/VideoOutputTarget.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/VideoOutputTarget.kt index 5b23c1b..7825584 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/VideoOutputTarget.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/VideoOutputTarget.kt @@ -7,8 +7,6 @@ import kotlinx.coroutines.flow.asStateFlow enum class VideoOutputTarget { NONE, PREVIEW, - FULLSCREEN, - PICTURE_IN_PICTURE, } object VideoOutputTargetState {