From 46ab8f66c4f4a29c33631c596563d62ce0a18387 Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Fri, 2 Oct 2026 12:08:11 +0000 Subject: [PATCH] open now playing when the media notification is tapped --- .../main/kotlin/com/subtracks/MainActivity.kt | 17 +++++++++- .../com/subtracks/playback/PlaybackService.kt | 3 +- .../main/kotlin/com/subtracks/ui/AppRoot.kt | 22 +++++++++++-- .../com/subtracks/ui/NowPlayingRequestTest.kt | 33 +++++++++++++++++++ 4 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 app/src/test/kotlin/com/subtracks/ui/NowPlayingRequestTest.kt diff --git a/app/src/main/kotlin/com/subtracks/MainActivity.kt b/app/src/main/kotlin/com/subtracks/MainActivity.kt index 5b933f52..3492a808 100644 --- a/app/src/main/kotlin/com/subtracks/MainActivity.kt +++ b/app/src/main/kotlin/com/subtracks/MainActivity.kt @@ -1,6 +1,7 @@ package com.subtracks import android.Manifest +import android.content.Intent import android.content.pm.PackageManager import android.os.Build import android.os.Bundle @@ -8,11 +9,14 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.mutableIntStateOf import androidx.core.content.ContextCompat import com.subtracks.ui.SubtracksRoot import com.subtracks.ui.theme.SubtracksTheme class MainActivity : ComponentActivity() { + private val nowPlayingRequest = mutableIntStateOf(0) + private val requestNotifications = registerForActivityResult(ActivityResultContracts.RequestPermission()) { } @@ -20,13 +24,20 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) requestNotificationPermission() enableEdgeToEdge() + if (intent?.action == ACTION_OPEN_NOW_PLAYING) nowPlayingRequest.intValue++ setContent { SubtracksTheme { - SubtracksRoot() + SubtracksRoot(nowPlayingRequest = nowPlayingRequest.intValue) } } } + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + setIntent(intent) + if (intent.action == ACTION_OPEN_NOW_PLAYING) nowPlayingRequest.intValue++ + } + private fun requestNotificationPermission() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return val granted = @@ -34,4 +45,8 @@ class MainActivity : ComponentActivity() { PackageManager.PERMISSION_GRANTED if (!granted) requestNotifications.launch(Manifest.permission.POST_NOTIFICATIONS) } + + companion object { + const val ACTION_OPEN_NOW_PLAYING = "com.subtracks.action.OPEN_NOW_PLAYING" + } } diff --git a/app/src/main/kotlin/com/subtracks/playback/PlaybackService.kt b/app/src/main/kotlin/com/subtracks/playback/PlaybackService.kt index 0b1ec74d..76da6ba3 100644 --- a/app/src/main/kotlin/com/subtracks/playback/PlaybackService.kt +++ b/app/src/main/kotlin/com/subtracks/playback/PlaybackService.kt @@ -129,7 +129,8 @@ class PlaybackService : MediaSessionService() { PendingIntent.getActivity( this, 0, - Intent(this, MainActivity::class.java), + Intent(this, MainActivity::class.java) + .setAction(MainActivity.ACTION_OPEN_NOW_PLAYING), PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, ), ).setBitmapLoader(loader) diff --git a/app/src/main/kotlin/com/subtracks/ui/AppRoot.kt b/app/src/main/kotlin/com/subtracks/ui/AppRoot.kt index d7841bba..fa252080 100644 --- a/app/src/main/kotlin/com/subtracks/ui/AppRoot.kt +++ b/app/src/main/kotlin/com/subtracks/ui/AppRoot.kt @@ -166,6 +166,12 @@ internal fun resetOnSourceSwitch( if (route in DETAIL_ROUTES) popDetail() } +internal fun shouldOpenNowPlaying( + request: Int, + handled: Int, + playerVisible: Boolean, +): Boolean = playerVisible && request > handled + internal data class BackStackKey( val route: String?, val argument: String?, @@ -181,7 +187,10 @@ internal fun focusPops( } @Composable -fun SubtracksRoot(root: RootViewModel = koinViewModel()) { +fun SubtracksRoot( + root: RootViewModel = koinViewModel(), + nowPlayingRequest: Int = 0, +) { when (val hasSource = root.hasSource.collectAsStateWithLifecycle().value) { null -> { LoadingState() @@ -195,14 +204,14 @@ fun SubtracksRoot(root: RootViewModel = koinViewModel()) { val playbackController = koinInject() LaunchedEffect(Unit) { playbackController.connect() } val playbackReady by playbackController.ready.collectAsStateWithLifecycle() - if (playbackReady) MainNavigation() else LoadingState() + if (playbackReady) MainNavigation(nowPlayingRequest) else LoadingState() } } } @OptIn(ExperimentalLayoutApi::class) @Composable -private fun MainNavigation() { +private fun MainNavigation(nowPlayingRequest: Int) { val navController = rememberNavController() val currentRoute = navController @@ -218,6 +227,7 @@ private fun MainNavigation() { val downloads by downloadRepository.states().collectAsStateWithLifecycle() var showingQueue by rememberSaveable { mutableStateOf(false) } var nowPlayingOpen by rememberSaveable { mutableStateOf(false) } + var handledNowPlayingRequest by rememberSaveable { mutableStateOf(0) } var nowPlayingProgress by remember { mutableFloatStateOf(0f) } var miniPlayerTopPx by remember { mutableFloatStateOf(0f) } var nowPlayingFadeOut by remember { mutableStateOf(false) } @@ -302,6 +312,12 @@ private fun MainNavigation() { nowPlayingProgress = 0f } } + LaunchedEffect(nowPlayingRequest, playerVisible) { + if (shouldOpenNowPlaying(nowPlayingRequest, handledNowPlayingRequest, playerVisible)) { + handledNowPlayingRequest = nowPlayingRequest + settleNowPlaying(true) + } + } val density = LocalDensity.current val navBarInset = with(density) { WindowInsets.navigationBars.getBottom(this).toDp() } val keyboardUp = WindowInsets.imeAnimationTarget.getBottom(density) > 0 diff --git a/app/src/test/kotlin/com/subtracks/ui/NowPlayingRequestTest.kt b/app/src/test/kotlin/com/subtracks/ui/NowPlayingRequestTest.kt new file mode 100644 index 00000000..4812e788 --- /dev/null +++ b/app/src/test/kotlin/com/subtracks/ui/NowPlayingRequestTest.kt @@ -0,0 +1,33 @@ +package com.subtracks.ui + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class NowPlayingRequestTest { + @Test + fun aNotificationWithNothingPlayingDoesNotOpen() { + assertFalse(shouldOpenNowPlaying(request = 1, handled = 0, playerVisible = false)) + } + + @Test + fun aNotificationWithSomethingPlayingOpens() { + assertTrue(shouldOpenNowPlaying(request = 1, handled = 0, playerVisible = true)) + } + + @Test + fun anAlreadyHandledRequestDoesNotReopen() { + assertFalse(shouldOpenNowPlaying(request = 1, handled = 1, playerVisible = true)) + } + + @Test + fun aNewerRequestOpensAgain() { + assertTrue(shouldOpenNowPlaying(request = 2, handled = 1, playerVisible = true)) + } + + @Test + fun anOldRequestWaitsUntilSomethingIsPlaying() { + assertFalse(shouldOpenNowPlaying(request = 1, handled = 0, playerVisible = false)) + assertTrue(shouldOpenNowPlaying(request = 1, handled = 0, playerVisible = true)) + } +}