Add support for conversation intent routing to MainActivity.

This commit is contained in:
Alex Hart
2025-04-24 12:24:24 -03:00
committed by Cody Henthorne
parent 9d593bcaff
commit ae90b2ecd9
9 changed files with 72 additions and 24 deletions

View File

@@ -1077,6 +1077,7 @@
android:enableOnBackInvokedCallback="true" android:enableOnBackInvokedCallback="true"
android:theme="@style/Theme.Signal.DayNight.NoActionBar" android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout"
android:windowSoftInputMode="stateUnchanged"
android:resizeableActivity="true" android:resizeableActivity="true"
android:exported="false"/> android:exported="false"/>

View File

@@ -103,6 +103,7 @@ import org.thoughtcrime.securesms.service.webrtc.AndroidTelecomUtil;
import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.AppForegroundObserver; import org.thoughtcrime.securesms.util.AppForegroundObserver;
import org.thoughtcrime.securesms.util.AppStartup; import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.ConversationUtil;
import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.RemoteConfig; import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.util.SignalLocalMetrics; import org.thoughtcrime.securesms.util.SignalLocalMetrics;
@@ -189,6 +190,7 @@ public class ApplicationContext extends Application implements AppForegroundObse
.addBlocking("tracer", this::initializeTracer) .addBlocking("tracer", this::initializeTracer)
.addNonBlocking(() -> RegistrationUtil.maybeMarkRegistrationComplete()) .addNonBlocking(() -> RegistrationUtil.maybeMarkRegistrationComplete())
.addNonBlocking(() -> Glide.get(this)) .addNonBlocking(() -> Glide.get(this))
.addNonBlocking(ConversationUtil::refreshRecipientShortcuts)
.addNonBlocking(this::cleanAvatarStorage) .addNonBlocking(this::cleanAvatarStorage)
.addNonBlocking(this::initializeRevealableMessageManager) .addNonBlocking(this::initializeRevealableMessageManager)
.addNonBlocking(this::initializePendingRetryReceiptManager) .addNonBlocking(this::initializePendingRetryReceiptManager)

View File

