address review: keep the notification tap when the activity is recreated

This commit is contained in:
austinried
2026-10-04 00:30:07 +00:00
parent b3cad97869
commit 0cd6f30c88
6 changed files with 63 additions and 50 deletions
@@ -9,6 +9,7 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.subtracks.ui.NowPlayingLauncher import com.subtracks.ui.NowPlayingLauncher
import com.subtracks.ui.SubtracksRoot import com.subtracks.ui.SubtracksRoot
@@ -16,6 +17,7 @@ import com.subtracks.ui.theme.SubtracksTheme
import org.koin.core.context.GlobalContext import org.koin.core.context.GlobalContext
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private val launchViewModel: NowPlayingLaunchViewModel by viewModels()
private val nowPlayingLauncher: NowPlayingLauncher by lazy { GlobalContext.get().get() } private val nowPlayingLauncher: NowPlayingLauncher by lazy { GlobalContext.get().get() }
private val requestNotifications = private val requestNotifications =
@@ -25,9 +27,7 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
requestNotificationPermission() requestNotificationPermission()
enableEdgeToEdge() enableEdgeToEdge()
if (shouldRecordNowPlayingLaunch(intent?.action, restoringState = savedInstanceState != null)) { if (launchViewModel.claimLaunch(intent?.action)) nowPlayingLauncher.request()
nowPlayingLauncher.request()
}
setContent { setContent {
SubtracksTheme { SubtracksTheme {
SubtracksRoot() SubtracksRoot()
@@ -38,7 +38,10 @@ class MainActivity : ComponentActivity() {
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent) super.onNewIntent(intent)
setIntent(intent) setIntent(intent)
if (intent.action == ACTION_OPEN_NOW_PLAYING) nowPlayingLauncher.request() if (intent.action == ACTION_OPEN_NOW_PLAYING) {
launchViewModel.markHandled()
nowPlayingLauncher.request()
}
} }
private fun requestNotificationPermission() { private fun requestNotificationPermission() {
@@ -53,8 +56,3 @@ class MainActivity : ComponentActivity() {
const val ACTION_OPEN_NOW_PLAYING = "com.subtracks.action.OPEN_NOW_PLAYING" const val ACTION_OPEN_NOW_PLAYING = "com.subtracks.action.OPEN_NOW_PLAYING"
} }
} }
internal fun shouldRecordNowPlayingLaunch(
action: String?,
restoringState: Boolean,
): Boolean = action == MainActivity.ACTION_OPEN_NOW_PLAYING && !restoringState
@@ -0,0 +1,17 @@
package com.subtracks
import androidx.lifecycle.ViewModel
class NowPlayingLaunchViewModel : ViewModel() {
private var launchHandled = false
fun claimLaunch(action: String?): Boolean {
if (action != MainActivity.ACTION_OPEN_NOW_PLAYING || launchHandled) return false
launchHandled = true
return true
}
fun markHandled() {
launchHandled = true
}
}
@@ -301,11 +301,10 @@ private fun MainNavigation() {
settleJob?.cancel() settleJob?.cancel()
nowPlayingOpen = false nowPlayingOpen = false
nowPlayingProgress = 0f nowPlayingProgress = 0f
return@LaunchedEffect
} }
}
LaunchedEffect(playerVisible) {
if (!playerVisible) return@LaunchedEffect
nowPlayingLauncher.openNowPlaying.collect { nowPlayingLauncher.openNowPlaying.collect {
showingQueue = false
if (navController.currentDestination?.route in DETAIL_ROUTES) { if (navController.currentDestination?.route in DETAIL_ROUTES) {
navController.popBackStack(Routes.LIBRARY, inclusive = false) navController.popBackStack(Routes.LIBRARY, inclusive = false)
} }
@@ -4,7 +4,7 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
class NowPlayingLauncher { internal class NowPlayingLauncher {
private val requests = Channel<Unit>(Channel.CONFLATED) private val requests = Channel<Unit>(Channel.CONFLATED)
val openNowPlaying: Flow<Unit> = requests.receiveAsFlow() val openNowPlaying: Flow<Unit> = requests.receiveAsFlow()
@@ -1,37 +0,0 @@
package com.subtracks
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class NowPlayingLaunchTest {
@Test
fun aFreshLaunchFromTheNotificationIsRecorded() {
assertTrue(
shouldRecordNowPlayingLaunch(
action = MainActivity.ACTION_OPEN_NOW_PLAYING,
restoringState = false,
),
)
}
@Test
fun recreatingTheActivityDoesNotReRecordTheLaunch() {
assertFalse(
shouldRecordNowPlayingLaunch(
action = MainActivity.ACTION_OPEN_NOW_PLAYING,
restoringState = true,
),
)
}
@Test
fun aNormalLaunchIsNotRecorded() {
assertFalse(shouldRecordNowPlayingLaunch(action = "android.intent.action.MAIN", restoringState = false))
}
@Test
fun aLaunchWithoutAnActionIsNotRecorded() {
assertFalse(shouldRecordNowPlayingLaunch(action = null, restoringState = false))
}
}
@@ -0,0 +1,36 @@
package com.subtracks
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class NowPlayingLaunchViewModelTest {
@Test
fun aFreshLaunchFromTheNotificationIsClaimed() {
assertTrue(NowPlayingLaunchViewModel().claimLaunch(MainActivity.ACTION_OPEN_NOW_PLAYING))
}
@Test
fun recreatingTheActivityDoesNotReClaimTheLaunch() {
val viewModel = NowPlayingLaunchViewModel()
assertTrue(viewModel.claimLaunch(MainActivity.ACTION_OPEN_NOW_PLAYING))
assertFalse(viewModel.claimLaunch(MainActivity.ACTION_OPEN_NOW_PLAYING))
}
@Test
fun aWarmTapMarksTheLaunchHandled() {
val viewModel = NowPlayingLaunchViewModel()
viewModel.markHandled()
assertFalse(viewModel.claimLaunch(MainActivity.ACTION_OPEN_NOW_PLAYING))
}
@Test
fun aNormalLaunchIsNotClaimed() {
assertFalse(NowPlayingLaunchViewModel().claimLaunch("android.intent.action.MAIN"))
}
@Test
fun aLaunchWithoutAnActionIsNotClaimed() {
assertFalse(NowPlayingLaunchViewModel().claimLaunch(null))
}
}