diff --git a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt index 6a8269ffa4..0fc8c6b411 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt @@ -23,6 +23,7 @@ import androidx.activity.enableEdgeToEdge import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints @@ -48,6 +49,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.Dp import androidx.fragment.app.DialogFragment import androidx.fragment.compose.AndroidFragment @@ -276,9 +278,10 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner val mainToolbarState by toolbarViewModel.state.collectAsStateWithLifecycle() val megaphone by mainNavigationViewModel.megaphone.collectAsStateWithLifecycle() val mainNavigationState by mainNavigationViewModel.mainNavigationState.collectAsStateWithLifecycle() + val mainNavigationDetailLocation by mainNavigationViewModel.detailLocation.collectAsStateWithLifecycle() - LaunchedEffect(mainNavigationState.selectedDestination) { - when (mainNavigationState.selectedDestination) { + LaunchedEffect(mainNavigationState.currentListLocation) { + when (mainNavigationState.currentListLocation) { MainNavigationListLocation.CHATS -> toolbarViewModel.presentToolbarForConversationListFragment() MainNavigationListLocation.ARCHIVE -> toolbarViewModel.presentToolbarForConversationListArchiveFragment() MainNavigationListLocation.CALLS -> toolbarViewModel.presentToolbarForCallLogFragment() @@ -359,7 +362,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner Box( modifier = Modifier.weight(1f) ) { - when (val destination = mainNavigationState.selectedDestination) { + when (val destination = mainNavigationState.currentListLocation) { MainNavigationListLocation.CHATS -> { val state = key(destination) { rememberFragmentState() } AndroidFragment( @@ -404,7 +407,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner } }, detailContent = { - when (val destination = wrappedNavigator.currentDestination?.contentKey) { + when (val destination = mainNavigationDetailLocation) { is MainNavigationDetailLocation.Conversation -> { val fragmentState = key(destination) { rememberFragmentState() } AndroidFragment( @@ -418,6 +421,22 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner .fillMaxSize() ) } + + MainNavigationDetailLocation.Empty -> { + Box( + modifier = Modifier + .padding(end = contentLayoutData.detailPaddingEnd) + .clip(contentLayoutData.shape) + .background(color = MaterialTheme.colorScheme.surface) + .fillMaxSize() + ) { + Image( + painter = painterResource(R.drawable.ic_signal_logo_large), + contentDescription = null, + modifier = Modifier.align(Alignment.Center) + ) + } + } } }, paneExpansionDragHandle = if (contentLayoutData.hasDragHandle()) { @@ -695,6 +714,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner private fun handleConversationIntent(intent: Intent) { if (ConversationIntents.isConversationIntent(intent)) { + mainNavigationViewModel.goTo(MainNavigationListLocation.CHATS) mainNavigationViewModel.goTo(MainNavigationDetailLocation.Conversation(intent)) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/main/MainNavigation.kt b/app/src/main/java/org/thoughtcrime/securesms/main/MainNavigation.kt index dafc3e823a..38952536ea 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/main/MainNavigation.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/main/MainNavigation.kt @@ -88,7 +88,7 @@ data class MainNavigationState( val storiesCount: Int = 0, val storyFailure: Boolean = false, val isStoriesFeatureEnabled: Boolean = true, - val selectedDestination: MainNavigationListLocation = MainNavigationListLocation.CHATS, + val currentListLocation: MainNavigationListLocation = MainNavigationListLocation.CHATS, val compact: Boolean = false ) @@ -123,7 +123,7 @@ fun MainNavigationBar( MainNavigationListLocation.STORIES -> state.storiesCount } - val selected = state.selectedDestination == destination + val selected = state.currentListLocation == destination NavigationBarItem( selected = selected, icon = { @@ -216,7 +216,7 @@ fun MainNavigationRail( containerColor = SignalTheme.colors.colorSurface1, header = { MainFloatingActionButtons( - destination = state.selectedDestination, + destination = state.currentListLocation, callback = mainFloatingActionButtonsCallback, modifier = Modifier.padding(vertical = 40.dp) ) @@ -231,7 +231,7 @@ fun MainNavigationRail( } entries.forEachIndexed { idx, destination -> - val selected = state.selectedDestination == destination + val selected = state.currentListLocation == destination Box { NavigationRailItem( @@ -346,7 +346,7 @@ private fun MainNavigationRailPreview() { chatsCount = 500, callsCount = 10, storiesCount = 5, - selectedDestination = selected + currentListLocation = selected ), mainFloatingActionButtonsCallback = MainFloatingActionButtonsCallback.Empty, onDestinationSelected = { selected = it } @@ -365,7 +365,7 @@ private fun MainNavigationBarPreview() { chatsCount = 500, callsCount = 10, storiesCount = 5, - selectedDestination = selected, + currentListLocation = selected, compact = false ), onDestinationSelected = { selected = it } diff --git a/app/src/main/java/org/thoughtcrime/securesms/main/MainNavigationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/main/MainNavigationViewModel.kt index 6a967363e3..32234518c7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/main/MainNavigationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/main/MainNavigationViewModel.kt @@ -7,9 +7,7 @@ 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 @@ -38,7 +36,9 @@ class MainNavigationViewModel( ) : ViewModel() { private val megaphoneRepository = AppDependencies.megaphoneRepository - private var navigator: LegacyNavigator? = null + private var navigator: ThreePaneScaffoldNavigator? = null + private var navigatorScope: CoroutineScope? = null + private var goToLegacyDetailLocation: ((MainNavigationDetailLocation) -> Unit)? = null /** * The latest detail location that has been requested, for consumption by other components. @@ -46,6 +46,7 @@ class MainNavigationViewModel( private val internalDetailLocation = MutableStateFlow(initialDetailLocation) val detailLocation: StateFlow = internalDetailLocation val detailLocationObservable: Observable = internalDetailLocation.asObservable() + var latestConversationLocation: MainNavigationDetailLocation.Conversation? = null private val internalMegaphone = MutableStateFlow(Megaphone.NONE) val megaphone: StateFlow = internalMegaphone @@ -58,7 +59,7 @@ class MainNavigationViewModel( private val notificationProfilesRepository: NotificationProfilesRepository = NotificationProfilesRepository() - private val internalMainNavigationState = MutableStateFlow(MainNavigationState(selectedDestination = initialListLocation)) + private val internalMainNavigationState = MutableStateFlow(MainNavigationState(currentListLocation = initialListLocation)) val mainNavigationState: StateFlow = internalMainNavigationState /** @@ -90,29 +91,10 @@ class MainNavigationViewModel( * such that we can react to navigateTo/Back signals and maintain proper state for internalDetailLocation. */ fun wrapNavigator(composeScope: CoroutineScope, threePaneScaffoldNavigator: ThreePaneScaffoldNavigator, goToLegacyDetailLocation: (MainNavigationDetailLocation) -> Unit): ThreePaneScaffoldNavigator { - 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 MainNavigationListLocation) { - goTo(destination) - } - } else { - goTo(mainNavigationState.value.selectedDestination) - } - - if (previous != null) { - val destination = previous.currentDestination?.contentKey ?: return wrapped - if (destination is MainNavigationDetailLocation) { - goTo(destination) - } - } else { - goTo(internalDetailLocation.value) - } - - return wrapped + this.goToLegacyDetailLocation = goToLegacyDetailLocation + this.navigatorScope = composeScope + this.navigator = threePaneScaffoldNavigator + return threePaneScaffoldNavigator } /** @@ -120,20 +102,48 @@ class MainNavigationViewModel( * "default" location to that specified, and we will route the user there when the navigator is set. */ fun goTo(location: MainNavigationDetailLocation) { - if (navigator == null) { - internalDetailLocation.update { - location + if (!SignalStore.internal.largeScreenUi) { + goToLegacyDetailLocation?.invoke(location) + return + } + + internalDetailLocation.update { + location + } + + val focusedPane = when (location) { + is MainNavigationDetailLocation.Empty -> { + ThreePaneScaffoldRole.Secondary + } + + is MainNavigationDetailLocation.Conversation -> { + latestConversationLocation = location + ThreePaneScaffoldRole.Primary } } - navigator?.composeScope?.launch { - navigator?.navigateTo(ThreePaneScaffoldRole.Primary, location) + navigatorScope?.launch { + navigator?.navigateTo(focusedPane) } } fun goTo(location: MainNavigationListLocation) { + if (location != MainNavigationListLocation.CHATS) { + internalDetailLocation.update { + MainNavigationDetailLocation.Empty + } + } else { + internalDetailLocation.update { + latestConversationLocation ?: MainNavigationDetailLocation.Empty + } + } + internalMainNavigationState.update { - it.copy(selectedDestination = location) + it.copy(currentListLocation = location) + } + + navigatorScope?.launch { + navigator?.navigateTo(ThreePaneScaffoldRole.Secondary) } } @@ -193,13 +203,11 @@ class MainNavigationViewModel( private fun onTabSelected(destination: MainNavigationListLocation) { viewModelScope.launch { - val currentTab = internalMainNavigationState.value.selectedDestination + val currentTab = internalMainNavigationState.value.currentListLocation if (currentTab == destination) { internalTabClickEvents.emit(destination) } else { - internalMainNavigationState.update { - it.copy(selectedDestination = destination) - } + goTo(destination) } } } @@ -215,65 +223,4 @@ class MainNavigationViewModel( 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, - private val goToLegacyDetailLocation: (MainNavigationDetailLocation) -> Unit - ) : ThreePaneScaffoldNavigator 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) - } - } - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/AudioManagerCompat.java b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/AudioManagerCompat.java index 16c95fc44b..6685c7dedf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/AudioManagerCompat.java +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/AudioManagerCompat.java @@ -103,7 +103,7 @@ public abstract class AudioManagerCompat { Log.w(TAG, "isSpeakerphoneOn: Failed to find communication device."); return false; } else { - return audioDeviceInfo.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER; + return AudioDeviceMapping.fromPlatformType(audioDeviceInfo.getType()) == SignalAudioManager.AudioDevice.SPEAKER_PHONE; } } else { return audioManager.isSpeakerphoneOn(); @@ -112,16 +112,16 @@ public abstract class AudioManagerCompat { public void setSpeakerphoneOn(boolean on) { if (Build.VERSION.SDK_INT >= 31) { - int desiredType = on ? AudioDeviceInfo.TYPE_BUILTIN_SPEAKER : AudioDeviceInfo.TYPE_BUILTIN_EARPIECE; - AudioDeviceInfo candidate = getAvailableCommunicationDevices().stream() - .filter(audioDeviceInfo -> audioDeviceInfo.getType() == desiredType) + SignalAudioManager.AudioDevice audioDevice = on ? SignalAudioManager.AudioDevice.SPEAKER_PHONE : SignalAudioManager.AudioDevice.EARPIECE; + AudioDeviceInfo candidate = getAvailableCommunicationDevices().stream() + .filter(it -> AudioDeviceMapping.fromPlatformType(it.getType()) == audioDevice) .findFirst() .orElse(null); if (candidate != null) { setCommunicationDevice(candidate); } else { - Log.w(TAG, "setSpeakerphoneOn: Failed to find candidate for device type {" + desiredType + "}. Falling back on deprecated method."); + Log.w(TAG, "setSpeakerphoneOn: Failed to find candidate for SignalAudioDevice {" + audioDevice + "}. Falling back on deprecated method."); audioManager.setSpeakerphoneOn(on); } } else {