@@ -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.settings.app.notifications.manual.NotificationProfileSelectionFragment
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner 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.ConversationFragment
import org.thoughtcrime.securesms.conversation.v2.MotionEventRelay 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.RelinkDevicesReminderBottomSheetFragment
import org.thoughtcrime.securesms.conversationlist.RestoreCompleteBottomSheetDialog import org.thoughtcrime.securesms.conversationlist.RestoreCompleteBottomSheetDialog
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter
@@ -163,6 +165,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
private val toolbarViewModel: MainToolbarViewModel by viewModels() private val toolbarViewModel: MainToolbarViewModel by viewModels()
private val toolbarCallback = ToolbarCallback() private val toolbarCallback = ToolbarCallback()
private val shareDataTimestampViewModel: ShareDataTimestampViewModel by viewModels()
private val motionEventRelay: MotionEventRelay by viewModels() private val motionEventRelay: MotionEventRelay by viewModels()
@@ -209,9 +212,11 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
} }
} }
shareDataTimestampViewModel.setTimestampFromActivityCreation(savedInstanceState, intent)
setContent { setContent {
val listHostState = rememberFragmentState() val listHostState = rememberFragmentState()
val detailLocation by mainNavigationViewModel.detailLocationRequests.collectAsStateWithLifecycle(MainNavigationDetailLocation.Empty) val detailLocation by mainNavigationViewModel.detailLocationRequests.collectAsStateWithLifecycle()
val snackbar by mainNavigationViewModel.snackbar.collectAsStateWithLifecycle() val snackbar by mainNavigationViewModel.snackbar.collectAsStateWithLifecycle()
val mainToolbarState by toolbarViewModel.state.collectAsStateWithLifecycle() val mainToolbarState by toolbarViewModel.state.collectAsStateWithLifecycle()
val megaphone by mainNavigationViewModel.megaphone.collectAsStateWithLifecycle() val megaphone by mainNavigationViewModel.megaphone.collectAsStateWithLifecycle()
@@ -255,6 +260,8 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
startActivity((detailLocation as MainNavigationDetailLocation.Conversation).intent) startActivity((detailLocation as MainNavigationDetailLocation.Conversation).intent)
} }
} }
mainNavigationViewModel.goTo(MainNavigationDetailLocation.Empty)
} }
AppScaffold( AppScaffold(
@@ -494,6 +501,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
} }
private fun handleDeepLinkIntent(intent: Intent) { private fun handleDeepLinkIntent(intent: Intent) {
handleConversationIntent(intent)
handleGroupLinkInIntent(intent) handleGroupLinkInIntent(intent)
handleProxyInIntent(intent) handleProxyInIntent(intent)
handleSignalMeIntent(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) { private fun handleGroupLinkInIntent(intent: Intent) {
intent.data?.let { data -> intent.data?.let { data ->
CommunicationActions.handlePotentialGroupLinkUrl(this, data.toString()) CommunicationActions.handlePotentialGroupLinkUrl(this, data.toString())

View File

@@ -167,10 +167,11 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
switchPref( switchPref(
title = DSLSettingsText.from("Enable new split pane UI."), 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, isChecked = state.largeScreenUi,
onClick = { onClick = {
viewModel.setUseLargeScreenUi(!state.largeScreenUi) viewModel.setUseLargeScreenUi(!state.largeScreenUi)
AppUtil.restart(requireContext())
} }
) )

View File

@@ -10,12 +10,14 @@ import androidx.annotation.MainThread;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; 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.badges.models.Badge;
import org.thoughtcrime.securesms.conversation.colors.ChatColors; import org.thoughtcrime.securesms.conversation.colors.ChatColors;
import org.thoughtcrime.securesms.conversation.v2.ConversationActivity; import org.thoughtcrime.securesms.conversation.v2.ConversationActivity;
import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.ThreadTable; 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.mediasend.Media;
import org.thoughtcrime.securesms.mms.SlideFactory; import org.thoughtcrime.securesms.mms.SlideFactory;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
@@ -34,7 +36,7 @@ import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
public class ConversationIntents { 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 BUBBLE_AUTHORITY = "bubble";
private static final String NOTIFICATION_CUSTOM_SCHEME = "custom"; 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) { public static @NonNull Builder createBuilderSync(@NonNull Context context, @NonNull RecipientId recipientId, long threadId) {
Preconditions.checkArgument(threadId > 0, "threadId is invalid"); 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<? extends Activity> getConversationActivityClass() {
return SignalStore.internal().getLargeScreenUi() ? MainActivity.class : ConversationActivity.class;
} }
static @Nullable Uri getIntentData(@NonNull Bundle bundle) { static @Nullable Uri getIntentData(@NonNull Bundle bundle) {
@@ -129,6 +135,10 @@ public class ConversationIntents {
return uri != null && Objects.equals(uri.getScheme(), NOTIFICATION_CUSTOM_SCHEME); 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 { public final static class Args {
private final RecipientId recipientId; private final RecipientId recipientId;
private final long threadId; private final long threadId;
@@ -393,9 +403,14 @@ public class ConversationIntents {
throw new IllegalStateException("Cannot have both sticker and media array"); 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()) { if (conversationScreenType.isInBubble()) {
intent.setData(new Uri.Builder().authority(BUBBLE_AUTHORITY) intent.setData(new Uri.Builder().authority(BUBBLE_AUTHORITY)

View File

@@ -6,6 +6,7 @@ import android.os.Bundle
import android.view.MotionEvent import android.view.MotionEvent
import android.view.Window import android.view.Window
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.lifecycle.enableSavedStateHandles
import io.reactivex.rxjava3.subjects.PublishSubject import io.reactivex.rxjava3.subjects.PublishSubject
import io.reactivex.rxjava3.subjects.Subject import io.reactivex.rxjava3.subjects.Subject
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
@@ -49,16 +50,12 @@ open class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaCo
} }
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
enableSavedStateHandles()
supportPostponeEnterTransition() supportPostponeEnterTransition()
transitionDebouncer.publish { supportStartPostponedEnterTransition() } transitionDebouncer.publish { supportStartPostponedEnterTransition() }
window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS) window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)
if (savedInstanceState != null) { shareDataTimestampViewModel.setTimestampFromActivityCreation(savedInstanceState, intent)
shareDataTimestampViewModel.timestamp = savedInstanceState.getLong(STATE_WATERMARK, -1L)
} else if (intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0) {
shareDataTimestampViewModel.timestamp = System.currentTimeMillis()
}
setContentView(R.layout.fragment_container) setContentView(R.layout.fragment_container)
if (savedInstanceState == null) { if (savedInstanceState == null) {
@@ -71,11 +68,6 @@ open class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaCo
theme.onResume(this) theme.onResume(this)
} }
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putLong(STATE_WATERMARK, shareDataTimestampViewModel.timestamp)
}
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
if (isChangingConfigurations) { if (isChangingConfigurations) {

View File

@@ -1564,7 +1564,7 @@ class ConversationFragment :
} }
private fun handleShareOrDraftData(inputReadyState: InputReadyState, data: ShareOrDraftData) { private fun handleShareOrDraftData(inputReadyState: InputReadyState, data: ShareOrDraftData) {
shareDataTimestampViewModel.timestamp = args.shareDataTimestamp shareDataTimestampViewModel.setTimestampFromConversationArgs(args)
if (inputReadyState.isAnnouncementGroup == true && inputReadyState.isAdmin == false) { 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() Toast.makeText(requireContext(), R.string.MultiselectForwardFragment__only_admins_can_send_messages_to_this_group, Toast.LENGTH_SHORT).show()

View File

@@ -5,12 +5,35 @@
package org.thoughtcrime.securesms.conversation.v2 package org.thoughtcrime.securesms.conversation.v2
import android.content.Intent
import android.os.Bundle
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel 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 * Hold the last share timestamp in an activity scoped view model for sharing between
* the activity and fragments. * the activity and fragments.
*/ */
class ShareDataTimestampViewModel : ViewModel() { class ShareDataTimestampViewModel(
var timestamp: Long = -1L 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
}
} }

View File

@@ -11,7 +11,6 @@ import io.reactivex.rxjava3.core.Observable
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filter 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. * 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<MainNavigationDetailLocation>() private val detailLocationRequestFlow = MutableStateFlow<MainNavigationDetailLocation>(MainNavigationDetailLocation.Empty)
val detailLocationRequests: SharedFlow<MainNavigationDetailLocation> = detailLocationRequestFlow val detailLocationRequests: StateFlow<MainNavigationDetailLocation> = detailLocationRequestFlow
/** /**
* The latest detail location that has been requested, for consumption by other components. * The latest detail location that has been requested, for consumption by other components.