From ae90b2ecd9070e5acb1b54ab853f5222b31c5f01 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 24 Apr 2025 12:24:24 -0300 Subject: [PATCH] Add support for conversation intent routing to MainActivity. --- app/src/main/AndroidManifest.xml | 1 + .../securesms/ApplicationContext.java | 2 ++ .../thoughtcrime/securesms/MainActivity.kt | 16 ++++++++++- .../app/internal/InternalSettingsFragment.kt | 3 ++- .../conversation/ConversationIntents.java | 25 +++++++++++++---- .../conversation/v2/ConversationActivity.kt | 14 +++------- .../conversation/v2/ConversationFragment.kt | 2 +- .../v2/ShareDataTimestampViewModel.kt | 27 +++++++++++++++++-- .../securesms/main/MainNavigationViewModel.kt | 6 ++--- 9 files changed, 72 insertions(+), 24 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6043530cd1..24649ca3d6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1077,6 +1077,7 @@ android:enableOnBackInvokedCallback="true" android:theme="@style/Theme.Signal.DayNight.NoActionBar" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout" + android:windowSoftInputMode="stateUnchanged" android:resizeableActivity="true" android:exported="false"/> diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 24c25b96ec..6a6e506c42 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -103,6 +103,7 @@ import org.thoughtcrime.securesms.service.webrtc.AndroidTelecomUtil; import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.util.AppForegroundObserver; import org.thoughtcrime.securesms.util.AppStartup; +import org.thoughtcrime.securesms.util.ConversationUtil; import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.RemoteConfig; import org.thoughtcrime.securesms.util.SignalLocalMetrics; @@ -189,6 +190,7 @@ public class ApplicationContext extends Application implements AppForegroundObse .addBlocking("tracer", this::initializeTracer) .addNonBlocking(() -> RegistrationUtil.maybeMarkRegistrationComplete()) .addNonBlocking(() -> Glide.get(this)) + .addNonBlocking(ConversationUtil::refreshRecipientShortcuts) .addNonBlocking(this::cleanAvatarStorage) .addNonBlocking(this::initializeRevealableMessageManager) .addNonBlocking(this::initializePendingRetryReceiptManager) diff --git a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt index 42cd1913a6..7584b43f74 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt @@ -70,8 +70,10 @@ import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity.Co import org.thoughtcrime.securesms.components.settings.app.notifications.manual.NotificationProfileSelectionFragment import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner +import org.thoughtcrime.securesms.conversation.ConversationIntents import org.thoughtcrime.securesms.conversation.v2.ConversationFragment import org.thoughtcrime.securesms.conversation.v2.MotionEventRelay +import org.thoughtcrime.securesms.conversation.v2.ShareDataTimestampViewModel import org.thoughtcrime.securesms.conversationlist.RelinkDevicesReminderBottomSheetFragment import org.thoughtcrime.securesms.conversationlist.RestoreCompleteBottomSheetDialog import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter @@ -163,6 +165,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner private val toolbarViewModel: MainToolbarViewModel by viewModels() private val toolbarCallback = ToolbarCallback() + private val shareDataTimestampViewModel: ShareDataTimestampViewModel by viewModels() private val motionEventRelay: MotionEventRelay by viewModels() @@ -209,9 +212,11 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner } } + shareDataTimestampViewModel.setTimestampFromActivityCreation(savedInstanceState, intent) + setContent { val listHostState = rememberFragmentState() - val detailLocation by mainNavigationViewModel.detailLocationRequests.collectAsStateWithLifecycle(MainNavigationDetailLocation.Empty) + val detailLocation by mainNavigationViewModel.detailLocationRequests.collectAsStateWithLifecycle() val snackbar by mainNavigationViewModel.snackbar.collectAsStateWithLifecycle() val mainToolbarState by toolbarViewModel.state.collectAsStateWithLifecycle() val megaphone by mainNavigationViewModel.megaphone.collectAsStateWithLifecycle() @@ -255,6 +260,8 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner startActivity((detailLocation as MainNavigationDetailLocation.Conversation).intent) } } + + mainNavigationViewModel.goTo(MainNavigationDetailLocation.Empty) } AppScaffold( @@ -494,6 +501,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner } private fun handleDeepLinkIntent(intent: Intent) { + handleConversationIntent(intent) handleGroupLinkInIntent(intent) handleProxyInIntent(intent) handleSignalMeIntent(intent) @@ -514,6 +522,12 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner } } + private fun handleConversationIntent(intent: Intent) { + if (ConversationIntents.isConversationIntent(intent)) { + mainNavigationViewModel.goTo(MainNavigationDetailLocation.Conversation(intent)) + } + } + private fun handleGroupLinkInIntent(intent: Intent) { intent.data?.let { data -> CommunicationActions.handlePotentialGroupLinkUrl(this, data.toString()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt index 0026792b58..e5c04609c0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt @@ -167,10 +167,11 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter switchPref( title = DSLSettingsText.from("Enable new split pane UI."), - summary = DSLSettingsText.from("Warning: Some bugs and non functional buttons are expected."), + summary = DSLSettingsText.from("Warning: Some bugs and non functional buttons are expected. App will restart."), isChecked = state.largeScreenUi, onClick = { viewModel.setUseLargeScreenUi(!state.largeScreenUi) + AppUtil.restart(requireContext()) } ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationIntents.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationIntents.java index 01faf3f1c5..cea383119e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationIntents.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationIntents.java @@ -10,12 +10,14 @@ import androidx.annotation.MainThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.MainActivity; import org.thoughtcrime.securesms.badges.models.Badge; import org.thoughtcrime.securesms.conversation.colors.ChatColors; import org.thoughtcrime.securesms.conversation.v2.ConversationActivity; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.ThreadTable; +import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.main.MainNavigationListLocation; import org.thoughtcrime.securesms.mediasend.Media; import org.thoughtcrime.securesms.mms.SlideFactory; import org.thoughtcrime.securesms.recipients.Recipient; @@ -34,7 +36,7 @@ import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.schedulers.Schedulers; public class ConversationIntents { - private static final String TAG = Log.tag(ConversationIntents.class); + private static final String ACTION = "ConversationIntents.ViewConversation"; private static final String BUBBLE_AUTHORITY = "bubble"; private static final String NOTIFICATION_CUSTOM_SCHEME = "custom"; @@ -97,7 +99,11 @@ public class ConversationIntents { */ public static @NonNull Builder createBuilderSync(@NonNull Context context, @NonNull RecipientId recipientId, long threadId) { Preconditions.checkArgument(threadId > 0, "threadId is invalid"); - return new Builder(context, ConversationActivity.class, recipientId, threadId, ConversationScreenType.NORMAL); + return new Builder(context, getConversationActivityClass(), recipientId, threadId, ConversationScreenType.NORMAL); + } + + private static @NonNull Class getConversationActivityClass() { + return SignalStore.internal().getLargeScreenUi() ? MainActivity.class : ConversationActivity.class; } static @Nullable Uri getIntentData(@NonNull Bundle bundle) { @@ -129,6 +135,10 @@ public class ConversationIntents { return uri != null && Objects.equals(uri.getScheme(), NOTIFICATION_CUSTOM_SCHEME); } + public static boolean isConversationIntent(@NonNull Intent intent) { + return ACTION.equals(intent.getAction()); + } + public final static class Args { private final RecipientId recipientId; private final long threadId; @@ -393,9 +403,14 @@ public class ConversationIntents { throw new IllegalStateException("Cannot have both sticker and media array"); } - Intent intent = new Intent(context, conversationActivityClass); + final Intent intent; + if (MainActivity.class.equals(conversationActivityClass)) { + intent = MainActivity.clearTop(context); + } else { + intent = new Intent(context, conversationActivityClass); + } - intent.setAction(Intent.ACTION_DEFAULT); + intent.setAction(ConversationIntents.ACTION); if (conversationScreenType.isInBubble()) { intent.setData(new Uri.Builder().authority(BUBBLE_AUTHORITY) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivity.kt index 5d5d7779b0..c89c52cc11 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivity.kt @@ -6,6 +6,7 @@ import android.os.Bundle import android.view.MotionEvent import android.view.Window import androidx.activity.viewModels +import androidx.lifecycle.enableSavedStateHandles import io.reactivex.rxjava3.subjects.PublishSubject import io.reactivex.rxjava3.subjects.Subject import org.signal.core.util.logging.Log @@ -49,16 +50,12 @@ open class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaCo } override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { + enableSavedStateHandles() supportPostponeEnterTransition() transitionDebouncer.publish { supportStartPostponedEnterTransition() } window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS) - if (savedInstanceState != null) { - shareDataTimestampViewModel.timestamp = savedInstanceState.getLong(STATE_WATERMARK, -1L) - } else if (intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0) { - shareDataTimestampViewModel.timestamp = System.currentTimeMillis() - } - + shareDataTimestampViewModel.setTimestampFromActivityCreation(savedInstanceState, intent) setContentView(R.layout.fragment_container) if (savedInstanceState == null) { @@ -71,11 +68,6 @@ open class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaCo theme.onResume(this) } - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putLong(STATE_WATERMARK, shareDataTimestampViewModel.timestamp) - } - override fun onStop() { super.onStop() if (isChangingConfigurations) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index 2db0a1c9bb..4b9efa24c3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -1564,7 +1564,7 @@ class ConversationFragment : } private fun handleShareOrDraftData(inputReadyState: InputReadyState, data: ShareOrDraftData) { - shareDataTimestampViewModel.timestamp = args.shareDataTimestamp + shareDataTimestampViewModel.setTimestampFromConversationArgs(args) if (inputReadyState.isAnnouncementGroup == true && inputReadyState.isAdmin == false) { Toast.makeText(requireContext(), R.string.MultiselectForwardFragment__only_admins_can_send_messages_to_this_group, Toast.LENGTH_SHORT).show() diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ShareDataTimestampViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ShareDataTimestampViewModel.kt index 5131785484..22f91ccece 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ShareDataTimestampViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ShareDataTimestampViewModel.kt @@ -5,12 +5,35 @@ package org.thoughtcrime.securesms.conversation.v2 +import android.content.Intent +import android.os.Bundle +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel +import org.thoughtcrime.securesms.conversation.ConversationIntents +import org.thoughtcrime.securesms.util.delegate /** * Hold the last share timestamp in an activity scoped view model for sharing between * the activity and fragments. */ -class ShareDataTimestampViewModel : ViewModel() { - var timestamp: Long = -1L +class ShareDataTimestampViewModel( + savedStateHandle: SavedStateHandle +) : ViewModel() { + + companion object { + private const val TIMESTAMP = "timestamp" + } + + var timestamp: Long by savedStateHandle.delegate(TIMESTAMP, -1L) + private set + + fun setTimestampFromActivityCreation(savedInstanceState: Bundle?, intent: Intent) { + if (savedInstanceState == null && intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0) { + timestamp = System.currentTimeMillis() + } + } + + fun setTimestampFromConversationArgs(args: ConversationIntents.Args) { + timestamp = args.shareDataTimestamp + } } 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 a44158dd2d..2995f45d37 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/main/MainNavigationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/main/MainNavigationViewModel.kt @@ -11,7 +11,6 @@ import io.reactivex.rxjava3.core.Observable import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filter @@ -32,9 +31,10 @@ class MainNavigationViewModel(initialListLocation: MainNavigationListLocation = /** * A shared flow of detail location requests that the MainActivity will service. + * This is immediately set back to empty after requesting a detail location to prevent duplicate launches. */ - private val detailLocationRequestFlow = MutableSharedFlow() - val detailLocationRequests: SharedFlow = detailLocationRequestFlow + private val detailLocationRequestFlow = MutableStateFlow(MainNavigationDetailLocation.Empty) + val detailLocationRequests: StateFlow = detailLocationRequestFlow /** * The latest detail location that has been requested, for consumption by other components.