mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-05-03 23:15:44 +01:00
Add initial working E2E flow for MediaSendV3.
This commit is contained in:
committed by
Greyson Parrelli
parent
17def87c17
commit
e2feaaf74c
@@ -482,7 +482,7 @@
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing" />
|
||||
|
||||
<activity
|
||||
android:name="org.signal.mediasend.MediaSendActivity"
|
||||
android:name=".mediasend.v3.MediaSendV3Activity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||
android:exported="false"
|
||||
android:launchMode="singleTop"
|
||||
|
||||
@@ -163,6 +163,7 @@ import org.thoughtcrime.securesms.main.rememberFocusRequester
|
||||
import org.thoughtcrime.securesms.main.storiesNavGraphBuilder
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXRemoteConfig
|
||||
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity
|
||||
import org.thoughtcrime.securesms.mediasend.v3.mediaSendLauncher
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphone
|
||||
import org.thoughtcrime.securesms.megaphone.MegaphoneActionController
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphones
|
||||
@@ -271,7 +272,7 @@ class MainActivity :
|
||||
override val googlePayRepository: GooglePayRepository by lazy { GooglePayRepository(this) }
|
||||
override val googlePayResultPublisher: Subject<GooglePayComponent.GooglePayResult> = PublishSubject.create()
|
||||
|
||||
private lateinit var mediaActivityLauncher: ActivityResultLauncher<MediaSendActivityContract.Args>
|
||||
private lateinit var mediaSendLauncher: ActivityResultLauncher<MediaSendActivityContract.Args>
|
||||
|
||||
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
|
||||
return motionEventRelay.offer(ev) || super.dispatchTouchEvent(ev)
|
||||
@@ -298,7 +299,7 @@ class MainActivity :
|
||||
super.onCreate(savedInstanceState, ready)
|
||||
navigator = MainNavigator(this, mainNavigationViewModel)
|
||||
|
||||
mediaActivityLauncher = registerForActivityResult(MediaSendActivityContract()) { }
|
||||
mediaSendLauncher = mediaSendLauncher()
|
||||
|
||||
AppForegroundObserver.addListener(object : AppForegroundObserver.Listener {
|
||||
override fun onForeground() {
|
||||
@@ -1124,7 +1125,7 @@ class MainActivity :
|
||||
if (isForQuickRestore) {
|
||||
startActivity(MediaSelectionActivity.cameraForQuickRestore(context = this@MainActivity))
|
||||
} else if (SignalStore.internal.useNewMediaActivity) {
|
||||
mediaActivityLauncher.launch(
|
||||
mediaSendLauncher.launch(
|
||||
MediaSendActivityContract.Args(
|
||||
isCameraFirst = false,
|
||||
isStory = destination == MainNavigationListLocation.STORIES
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.mediasend.v3
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.fragment.compose.AndroidFragment
|
||||
import org.signal.mediasend.MediaSendActivityContract
|
||||
import org.signal.mediasend.MediaSendScreen
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity
|
||||
|
||||
/**
|
||||
* Encapsulates the media send flow for v3.
|
||||
*/
|
||||
class MediaSendV3Activity : PassphraseRequiredActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
enableEdgeToEdge()
|
||||
|
||||
val contractArgs = MediaSendActivityContract.Args.fromIntent(intent)
|
||||
|
||||
setContent {
|
||||
MediaSendScreen(
|
||||
contractArgs = contractArgs,
|
||||
sendSlot = {
|
||||
AndroidFragment(
|
||||
clazz = MediaSendV3ForwardFragment::class.java,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.mediasend.v3
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import org.signal.mediasend.MediaSendActivityContract
|
||||
import org.signal.mediasend.StorySendRequirements
|
||||
import org.thoughtcrime.securesms.stories.Stories
|
||||
|
||||
private fun contract(): ActivityResultContract<MediaSendActivityContract.Args, MediaSendActivityContract.Result?> = MediaSendActivityContract(MediaSendV3Activity::class.java)
|
||||
|
||||
fun Fragment.mediaSendLauncher(callback: (MediaSendActivityContract.Result?) -> Unit = {}): ActivityResultLauncher<MediaSendActivityContract.Args> = registerForActivityResult(contract(), callback)
|
||||
|
||||
fun AppCompatActivity.mediaSendLauncher(callback: (MediaSendActivityContract.Result?) -> Unit = {}): ActivityResultLauncher<MediaSendActivityContract.Args> = registerForActivityResult(contract(), callback)
|
||||
|
||||
/**
|
||||
* Maps the feature-module [StorySendRequirements] to the app-layer [Stories.MediaTransform.SendRequirements].
|
||||
*/
|
||||
fun StorySendRequirements.toAppSendRequirements(): Stories.MediaTransform.SendRequirements = when (this) {
|
||||
StorySendRequirements.CAN_SEND -> Stories.MediaTransform.SendRequirements.VALID_DURATION
|
||||
StorySendRequirements.CAN_NOT_SEND -> Stories.MediaTransform.SendRequirements.CAN_NOT_SEND
|
||||
StorySendRequirements.REQUIRES_CROP -> Stories.MediaTransform.SendRequirements.REQUIRES_CLIP
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.mediasend.v3
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.util.getParcelableArrayListCompat
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.mediasend.MediaRecipientId
|
||||
import org.signal.mediasend.MediaSendActivityContract
|
||||
import org.signal.mediasend.MediaSendState
|
||||
import org.signal.mediasend.MediaSendViewModel
|
||||
import org.signal.mediasend.SendResult
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
|
||||
import org.thoughtcrime.securesms.stories.Stories
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
|
||||
/**
|
||||
* View-backed wrapper around [MultiselectForwardFragment] that provides the [ViewGroup] container
|
||||
* required by [MultiselectForwardFragment.Callback.getContainer] for bottom bar inflation.
|
||||
*
|
||||
* Implements the callback interface and uses the shared [MediaSendViewModel] to drive
|
||||
* the send flow forward.
|
||||
*/
|
||||
class MediaSendV3ForwardFragment : Fragment(R.layout.multiselect_forward_activity), MultiselectForwardFragment.Callback {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(MediaSendV3ForwardFragment::class.java)
|
||||
}
|
||||
|
||||
private val viewModel: MediaSendViewModel by activityViewModels {
|
||||
MediaSendViewModel.Factory(args = MediaSendActivityContract.Args.fromIntent(requireActivity().intent))
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
val state = viewModel.state.value
|
||||
val forwardFragment = MultiselectForwardFragment.create(
|
||||
MultiselectForwardFragmentArgs(
|
||||
title = R.string.MediaReviewFragment__send_to,
|
||||
storySendRequirements = state.storySendRequirements.toAppSendRequirements(),
|
||||
isSearchEnabled = !state.isStory,
|
||||
isViewOnce = state.viewOnceToggleState == MediaSendState.ViewOnceToggleState.ONCE
|
||||
)
|
||||
)
|
||||
|
||||
childFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragment_container, forwardFragment)
|
||||
.commitNow()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFinishForwardAction() = Unit
|
||||
|
||||
override fun exitFlow() {
|
||||
requireActivity().finish()
|
||||
}
|
||||
|
||||
override fun onSearchInputFocused() = Unit
|
||||
|
||||
override fun setResult(bundle: Bundle) {
|
||||
val selectedRecipients: List<ContactSearchKey.RecipientSearchKey> = bundle.getParcelableArrayListCompat(MultiselectForwardFragment.RESULT_SELECTION, ContactSearchKey.RecipientSearchKey::class.java)
|
||||
?: emptyList()
|
||||
|
||||
val recipientIds = selectedRecipients.map { MediaRecipientId(it.recipientId.toLong()) }
|
||||
viewModel.setAdditionalRecipients(recipientIds)
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
when (val result = viewModel.send()) {
|
||||
is SendResult.Success -> {
|
||||
Log.d(TAG, "Send completed successfully.")
|
||||
requireActivity().finish()
|
||||
}
|
||||
is SendResult.Error -> {
|
||||
Log.w(TAG, "Send failed: ${result.message}")
|
||||
requireActivity().finish()
|
||||
}
|
||||
is SendResult.UntrustedIdentity -> {
|
||||
Log.w(TAG, "Send failed due to untrusted identities.")
|
||||
SafetyNumberBottomSheet
|
||||
.forRecipientIdsAndDestinations(result.recipientIds.map { RecipientId.from(it) }, selectedRecipients)
|
||||
.show(childFragmentManager)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getContainer(): ViewGroup {
|
||||
return requireView().findViewById(R.id.fragment_container_wrapper)
|
||||
}
|
||||
|
||||
override fun getDialogBackgroundColor(): Int {
|
||||
return ContextCompat.getColor(requireContext(), CoreUiR.color.signal_colorBackground)
|
||||
}
|
||||
|
||||
override fun getStorySendRequirements(): Stories.MediaTransform.SendRequirements? {
|
||||
return viewModel.getStorySendRequirements().toAppSendRequirements()
|
||||
}
|
||||
}
|
||||
@@ -93,9 +93,6 @@ object MediaSendV3Repository : MediaSendRepository {
|
||||
return@withContext SendResult.Error("No recipients provided.")
|
||||
}
|
||||
|
||||
val singleContact = if (recipients.size == 1) recipients.first() else null
|
||||
val contacts = if (recipients.size > 1) recipients else emptyList()
|
||||
|
||||
val legacyEditorStateMap = mapLegacyEditorState(request.editorStateMap)
|
||||
val quality = SentMediaQuality.fromCode(request.quality)
|
||||
|
||||
@@ -106,8 +103,8 @@ object MediaSendV3Repository : MediaSendRepository {
|
||||
quality = quality,
|
||||
message = request.message,
|
||||
isViewOnce = request.isViewOnce,
|
||||
singleContact = singleContact,
|
||||
contacts = contacts,
|
||||
singleContact = null,
|
||||
contacts = recipients,
|
||||
mentions = emptyList(),
|
||||
bodyRanges = null,
|
||||
sendType = resolveSendType(request.sendType),
|
||||
|
||||
@@ -122,6 +122,17 @@ object SafetyNumberBottomSheet {
|
||||
return SheetFactory(args)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a factory to generate a sheet for the given recipient IDs and destinations.
|
||||
*
|
||||
* @param recipientIds The list of untrusted recipient IDs
|
||||
* @param destinations The list of locations the user was trying to send content
|
||||
*/
|
||||
@JvmStatic
|
||||
fun forRecipientIdsAndDestinations(recipientIds: List<RecipientId>, destinations: List<ContactSearchKey.RecipientSearchKey>): Factory {
|
||||
return SheetFactory(SafetyNumberBottomSheetArgs(recipientIds, destinations))
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a factory to generate a sheet for the given identity records and single destination.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user