Fix action mode to use our own action mode toolbar instead of the built in androidx one.

This commit is contained in:
lisa-signal
2025-06-03 14:22:35 -04:00
committed by Cody Henthorne
parent 4db60ac63f
commit 74d9195d94
7 changed files with 98 additions and 101 deletions

View File

@@ -16,6 +16,7 @@ import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
@@ -250,6 +251,22 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
}
}
val callback = object : OnBackPressedCallback(toolbarViewModel.state.value.mode == MainToolbarMode.ACTION_MODE) {
override fun handleOnBackPressed() {
toolbarCallback.onCloseActionModeClick()
}
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
toolbarViewModel.state.collect { state ->
callback.isEnabled = state.mode == MainToolbarMode.ACTION_MODE
}
}
}
onBackPressedDispatcher.addCallback(this, callback)
shareDataTimestampViewModel.setTimestampFromActivityCreation(savedInstanceState, intent)
setContent {
@@ -810,6 +827,15 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
toolbarViewModel.emitEvent(MainToolbarViewModel.Event.Chats.CloseArchive)
}
override fun onCloseActionModeClick() {
supportFragmentManager.fragments.forEach { fragment ->
when (fragment) {
is ConversationListFragment -> fragment.endActionModeIfActive()
is CallLogFragment -> fragment.CallLogActionModeCallback().onActionModeWillEnd()
}
}
}
override fun onSearchQueryUpdated(query: String) {
toolbarViewModel.setSearchQuery(query)
}

View File

