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

View File

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