diff --git a/app/src/main/java/org/thoughtcrime/securesms/chats/ChatsNavigation.kt b/app/src/main/java/org/thoughtcrime/securesms/chats/ChatsNavigation.kt index a71f68e0d4..1dd95827e6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/chats/ChatsNavigation.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/chats/ChatsNavigation.kt @@ -30,6 +30,7 @@ import kotlinx.coroutines.launch import org.signal.core.ui.navigation.TransitionSpecs import org.thoughtcrime.securesms.MainNavigator import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsNavHostFragment +import org.thoughtcrime.securesms.compose.AndroidNavHostFragment import org.thoughtcrime.securesms.compose.FragmentBackHandler import org.thoughtcrime.securesms.compose.FragmentBackPressedState import org.thoughtcrime.securesms.conversation.ConversationIntents @@ -141,7 +142,7 @@ private fun ConversationSettingsEntry(route: MainNavigationDetailLocation.Chats. val backPressedState = remember { FragmentBackPressedState() } FragmentBackHandler(backPressedState) - AndroidFragment( + AndroidNavHostFragment( clazz = ConversationSettingsNavHostFragment::class.java, fragmentState = fragmentState, arguments = args, diff --git a/app/src/main/java/org/thoughtcrime/securesms/compose/AndroidNavHostFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/compose/AndroidNavHostFragment.kt new file mode 100644 index 0000000000..a7aa13b237 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/compose/AndroidNavHostFragment.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.compose + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentContainerView +import androidx.fragment.app.FragmentFactory +import androidx.fragment.app.commitNow +import androidx.fragment.compose.AndroidFragment +import androidx.fragment.compose.FragmentState +import androidx.fragment.compose.rememberFragmentState +import androidx.navigation.fragment.NavHostFragment +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import org.thoughtcrime.securesms.R + +/** + * Hosts a [NavHostFragment] inside Compose. + * + * [AndroidFragment] cannot host a [NavHostFragment] directly. It gives its container a + * composition-derived id, and [NavHostFragment] reuses its own container id for the destinations it + * navigates to. That id is written into the saved child-fragment state, so once the screen is + * restored the child [androidx.fragment.app.FragmentManager] looks for a container id that no longer + * exists and throws "No view found for id ...". + * + * Interposing [NavHostWrapperFragment] gives the nav host a container with a stable resource id, + * which survives configuration changes and process death. + */ +@Composable +fun AndroidNavHostFragment( + clazz: Class, + modifier: Modifier = Modifier, + fragmentState: FragmentState = rememberFragmentState(), + arguments: Bundle = Bundle.EMPTY, + onUpdate: (Fragment) -> Unit = {} +) { + val wrapperArguments = remember(clazz, arguments) { NavHostWrapperFragment.createArgs(clazz, arguments) } + + AndroidFragment( + clazz = NavHostWrapperFragment::class.java, + modifier = modifier, + fragmentState = fragmentState, + arguments = wrapperArguments, + onUpdate = onUpdate + ) +} + +/** + * Hosts a [NavHostFragment] in a container with a stable id. Use via [AndroidNavHostFragment]. + */ +class NavHostWrapperFragment : Fragment(), FragmentBackPressedInfoProvider { + + companion object { + private const val ARG_NAV_HOST_CLASS_NAME = "nav_host_wrapper.class_name" + private const val ARG_NAV_HOST_ARGUMENTS = "nav_host_wrapper.arguments" + + fun createArgs(clazz: Class, arguments: Bundle): Bundle { + return bundleOf( + ARG_NAV_HOST_CLASS_NAME to clazz.name, + ARG_NAV_HOST_ARGUMENTS to arguments + ) + } + } + + private val navHostFragment: NavHostFragment? + get() = childFragmentManager.findFragmentById(R.id.nav_host_wrapper_container) as? NavHostFragment + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (navHostFragment != null) { + return + } + + val args = requireArguments() + val className = requireNotNull(args.getString(ARG_NAV_HOST_CLASS_NAME)) { "No nav host class name in arguments." } + val navHostClass = FragmentFactory.loadFragmentClass(requireContext().classLoader, className) + + childFragmentManager.commitNow { + setReorderingAllowed(true) + add(R.id.nav_host_wrapper_container, navHostClass, args.getBundle(ARG_NAV_HOST_ARGUMENTS)) + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return FragmentContainerView(inflater.context).apply { + id = R.id.nav_host_wrapper_container + layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) + } + } + + override fun getFragmentBackPressedInfo(): Flow { + val provider = navHostFragment as? FragmentBackPressedInfoProvider + return provider?.getFragmentBackPressedInfo() ?: flowOf(FragmentBackPressedInfo.Disabled) + } +} diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml index b03512ee37..164dbe7f22 100644 --- a/app/src/main/res/values/ids.xml +++ b/app/src/main/res/values/ids.xml @@ -32,4 +32,6 @@ + +