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:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout"
android:windowSoftInputMode="stateUnchanged"
android:resizeableActivity="true"
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.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)

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.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())

View File

@@ -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())
}
)

View File

@@ -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<? extends Activity> 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)

View File

@@ -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) {

View File

@@ -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()

View File

@@ -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
}
}

View File

@@ -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<MainNavigationDetailLocation>()
val detailLocationRequests: SharedFlow<MainNavigationDetailLocation> = detailLocationRequestFlow
private val detailLocationRequestFlow = MutableStateFlow<MainNavigationDetailLocation>(MainNavigationDetailLocation.Empty)
val detailLocationRequests: StateFlow<MainNavigationDetailLocation> = detailLocationRequestFlow
/**
* The latest detail location that has been requested, for consumption by other components.