mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-25 05:27:42 +00:00
Prevent multiple activity instances and fix strange launch behavior.
This commit is contained in:
committed by
Cody Henthorne
parent
04c14a82be
commit
fabec719ab
@@ -1068,6 +1068,7 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout"
|
||||
android:windowSoftInputMode="stateUnchanged"
|
||||
android:resizeableActivity="true"
|
||||
android:launchMode="singleTask"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".pin.PinRestoreActivity"
|
||||
|
||||
@@ -33,17 +33,19 @@ import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
|
||||
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
||||
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole
|
||||
import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective
|
||||
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
|
||||
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.compose.AndroidFragment
|
||||
import androidx.fragment.compose.rememberFragmentState
|
||||
@@ -216,7 +218,6 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
|
||||
setContent {
|
||||
val listHostState = rememberFragmentState()
|
||||
val detailLocation by mainNavigationViewModel.detailLocationRequests.collectAsStateWithLifecycle()
|
||||
val snackbar by mainNavigationViewModel.snackbar.collectAsStateWithLifecycle()
|
||||
val mainToolbarState by toolbarViewModel.state.collectAsStateWithLifecycle()
|
||||
val megaphone by mainNavigationViewModel.megaphone.collectAsStateWithLifecycle()
|
||||
@@ -242,37 +243,10 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
val contentLayoutData = MainContentLayoutData.rememberContentLayoutData()
|
||||
|
||||
MainContainer {
|
||||
val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator<Any>(
|
||||
scaffoldDirective = calculatePaneScaffoldDirective(
|
||||
currentWindowAdaptiveInfo()
|
||||
).copy(
|
||||
maxHorizontalPartitions = if (windowSizeClass.isSplitPane()) 2 else 1,
|
||||
horizontalPartitionSpacerSize = contentLayoutData.partitionWidth,
|
||||
defaultPanePreferredWidth = contentLayoutData.rememberDefaultPanePreferredWidth(maxWidth)
|
||||
)
|
||||
)
|
||||
|
||||
if (SignalStore.internal.largeScreenUi) {
|
||||
LaunchedEffect(scaffoldNavigator.currentDestination) {
|
||||
if (scaffoldNavigator.currentDestination?.pane == ThreePaneScaffoldRole.Secondary) {
|
||||
mainNavigationViewModel.goTo(MainNavigationDetailLocation.Empty)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(detailLocation) {
|
||||
if (detailLocation is MainNavigationDetailLocation.Conversation) {
|
||||
if (SignalStore.internal.largeScreenUi) {
|
||||
scaffoldNavigator.navigateTo(ThreePaneScaffoldRole.Primary, detailLocation)
|
||||
} else {
|
||||
startActivity((detailLocation as MainNavigationDetailLocation.Conversation).intent)
|
||||
mainNavigationViewModel.goTo(MainNavigationDetailLocation.Empty)
|
||||
}
|
||||
}
|
||||
}
|
||||
val wrappedNavigator = rememberNavigator(windowSizeClass, contentLayoutData, maxWidth)
|
||||
|
||||
AppScaffold(
|
||||
navigator = scaffoldNavigator,
|
||||
navigator = wrappedNavigator,
|
||||
bottomNavContent = {
|
||||
if (isNavigationVisible) {
|
||||
Column(
|
||||
@@ -338,7 +312,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
}
|
||||
},
|
||||
detailContent = {
|
||||
when (val destination = scaffoldNavigator.currentDestination?.contentKey) {
|
||||
when (val destination = wrappedNavigator.currentDestination?.contentKey) {
|
||||
is MainNavigationDetailLocation.Conversation -> {
|
||||
val fragmentState = key(destination) { rememberFragmentState() }
|
||||
AndroidFragment(
|
||||
@@ -384,6 +358,41 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
lifecycleDisposable += vitalsViewModel.vitalsState.subscribe(this::presentVitalsState)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and wraps a scaffold navigator such that we can use it to operate with both
|
||||
* our split pane and legacy activities.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||
@Composable
|
||||
private fun rememberNavigator(
|
||||
windowSizeClass: WindowSizeClass,
|
||||
contentLayoutData: MainContentLayoutData,
|
||||
maxWidth: Dp
|
||||
): ThreePaneScaffoldNavigator<Any> {
|
||||
val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator<Any>(
|
||||
scaffoldDirective = calculatePaneScaffoldDirective(
|
||||
currentWindowAdaptiveInfo()
|
||||
).copy(
|
||||
maxHorizontalPartitions = if (windowSizeClass.isSplitPane()) 2 else 1,
|
||||
horizontalPartitionSpacerSize = contentLayoutData.partitionWidth,
|
||||
defaultPanePreferredWidth = contentLayoutData.rememberDefaultPanePreferredWidth(maxWidth)
|
||||
)
|
||||
)
|
||||
|
||||
val coroutine = rememberCoroutineScope()
|
||||
|
||||
return remember(scaffoldNavigator, coroutine) {
|
||||
mainNavigationViewModel.wrapNavigator(coroutine, scaffoldNavigator) { detailLocation ->
|
||||
when (detailLocation) {
|
||||
is MainNavigationDetailLocation.Conversation -> {
|
||||
startActivity(detailLocation.intent)
|
||||
}
|
||||
MainNavigationDetailLocation.Empty -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MainContainer(content: @Composable BoxWithConstraintsScope.() -> Unit) {
|
||||
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
|
||||
|
||||
@@ -5,9 +5,15 @@
|
||||
|
||||
package org.thoughtcrime.securesms.main
|
||||
|
||||
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
|
||||
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole
|
||||
import androidx.compose.material3.adaptive.navigation.BackNavigationBehavior
|
||||
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -26,22 +32,21 @@ import org.thoughtcrime.securesms.megaphone.Megaphones
|
||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||
import org.thoughtcrime.securesms.stories.Stories
|
||||
|
||||
class MainNavigationViewModel(initialListLocation: MainNavigationListLocation = MainNavigationListLocation.CHATS) : ViewModel() {
|
||||
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||
class MainNavigationViewModel(
|
||||
initialListLocation: MainNavigationListLocation = MainNavigationListLocation.CHATS,
|
||||
initialDetailLocation: MainNavigationDetailLocation = MainNavigationDetailLocation.Empty
|
||||
) : ViewModel() {
|
||||
private val megaphoneRepository = AppDependencies.megaphoneRepository
|
||||
|
||||
/**
|
||||
* A shared flow of detail location requests that the MainActivity will service.
|
||||
* This is immediately set back to empty after requesting a detail location to prevent duplicate launches.
|
||||
*/
|
||||
private val detailLocationRequestFlow = MutableStateFlow<MainNavigationDetailLocation>(MainNavigationDetailLocation.Empty)
|
||||
val detailLocationRequests: StateFlow<MainNavigationDetailLocation> = detailLocationRequestFlow
|
||||
private var navigator: LegacyNavigator? = null
|
||||
|
||||
/**
|
||||
* The latest detail location that has been requested, for consumption by other components.
|
||||
*/
|
||||
private val detailLocationFlow = MutableStateFlow<MainNavigationDetailLocation>(MainNavigationDetailLocation.Empty)
|
||||
val detailLocation: StateFlow<MainNavigationDetailLocation> = detailLocationFlow
|
||||
val detailLocationObservable: Observable<MainNavigationDetailLocation> = detailLocationFlow.asObservable()
|
||||
private val internalDetailLocation = MutableStateFlow(initialDetailLocation)
|
||||
val detailLocation: StateFlow<MainNavigationDetailLocation> = internalDetailLocation
|
||||
val detailLocationObservable: Observable<MainNavigationDetailLocation> = internalDetailLocation.asObservable()
|
||||
|
||||
private val internalMegaphone = MutableStateFlow(Megaphone.NONE)
|
||||
val megaphone: StateFlow<Megaphone> = internalMegaphone
|
||||
@@ -81,10 +86,40 @@ class MainNavigationViewModel(initialListLocation: MainNavigationListLocation =
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the navigator on the view-model. This wraps the given navigator in our own delegating implementation
|
||||
* such that we can react to navigateTo/Back signals and maintain proper state for internalDetailLocation.
|
||||
*/
|
||||
fun wrapNavigator(composeScope: CoroutineScope, threePaneScaffoldNavigator: ThreePaneScaffoldNavigator<Any>, goToLegacyDetailLocation: (MainNavigationDetailLocation) -> Unit): ThreePaneScaffoldNavigator<Any> {
|
||||
val previous = this.navigator
|
||||
val wrapped = LegacyNavigator(composeScope, threePaneScaffoldNavigator, goToLegacyDetailLocation)
|
||||
this.navigator = wrapped
|
||||
|
||||
if (previous != null) {
|
||||
val destination = previous.currentDestination?.contentKey ?: return wrapped
|
||||
if (destination is MainNavigationDetailLocation) {
|
||||
goTo(destination)
|
||||
}
|
||||
} else {
|
||||
goTo(internalDetailLocation.value)
|
||||
}
|
||||
|
||||
return wrapped
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the requested location. If the navigator is not present, this functionally sets our
|
||||
* "default" location to that specified, and we will route the user there when the navigator is set.
|
||||
*/
|
||||
fun goTo(location: MainNavigationDetailLocation) {
|
||||
viewModelScope.launch {
|
||||
detailLocationRequestFlow.emit(location)
|
||||
detailLocationFlow.emit(location)
|
||||
if (navigator == null) {
|
||||
internalDetailLocation.update {
|
||||
location
|
||||
}
|
||||
}
|
||||
|
||||
navigator?.composeScope?.launch {
|
||||
navigator?.navigateTo(ThreePaneScaffoldRole.Primary, location)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,4 +193,65 @@ class MainNavigationViewModel(initialListLocation: MainNavigationListLocation =
|
||||
enum class NavigationEvent {
|
||||
STORY_CAMERA_FIRST
|
||||
}
|
||||
|
||||
/**
|
||||
* ScaffoldNavigator wrapper that delegates to a default implementation
|
||||
* Ensures we properly update `internalDetailLocation` as the user moves between
|
||||
* screens.
|
||||
*
|
||||
* Delegates to a legacy method if the user is not a large-screen-ui enabled user.
|
||||
*/
|
||||
@Stable
|
||||
private inner class LegacyNavigator(
|
||||
val composeScope: CoroutineScope,
|
||||
private val delegate: ThreePaneScaffoldNavigator<Any>,
|
||||
private val goToLegacyDetailLocation: (MainNavigationDetailLocation) -> Unit
|
||||
) : ThreePaneScaffoldNavigator<Any> by delegate {
|
||||
|
||||
/**
|
||||
* Due to some weirdness with `navigateBack`, we don't seem to be able to execute
|
||||
* code after running the delegate method. So instead, we mark that we saw the call
|
||||
* and then handle updates in `seekBack`.
|
||||
*/
|
||||
private var didNavigateBack: Boolean = false
|
||||
|
||||
/**
|
||||
* If we're not a large screen user, this delegates to the legacy method.
|
||||
* Otherwise, we will delegate to the delegate, and update our detail location.
|
||||
*/
|
||||
override suspend fun navigateTo(pane: ThreePaneScaffoldRole, contentKey: Any?) {
|
||||
if (!SignalStore.internal.largeScreenUi && contentKey is MainNavigationDetailLocation) {
|
||||
goToLegacyDetailLocation(contentKey)
|
||||
} else if (contentKey is MainNavigationDetailLocation.Conversation) {
|
||||
delegate.navigateTo(pane, contentKey)
|
||||
}
|
||||
|
||||
if (SignalStore.internal.largeScreenUi && contentKey is MainNavigationDetailLocation) {
|
||||
internalDetailLocation.emit(contentKey)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the back, and delegates to the delegate.
|
||||
*/
|
||||
override suspend fun navigateBack(backNavigationBehavior: BackNavigationBehavior): Boolean {
|
||||
didNavigateBack = true
|
||||
return delegate.navigateBack(backNavigationBehavior)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates to the delegate, and then consumes the back. If back is consumed, we will update
|
||||
* the internal detail location.
|
||||
*/
|
||||
override suspend fun seekBack(backNavigationBehavior: BackNavigationBehavior, fraction: Float) {
|
||||
delegate.seekBack(backNavigationBehavior, fraction)
|
||||
|
||||
if (didNavigateBack) {
|
||||
didNavigateBack = false
|
||||
|
||||
val destination = currentDestination?.contentKey as? MainNavigationDetailLocation ?: MainNavigationDetailLocation.Empty
|
||||
internalDetailLocation.emit(destination)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user