@@ -1,72 +1,37 @@
package org.thoughtcrime.securesms.calls.log
import android.content.res.Resources
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.view.ActionMode
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.main.MainToolbarViewModel
class CallLogActionMode(
private val callback: Callback
) : ActionMode.Callback {
private val callback: Callback,
private val mainToolbarViewModel: MainToolbarViewModel
) {
private var actionMode: ActionMode? = null
private var count: Int = 0
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
mode?.title = getTitle(1)
return true
}
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
return false
}
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
return true
}
override fun onDestroyActionMode(mode: ActionMode?) {
callback.onResetSelectionState()
endIfActive()
}
fun isInActionMode(): Boolean {
return actionMode != null
}
fun getCount(): Int {
return if (actionMode != null) count else 0
return if (mainToolbarViewModel.isInActionMode()) count else 0
}
fun setCount(count: Int) {
this.count = count
actionMode?.title = getTitle(count)
mainToolbarViewModel.setActionModeCount(count)
}
fun start() {
actionMode = callback.startActionMode(this)
callback.startActionMode()
}
fun end() {
callback.onActionModeWillEnd()
actionMode?.finish()
count = 0
actionMode = null
}
private fun getTitle(callLogsSelected: Int): String {
return callback.getResources().getQuantityString(R.plurals.ConversationListFragment_s_selected, callLogsSelected, callLogsSelected)
}
private fun endIfActive() {
if (actionMode != null) {
end()
if (mainToolbarViewModel.isInActionMode()) {
callback.onActionModeWillEnd()
count = 0
}
}
interface Callback {
fun startActionMode(callback: ActionMode.Callback): ActionMode?
fun startActionMode()
fun onActionModeWillEnd()
fun getResources(): Resources
fun onResetSelectionState()

View File

@@ -7,8 +7,6 @@ import android.view.View
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.compose.material3.SnackbarDuration
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.Fragment
@@ -77,7 +75,7 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
private val disposables = LifecycleDisposable()
private val callLogContextMenu = CallLogContextMenu(this, this)
private val callLogActionMode = CallLogActionMode(CallLogActionModeCallback())
private lateinit var callLogActionMode: CallLogActionMode
private val conversationUpdateTick: ConversationUpdateTick = ConversationUpdateTick(this::onTimestampTick)
private var callLogAdapter: CallLogAdapter? = null
@@ -91,6 +89,8 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
viewLifecycleOwner.lifecycle.addObserver(conversationUpdateTick)
viewLifecycleOwner.lifecycle.addObserver(viewModel.callLogPeekHelper)
callLogActionMode = CallLogActionMode(CallLogActionModeCallback(), mainToolbarViewModel)
val callLogAdapter = CallLogAdapter(this)
disposables.bindTo(viewLifecycleOwner)
@@ -134,7 +134,7 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
.subscribe { (selected, totalCount) ->
if (selected.isNotEmpty(totalCount)) {
callLogActionMode.setCount(selected.count(totalCount))
} else if (callLogActionMode.isInActionMode()) {
} else if (mainToolbarViewModel.isInActionMode()) {
callLogActionMode.end()
}
}
@@ -275,7 +275,7 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
}
override fun canStartNestedScroll(): Boolean {
return !callLogActionMode.isInActionMode() && !isSearchOpen()
return !mainToolbarViewModel.isInActionMode() && !isSearchOpen()
}
}
@@ -464,17 +464,16 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
override fun onBottomActionBarVisibilityChanged(visibility: Int) = Unit
}
private inner class CallLogActionModeCallback : CallLogActionMode.Callback {
override fun startActionMode(callback: ActionMode.Callback): ActionMode? {
val actionMode = (requireActivity() as AppCompatActivity).startSupportActionMode(callback)
inner class CallLogActionModeCallback : CallLogActionMode.Callback {
override fun startActionMode() {
requireListener<Callback>().onMultiSelectStarted()
signalBottomActionBarController.setVisibility(true)
return actionMode
}
override fun onActionModeWillEnd() {
requireListener<Callback>().onMultiSelectFinished()
signalBottomActionBarController.setVisibility(false)
viewModel.clearSelected()
}
override fun getResources(): Resources = resources

View File

@@ -26,9 +26,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.annotation.WorkerThread;
import androidx.appcompat.view.ActionMode;
import androidx.compose.material3.SnackbarDuration;
import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.RecyclerView;
import org.signal.core.util.concurrent.LifecycleDisposable;
@@ -48,7 +46,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers;
import kotlin.Unit;
public class ConversationListArchiveFragment extends ConversationListFragment implements ActionMode.Callback
public class ConversationListArchiveFragment extends ConversationListFragment
{
private View coordinator;
private RecyclerView list;

View File

@@ -30,8 +30,6 @@ import android.os.AsyncTask;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
@@ -44,9 +42,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.PluralsRes;
import androidx.annotation.WorkerThread;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.view.ActionMode;
import androidx.compose.material3.SnackbarDuration;
import androidx.compose.ui.platform.ComposeView;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
@@ -184,8 +180,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers;
import kotlin.Unit;
public class ConversationListFragment extends MainFragment implements ActionMode.Callback,
ConversationListAdapter.OnConversationClickListener,
public class ConversationListFragment extends MainFragment implements ConversationListAdapter.OnConversationClickListener,
ClearFilterViewHolder.OnClearFilterClickListener,
ChatFolderAdapter.Callbacks,
ConversationListAdapter.EmptyFolderViewHolder.OnFolderSettingsClickListener
@@ -201,8 +196,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode
private static final int MAX_CHATS_ABOVE_FOLD = 7;
private static final int MAX_CONTACTS_ABOVE_FOLD = 5;
private static final int MAX_GROUP_MEMBERSHIPS_ABOVE_FOLD = 5;
private ActionMode actionMode;
private View coordinator;
private RecyclerView chatFolderList;
private RecyclerView list;
@@ -1150,22 +1143,20 @@ public class ConversationListFragment extends MainFragment implements ActionMode
}
private void startActionMode() {
actionMode = ((AppCompatActivity) getActivity()).startSupportActionMode(ConversationListFragment.this);
ViewUtil.animateIn(bottomActionBar, bottomActionBar.getEnterAnimation());
requireCallback().onMultiSelectStarted();
}
private void endActionModeIfActive() {
if (actionMode != null) {
public void endActionModeIfActive() {
if (mainToolbarViewModel.isInActionMode()) {
endActionMode();
}
}
private void endActionMode() {
actionMode.finish();
actionMode = null;
ViewUtil.animateOut(bottomActionBar, bottomActionBar.getExitAnimation());
requireCallback().onMultiSelectFinished();
viewModel.endSelection();
}
void updateEmptyState(boolean isConversationEmpty) {
@@ -1186,7 +1177,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
@Override
public void onConversationClick(@NonNull Conversation conversation) {
if (actionMode == null) {
if (!mainToolbarViewModel.isInActionMode()) {
handleCreateConversation(conversation.getThreadRecord().getThreadId(), conversation.getThreadRecord().getRecipient(), conversation.getThreadRecord().getDistributionType());
} else {
viewModel.toggleConversationSelected(conversation);
@@ -1195,7 +1186,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
@Override
public boolean onConversationLongClick(@NonNull Conversation conversation, @NonNull View view) {
if (actionMode != null) {
if (mainToolbarViewModel.isInActionMode()) {
onConversationClick(conversation);
return true;
}
@@ -1269,29 +1260,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode
return true;
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
mode.setTitle(requireContext().getResources().getQuantityString(R.plurals.ConversationListFragment_s_selected, 1, 1));
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
updateMultiSelectState();
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
return true;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
viewModel.endSelection();
endActionModeIfActive();
}
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
public void onEvent(MessageSender.MessageSentEvent event) {
EventBus.getDefault().removeStickyEvent(event);
@@ -1351,8 +1319,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode
boolean hasUnmuted = Stream.of(viewModel.currentSelectedConversations()).anyMatch(conversation -> !conversation.getThreadRecord().getRecipient().live().get().isMuted());
boolean canPin = viewModel.getPinnedCount() < MAXIMUM_PINNED_CONVERSATIONS;
if (actionMode != null) {
actionMode.setTitle(requireContext().getResources().getQuantityString(R.plurals.ConversationListFragment_s_selected, count, count));
if (mainToolbarViewModel.isInActionMode()) {
mainToolbarViewModel.setActionModeCount(count);
}
List<ActionItem> items = new ArrayList<>();
@@ -1622,7 +1590,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
viewHolder instanceof ConversationListAdapter.HeaderViewHolder ||
viewHolder instanceof ClearFilterViewHolder ||
viewHolder instanceof ConversationListAdapter.EmptyFolderViewHolder ||
actionMode != null ||
mainToolbarViewModel.isInActionMode() ||
viewHolder.itemView.isSelected() ||
activeAdapter == searchAdapter)
{

View File

@@ -78,6 +78,7 @@ import org.thoughtcrime.securesms.avatar.AvatarImage
import org.thoughtcrime.securesms.calls.log.CallLogFilter
import org.thoughtcrime.securesms.components.settings.app.subscription.BadgeImageSmall
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter
import org.thoughtcrime.securesms.dependencies.GooglePlayBillingDependencies.context
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.rememberRecipientField
@@ -98,6 +99,7 @@ interface MainToolbarCallback {
fun onStoryPrivacyClick()
fun onCloseSearchClick()
fun onCloseArchiveClick()
fun onCloseActionModeClick()
fun onSearchQueryUpdated(query: String)
fun onNotificationProfileTooltipDismissed()
@@ -118,6 +120,7 @@ interface MainToolbarCallback {
override fun onStoryPrivacyClick() = Unit
override fun onCloseSearchClick() = Unit
override fun onCloseArchiveClick() = Unit
override fun onCloseActionModeClick() = Unit
override fun onSearchQueryUpdated(query: String) = Unit
override fun onNotificationProfileTooltipDismissed() = Unit
}
@@ -144,7 +147,8 @@ data class MainToolbarState(
val hasPassphrase: Boolean = false,
val proxyState: ProxyState = ProxyState.NONE,
@StringRes val searchHint: Int = R.string.SearchToolbar_search,
val searchQuery: String = ""
val searchQuery: String = "",
val actionModeCount: Int = 0
) {
enum class ProxyState(@DrawableRes val icon: Int) {
NONE(-1),
@@ -161,7 +165,10 @@ fun MainToolbar(
callback: MainToolbarCallback
) {
if (state.mode == MainToolbarMode.ACTION_MODE) {
TopAppBar(title = {})
ActionModeToolbar(
state = state,
callback = callback
)
return
}
@@ -209,6 +216,32 @@ fun MainToolbar(
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ActionModeToolbar(
state: MainToolbarState,
callback: MainToolbarCallback
) {
TopAppBar(
colors = TopAppBarDefaults.topAppBarColors(
containerColor = state.toolbarColor ?: MaterialTheme.colorScheme.surface
),
navigationIcon = {
IconButtons.IconButton(onClick = {
callback.onCloseActionModeClick()
}) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.symbol_x_24),
contentDescription = stringResource(R.string.CallScreenTopBar__go_back)
)
}
},
title = {
Text(text = context.resources.getQuantityString(R.plurals.ConversationListFragment_s_selected, state.actionModeCount, state.actionModeCount))
}
)
}
@Composable
private fun SearchToolbar(
state: MainToolbarState,

View File

@@ -73,6 +73,14 @@ class MainToolbarViewModel : ViewModel() {
}
}
fun setActionModeCount(count: Int) {
internalStateFlow.update {
it.copy(actionModeCount = count)
}
}
fun isInActionMode(): Boolean = state.value.mode == MainToolbarMode.ACTION_MODE
fun presentToolbarForConversationListFragment() {
setToolbarMode(MainToolbarMode.FULL, destination = MainNavigationListLocation.CHATS, overwriteSearchMode = false)
}