mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-23 12:38:33 +00:00
Add support for conversation intent routing to MainActivity.
This commit is contained in:
committed by
Cody Henthorne
parent
9d593bcaff
commit
ae90b2ecd9
@@ -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"/>
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user