Add BackHandler compatibility layer.

This commit is contained in:
Alex Hart
2025-11-04 15:56:10 -04:00
committed by Michelle Tang
parent b9897eba79
commit c0fe2dfdc0
3 changed files with 112 additions and 8 deletions

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.compose
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.annotation.RememberInComposition
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
/**
* Allows us to support view based fragments hosted in a compose-based activity having their
* own back handling.
*/
@Stable
class FragmentBackPressedState @RememberInComposition constructor() {
var info by mutableStateOf<FragmentBackPressedInfo?>(null)
fun attach(fragment: Fragment) {
if (fragment is FragmentBackPressedInfoProvider) {
with(fragment) {
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(state = Lifecycle.State.CREATED) {
getFragmentBackPressedInfo().collect {
info = it
}
}
}
}
}
}
}
/**
* Describes the current back-pressed state, produced by a [Fragment]
*/
sealed interface FragmentBackPressedInfo {
object Disabled : FragmentBackPressedInfo
data class Enabled(val callback: () -> Unit) : FragmentBackPressedInfo
}
/**
* Fragment should implement this interface.
*/
interface FragmentBackPressedInfoProvider {
fun getFragmentBackPressedInfo(): Flow<FragmentBackPressedInfo>
}
/**
* BackHandler for interop with legacy style fragments.
* Don't forget to call [FragmentBackPressedState.attach]!
*/
@Composable
fun FragmentBackHandler(state: FragmentBackPressedState) {
val info = state.info
val enabled = when (info) {
FragmentBackPressedInfo.Disabled -> false
is FragmentBackPressedInfo.Enabled -> true
null -> false
}
BackHandler(enabled = enabled) {
if (info is FragmentBackPressedInfo.Enabled) {
info.callback()
}
}
}

View File

@@ -86,11 +86,13 @@ import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.kotlin.subscribeBy
import io.reactivex.rxjava3.schedulers.Schedulers
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
@@ -153,6 +155,8 @@ import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState
import org.thoughtcrime.securesms.components.voice.VoiceNotePlayerView
import org.thoughtcrime.securesms.compose.FragmentBackPressedInfo
import org.thoughtcrime.securesms.compose.FragmentBackPressedInfoProvider
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey.RecipientSearchKey
import org.thoughtcrime.securesms.contactshare.Contact
import org.thoughtcrime.securesms.contactshare.ContactUtil
@@ -393,7 +397,8 @@ class ConversationFragment :
SafetyNumberBottomSheet.Callbacks,
EnableCallNotificationSettingsDialog.Callback,
MultiselectForwardBottomSheet.Callback,
DoubleTapEditEducationSheet.Callback {
DoubleTapEditEducationSheet.Callback,
FragmentBackPressedInfoProvider {
companion object {
private val TAG = Log.tag(ConversationFragment::class.java)
@@ -935,6 +940,18 @@ class ConversationFragment :
override fun onDismissForwardSheet() = Unit
override fun getFragmentBackPressedInfo(): Flow<FragmentBackPressedInfo> {
return viewModel.backPressedState.map {
if (it.shouldHandleBackPressed()) {
FragmentBackPressedInfo.Enabled({
BackPressedCallback().handleOnBackPressed()
})
} else {
FragmentBackPressedInfo.Disabled
}
}
}
//endregion
private fun startActionMode() {
@@ -1034,13 +1051,15 @@ class ConversationFragment :
activity?.supportStartPostponedEnterTransition()
internalDidFirstFrameRender.update { true }
val backPressedDelegate = BackPressedDelegate()
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, backPressedDelegate)
if (requireActivity() is ConversationActivity) {
val backPressedCallback = BackPressedCallback()
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, backPressedCallback)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
viewModel.backPressedState.collectLatest {
backPressedDelegate.isEnabled = it.shouldHandleBackPressed()
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
viewModel.backPressedState.collectLatest {
backPressedCallback.isEnabled = it.shouldHandleBackPressed()
}
}
}
}
@@ -2489,7 +2508,7 @@ class ConversationFragment :
}
}
private inner class BackPressedDelegate : OnBackPressedCallback(false) {
private inner class BackPressedCallback : OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
Log.d(TAG, "onBackPressed()")
val state = viewModel.backPressedState.value

View File

@@ -44,6 +44,8 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.thoughtcrime.securesms.MainNavigator
import org.thoughtcrime.securesms.compose.FragmentBackHandler
import org.thoughtcrime.securesms.compose.FragmentBackPressedState
import org.thoughtcrime.securesms.conversation.ConversationArgs
import org.thoughtcrime.securesms.conversation.ConversationIntents
import org.thoughtcrime.securesms.conversation.v2.ConversationFragment
@@ -106,6 +108,9 @@ fun NavGraphBuilder.chatNavGraphBuilder(
)
}
val backPressedState = remember { FragmentBackPressedState() }
FragmentBackHandler(backPressedState)
AndroidFragment(
clazz = ConversationFragment::class.java,
fragmentState = fragmentState,
@@ -127,6 +132,8 @@ fun NavGraphBuilder.chatNavGraphBuilder(
}
}
backPressedState.attach(fragment)
fragment.viewLifecycleOwner.lifecycleScope.launch {
fragment.repeatOnLifecycle(Lifecycle.State.STARTED) {
fragment.didFirstFrameRender.collectLatest {