diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/main/MainNavigationLaunchTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/main/MainNavigationLaunchTest.kt index 71ffd211ea..4c848a8192 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/main/MainNavigationLaunchTest.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/main/MainNavigationLaunchTest.kt @@ -13,7 +13,12 @@ import android.graphics.Bitmap import android.net.Uri import android.os.Bundle import android.view.View +import android.view.ViewGroup import android.widget.TextView +import androidx.compose.ui.node.RootForTest +import androidx.compose.ui.semantics.SemanticsProperties +import androidx.compose.ui.semantics.getAllSemanticsNodes +import androidx.compose.ui.semantics.getOrNull import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentManager @@ -33,7 +38,7 @@ import org.thoughtcrime.securesms.conversation.v2.ConversationFragment import org.thoughtcrime.securesms.conversationlist.ConversationListArchiveFragment import org.thoughtcrime.securesms.conversationlist.ConversationListFragment import org.thoughtcrime.securesms.dependencies.AppDependencies -import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity +import org.thoughtcrime.securesms.mediasend.v3.MediaSendV3Activity import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.stories.landing.StoriesLandingFragment @@ -42,6 +47,7 @@ import java.io.ByteArrayOutputStream import java.util.Collections import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit +import org.signal.mediasend.R as MediaSendR /** * End-to-end launch tests for [MainActivity], covering cold-launch and onNewIntent paths @@ -121,9 +127,9 @@ class MainNavigationLaunchTest { /** * Image-share cold-launch: the dispatch path through `ShareOrDraftData.StartSendMedia` * that hops the user from the conversation into the media-send screen - * ([MediaSelectionActivity]). Asserts that the secondary activity actually launches and - * that its [MediaReviewFragment] surfaces the recipient's display name in the top - * corner — i.e. it knows who the share is targeted at. + * ([MediaSendV3Activity]). Asserts that the secondary activity actually launches and that + * its edit screen's summary pill names both the recipient and the shared photo — i.e. it + * knows who the share is targeted at and what is being sent. */ @Test fun coldLaunch_shareImageIntent_opensMediaSendForRecipient() { @@ -131,13 +137,18 @@ class MainNavigationLaunchTest { val intent = shareImageIntent(recipient = recipient, media = media) launchSync(intent).use { launched -> - val mediaSend = launched.awaitActivity(MediaSelectionActivity::class.java, timeoutMs = 20_000) - val expectedName = runOnMainSync { Recipient.resolved(recipient).getDisplayName(context) } + launched.awaitActivity(MediaSendV3Activity::class.java, timeoutMs = 20_000) - await(timeoutMs = 15_000, description = "recipient label populated in MediaReviewFragment") { - // await() already runs the predicate on the main thread; nesting another - // runOnMainSync here would throw "can not be called from the main application thread". - mediaSend.findViewById(R.id.recipient)?.text?.toString() == expectedName + val expectedName = runOnMainSync { Recipient.resolved(recipient).getDisplayName(context) } + val expectedMedia = context.resources.getQuantityString(MediaSendR.plurals.MediaEditScreen__photo, 1, 1) + + await(timeoutMs = 15_000, description = "media editor summary pill showing \"$expectedName\" / \"$expectedMedia\"") { + // Re-resolve each poll rather than closing over the awaitActivity instance: the flow + // toggles requestedOrientation as it settles, and a recreated activity would leave us + // reading a dead composition. + val mediaSend = launched.latestActivity(MediaSendV3Activity::class.java) ?: return@await false + val texts = mediaSend.composeTexts() + expectedName in texts && expectedMedia in texts } // Exactly one ConversationFragment should have been created — the share dispatch @@ -580,10 +591,9 @@ class MainNavigationLaunchTest { } /** - * Build a [Media] backed by a real 1×1 JPEG. The media-send screen attempts to decode - * the image during MediaReviewFragment setup, so a fake byte array won't survive — we - * need genuine JPEG bytes for the fragment to reach the state where `R.id.recipient` - * is populated. + * Build a [Media] backed by a real 1×1 JPEG. The media editor decodes each page to render + * it, so genuine JPEG bytes keep the screen in the state a real share would put it in + * rather than one recovering from a decode failure. */ private fun realJpegMedia(): Media { val bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) @@ -739,6 +749,32 @@ class MainNavigationLaunchTest { MainNavigationListLocation.STORIES -> StoriesLandingFragment::class.java } + /** + * Every string the activity's Compose hierarchy is currently rendering, read straight off the + * semantics tree. Media send is Compose end-to-end, so there is no view to findViewById; and the + * looper never goes idle here (see [launchSync]), which rules out the idle-synchronized matchers + * of a Compose test rule. Main-thread read — [await] already provides that. + */ + private fun Activity.composeTexts(): List { + return window.decorView.composeRoots() + .flatMap { it.semanticsOwner.getAllSemanticsNodes(mergingEnabled = false) } + .flatMap { it.config.getOrNull(SemanticsProperties.Text).orEmpty() } + .map { it.text } + } + + private fun View.composeRoots(): List { + val roots = mutableListOf() + if (this is RootForTest) { + roots += this + } + if (this is ViewGroup) { + for (i in 0 until childCount) { + roots += getChildAt(i).composeRoots() + } + } + return roots + } + private fun awaitListFragment(launched: LaunchedActivity, location: MainNavigationListLocation) { val expected = listFragmentClass(location) try { @@ -808,17 +844,20 @@ class MainNavigationLaunchTest { */ val activity: MainActivity get() = checkNotNull(activityHolder[0]) { "No active MainActivity" } + /** Most-recently-created, not-yet-destroyed activity of [clazz], or null. */ + fun latestActivity(clazz: Class): T? = synchronized(allActivities) { + allActivities.lastOrNull { clazz.isInstance(it) }?.let { clazz.cast(it) } + } + /** * Poll until an activity of [clazz] has been created, then return it. Used to assert - * the share-image flow's hop into MediaSelectionActivity. + * the share-image flow's hop into [MediaSendV3Activity]. */ fun awaitActivity(clazz: Class, timeoutMs: Long = 10_000): T { val deadline = System.currentTimeMillis() + timeoutMs while (System.currentTimeMillis() < deadline) { - val match = synchronized(allActivities) { - allActivities.firstOrNull { clazz.isInstance(it) } - } - if (match != null) return clazz.cast(match)!! + val match = latestActivity(clazz) + if (match != null) return match Thread.sleep(50) } val seen = synchronized(allActivities) { allActivities.map { it::class.simpleName } } @@ -830,7 +869,7 @@ class MainNavigationLaunchTest { } override fun close() { - // Don't wait for looper idle — secondary activities (e.g. MediaSelectionActivity + // Don't wait for looper idle — secondary activities (e.g. MediaSendV3Activity // opened by share processing) can keep it busy indefinitely. Finish every tracked // activity so subsequent tests start from a clean slate. val toFinish = synchronized(allActivities) { allActivities.toList() } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6534be85a9..bbe6e02dfa 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -466,15 +466,6 @@ android:theme="@style/Theme.Signal.DayNight.NoActionBar" android:windowSoftInputMode="stateHidden" /> - - () { override fun createIntent(context: Context, input: MediaSelectionInput): Intent { val (media, recipientId, text) = input - return MediaSendLauncher.editor(context, MessageSendType.SignalMessageSendType, media, recipientId, text) + return MediaSendLauncher.editor(context, media, recipientId, text) } override fun parseResult(resultCode: Int, intent: Intent?): MediaSendActivityResult? { @@ -132,7 +132,7 @@ class ConversationActivityResultContracts(private val fragment: Fragment, privat private object MediaCapture : ActivityResultContract() { override fun createIntent(context: Context, input: MediaSelectionInput): Intent { val (_, recipientId, _, isReply) = input - return MediaSendLauncher.camera(context, MessageSendType.SignalMessageSendType, recipientId, isReply) + return MediaSendLauncher.camera(context, recipientId, isReply) } override fun parseResult(resultCode: Int, intent: Intent?): MediaSendActivityResult? { @@ -143,7 +143,7 @@ class ConversationActivityResultContracts(private val fragment: Fragment, privat private object MediaGallery : ActivityResultContract() { override fun createIntent(context: Context, input: MediaSelectionInput): Intent { val (media, recipientId, text, isReply) = input - return MediaSendLauncher.gallery(context, MessageSendType.SignalMessageSendType, media, recipientId, text, isReply) + return MediaSendLauncher.gallery(context, media, recipientId, text, isReply) } override fun parseResult(resultCode: Int, intent: Intent?): MediaSendActivityResult? { diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java index ce9ab27caf..96b51fa027 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java @@ -131,7 +131,7 @@ public class GiphyActivity extends PassphraseRequiredActivity implements Keyboar } Media media = new Media(success.getBlobUri(), mimeType, 0, success.getWidth(), success.getHeight(), 0, 0, false, true, null, null, null, null); - startActivityForResult(MediaSendLauncher.editor(this, sendType, Collections.singletonList(media), recipientId, text), MEDIA_SENDER); + startActivityForResult(MediaSendLauncher.editor(this, Collections.singletonList(media), recipientId, text), MEDIA_SENDER); } private void handleGiphyMp4ErrorResult(@NonNull GiphyMp4SaveResult.Error error) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.kt index 42bc5bb300..ff88f043bb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.kt @@ -41,7 +41,6 @@ class InternalValues internal constructor(store: KeyValueStore) : SignalStoreVal const val SHOW_ARCHIVE_STATE_HINT: String = "internal.show_archive_state_hint" const val INCLUDE_DEBUGLOG_IN_BACKUP: String = "internal.include_debuglog_in_backup" const val IMPORTED_BACKUP_DEBUG_INFO: String = "internal.imported_backup_debug_info" - const val USE_NEW_MEDIA_ACTIVITY: String = "internal.use_new_media_activity" const val ANR_DETECTION_CRASH: String = "internal.anr_detection_crash" const val ISSUE_NOTIFICATION_PRIORITY: String = "internal.issue_notification_priority" const val ISSUE_NOTIFY_TIMES: String = "internal.issue_notify_times" @@ -61,19 +60,6 @@ class InternalValues internal constructor(store: KeyValueStore) : SignalStoreVal */ var forceSinglePane by booleanValue(FORCE_SINGLE_PANE_ON_ALL_DEVICES, false).falseForExternalUsers() - /** - * Whether to use the new media-send flow. Internal users can override the remote value. - */ - var useNewMediaActivity: Boolean - get() = if (RemoteConfig.internalUser) { - getBoolean(USE_NEW_MEDIA_ACTIVITY, RemoteConfig.useNewMediaSendFlow) - } else { - RemoteConfig.useNewMediaSendFlow - } - set(value) { - putBoolean(USE_NEW_MEDIA_ACTIVITY, value) - } - /** * Members will not be added directly to a GV2 even if they could be. */ diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/FlipTransformation.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/FlipTransformation.java deleted file mode 100644 index 00588f199d..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/FlipTransformation.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.thoughtcrime.securesms.mediasend; - -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Matrix; - -import androidx.annotation.NonNull; - -import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; -import com.bumptech.glide.load.resource.bitmap.BitmapTransformation; - -import java.security.MessageDigest; - -public class FlipTransformation extends BitmapTransformation { - - @Override - protected Bitmap transform(@NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight) { - Bitmap output = pool.get(toTransform.getWidth(), toTransform.getHeight(), toTransform.getConfig()); - - Canvas canvas = new Canvas(output); - Matrix matrix = new Matrix(); - matrix.setScale(-1, 1); - matrix.postTranslate(toTransform.getWidth(), 0); - - canvas.drawBitmap(toTransform, matrix, null); - - return output; - } - - @Override - public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { - messageDigest.update(FlipTransformation.class.getSimpleName().getBytes()); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendConstants.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendConstants.java deleted file mode 100644 index 411fd9c8bd..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendConstants.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.thoughtcrime.securesms.mediasend; - -public class MediaSendConstants { - public static final int MAX_PUSH = 32; - public static final int MAX_SMS = 1; -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendDocumentFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendDocumentFragment.kt deleted file mode 100644 index c3c50661de..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendDocumentFragment.kt +++ /dev/null @@ -1,92 +0,0 @@ -package org.thoughtcrime.securesms.mediasend - -import android.net.Uri -import android.os.Bundle -import android.view.View -import android.widget.TextView -import android.widget.Toast -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import org.signal.core.models.media.Media -import org.signal.core.util.bytes -import org.signal.core.util.getParcelableCompat -import org.signal.core.util.logging.Log -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.mediasend.MediaSendDocumentViewModel.DocumentInfo -import org.signal.core.ui.R as CoreUiR - -/** - * Fragment to show full screen document attachments - */ -class MediaSendDocumentFragment : Fragment(R.layout.mediasend_document_fragment), MediaSendPageFragment { - - companion object { - private val TAG = Log.tag(MediaSendDocumentFragment::class.java) - - private const val KEY_MEDIA = "media" - - fun newInstance(media: Media): MediaSendDocumentFragment { - val args = Bundle() - args.putParcelable(KEY_MEDIA, media) - - val fragment = MediaSendDocumentFragment() - fragment.arguments = args - fragment.uri = media.uri - return fragment - } - } - - private lateinit var uri: Uri - private lateinit var media: Media - - private val viewModel: MediaSendDocumentViewModel by viewModels { - MediaSendDocumentViewModel.Factory(media) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - val name: TextView = view.findViewById(R.id.name) - val size: TextView = view.findViewById(R.id.size) - val extension: TextView = view.findViewById(R.id.extension) - - this.media = requireNotNull(requireArguments().getParcelableCompat(KEY_MEDIA, Media::class.java)) - - viewModel.documentInfo.observe(viewLifecycleOwner) { documentInfoOptional -> - val documentInfo: DocumentInfo? = documentInfoOptional.orElse(null) - if (documentInfo != null) { - media.fileName = documentInfo.fileName - - name.text = documentInfo.fileName ?: getString(R.string.DocumentView_unnamed_file) - size.text = documentInfo.fileSize.bytes.toUnitString() - - if (documentInfo.extension.length <= 3) { - extension.text = documentInfo.extension - extension.setTextAppearance(requireContext(), CoreUiR.style.Signal_Text_BodySmall) - } else if (documentInfo.extension.length == 4) { - extension.text = documentInfo.extension - extension.setTextAppearance(requireContext(), CoreUiR.style.Signal_Text_Caption) - } - } else { - Toast.makeText(requireContext(), R.string.ConversationActivity_sorry_there_was_an_error_setting_your_attachment, Toast.LENGTH_SHORT).show() - requireActivity().finishAfterTransition() - } - } - - viewModel.loadDocumentInfo() - } - - override fun getUri(): Uri { - return uri - } - - override fun setUri(uri: Uri) { - this.uri = uri - } - - override fun saveState(): Any = Unit - - override fun restoreState(state: Any) = Unit - - override fun notifyHidden() = Unit -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendDocumentViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendDocumentViewModel.kt deleted file mode 100644 index 6813c7235f..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendDocumentViewModel.kt +++ /dev/null @@ -1,105 +0,0 @@ -package org.thoughtcrime.securesms.mediasend - -import android.content.Context -import android.database.Cursor -import android.net.Uri -import android.provider.OpenableColumns -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.signal.core.models.media.Media -import org.signal.core.util.logging.Log -import org.thoughtcrime.securesms.dependencies.AppDependencies -import org.thoughtcrime.securesms.mms.PartAuthority -import org.thoughtcrime.securesms.util.MediaUtil -import java.io.IOException -import java.util.Optional - -class MediaSendDocumentViewModel(private val media: Media) : ViewModel() { - - private val internalDocumentInfo = MutableLiveData>() - val documentInfo: LiveData> = internalDocumentInfo - - fun loadDocumentInfo() { - viewModelScope.launch { - internalDocumentInfo.value = withContext(Dispatchers.IO) { - Optional.ofNullable(computeDocumentInfo()) - } - } - } - - private fun computeDocumentInfo(): DocumentInfo? { - val context = AppDependencies.application - val fileInfo: Pair = getFileInfo(context) ?: return null - - val extensionText: String = MediaUtil.getFileType(context, Optional.ofNullable(fileInfo.first), media.uri).orElse("") - - return DocumentInfo(fileInfo.first, fileInfo.second, extensionText) - } - - private fun getFileInfo(context: Context): Pair? { - val uri = media.uri - return try { - if (PartAuthority.isLocalUri(uri)) { - getManuallyCalculatedFileInfo(context, uri) - } else { - getContentResolverFileInfo(context, uri) ?: getManuallyCalculatedFileInfo(context, uri) - } - } catch (e: IOException) { - Log.w(TAG, e) - null - } - } - - @Throws(IOException::class) - private fun getManuallyCalculatedFileInfo(context: Context, uri: Uri): Pair { - var fileName: String? = null - var fileSize: Long? = null - - if (PartAuthority.isLocalUri(uri)) { - fileSize = PartAuthority.getAttachmentSize(context, uri) - fileName = PartAuthority.getAttachmentFileName(context, uri) - } - if (fileSize == null) { - fileSize = MediaUtil.getMediaSize(context, uri) - } - - return Pair(fileName, fileSize) - } - - private fun getContentResolverFileInfo(context: Context, uri: Uri): Pair? { - var cursor: Cursor? = null - - try { - cursor = context.contentResolver.query(uri, null, null, null, null) - - if (cursor != null && cursor.moveToFirst()) { - val fileName = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)) - val fileSize = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE)) - - return Pair(fileName, fileSize) - } - } finally { - cursor?.close() - } - - return null - } - - data class DocumentInfo(val fileName: String?, val fileSize: Long, val extension: String) - - class Factory(private val media: Media) : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - return requireNotNull(modelClass.cast(MediaSendDocumentViewModel(media))) - } - } - - companion object { - private val TAG = Log.tag(MediaSendDocumentViewModel::class.java) - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendGifFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendGifFragment.java deleted file mode 100644 index b050bb3987..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendGifFragment.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.thoughtcrime.securesms.mediasend; - -import android.net.Uri; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; - -import com.bumptech.glide.Glide; - -import org.thoughtcrime.securesms.R; -import org.signal.glide.decryptableuri.DecryptableUri; - -public class MediaSendGifFragment extends Fragment implements MediaSendPageFragment { - - private static final String KEY_URI = "uri"; - - private Uri uri; - - public static MediaSendGifFragment newInstance(@NonNull Uri uri) { - Bundle args = new Bundle(); - args.putParcelable(KEY_URI, uri); - - MediaSendGifFragment fragment = new MediaSendGifFragment(); - fragment.setArguments(args); - fragment.setUri(uri); - return fragment; - } - - @Override - public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - return inflater.inflate(R.layout.mediasend_image_fragment, container, false); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - uri = getArguments().getParcelable(KEY_URI); - Glide.with(this).load(new DecryptableUri(uri)).fitCenter().into((ImageView) view); - } - - @Override - public void setUri(@NonNull Uri uri) { - this.uri = uri; - } - - @Override - public @NonNull Uri getUri() { - return uri; - } - - @Override - public @Nullable Object saveState() { - return null; - } - - @Override - public void restoreState(@NonNull Object state) { } - - @Override - public void notifyHidden() { - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendLauncher.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendLauncher.kt index 6bf90346d0..70c32125f2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendLauncher.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendLauncher.kt @@ -15,26 +15,15 @@ import org.signal.mediasend.MediaRecipientId import org.signal.mediasend.MediaSendFlowActivityContract import org.signal.mediasend.MediaSendRecipient import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey -import org.thoughtcrime.securesms.conversation.MessageSendType -import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity import org.thoughtcrime.securesms.mediasend.v3.MediaSendV3Activity import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.util.RemoteConfig /** * Single entry point for launching the media send flow. - * - * Callers describe the flow they want, and this decides whether to launch the v2 [MediaSelectionActivity] or the - * v3 [MediaSendV3Activity], translating the request into that implementation's arguments. - * - * Every flow is expressible by both implementations, so the choice is purely [useV3]. */ object MediaSendLauncher { - private val useV3: Boolean - get() = SignalStore.internal.useNewMediaActivity - @JvmStatic fun camera(context: Context): Intent { return camera(context, isStory = false) @@ -42,161 +31,125 @@ object MediaSendLauncher { @JvmStatic fun camera(context: Context, isStory: Boolean): Intent { - return if (useV3) { - v3Intent( - context, - MediaSendFlowActivityContract.Args( - isCameraFirst = true, - mode = MediaSendFlowActivityContract.Mode.ChooseAfterMediaSelection, - isStory = isStory - ) + return v3Intent( + context, + MediaSendFlowActivityContract.Args( + isCameraFirst = true, + mode = MediaSendFlowActivityContract.Mode.ChooseAfterMediaSelection, + isStory = isStory ) - } else { - MediaSelectionActivity.camera(context, isStory) - } + ) } @JvmStatic fun camera( context: Context, - messageSendType: MessageSendType, recipientId: RecipientId, isReply: Boolean ): Intent { - return if (useV3) { - v3Intent( - context, - MediaSendFlowActivityContract.Args( - isCameraFirst = true, - mode = MediaSendFlowActivityContract.Mode.SingleRecipient, - recipientId = recipientId.toMediaRecipientId(), - isReply = isReply - ) + return v3Intent( + context, + MediaSendFlowActivityContract.Args( + isCameraFirst = true, + mode = MediaSendFlowActivityContract.Mode.SingleRecipient, + recipientId = recipientId.toMediaRecipientId(), + isReply = isReply ) - } else { - MediaSelectionActivity.camera(context, messageSendType, recipientId, isReply) - } + ) } fun cameraForQuickRestore(context: Context): Intent { - return if (useV3) { - v3Intent( - context, - MediaSendFlowActivityContract.Args( - isCameraFirst = true, - mode = MediaSendFlowActivityContract.Mode.ChooseAfterMediaSelection, - isForQuickRestore = true - ) + return v3Intent( + context, + MediaSendFlowActivityContract.Args( + isCameraFirst = true, + mode = MediaSendFlowActivityContract.Mode.ChooseAfterMediaSelection, + isForQuickRestore = true ) - } else { - MediaSelectionActivity.cameraForQuickRestore(context) - } + ) } fun addToGroupStory(context: Context, recipientId: RecipientId): Intent { - return if (useV3) { - v3Intent( - context, - MediaSendFlowActivityContract.Args( - isCameraFirst = true, - mode = MediaSendFlowActivityContract.Mode.SingleRecipient, - recipientId = recipientId.toMediaRecipientId(), - isStory = true, - isAddToGroupStoryFlow = true - ) + return v3Intent( + context, + MediaSendFlowActivityContract.Args( + isCameraFirst = true, + mode = MediaSendFlowActivityContract.Mode.SingleRecipient, + recipientId = recipientId.toMediaRecipientId(), + isStory = true, + isAddToGroupStoryFlow = true ) - } else { - MediaSelectionActivity.addToGroupStory(context, recipientId) - } + ) } @JvmStatic fun gallery( context: Context, - messageSendType: MessageSendType, media: List, recipientId: RecipientId, message: CharSequence?, isReply: Boolean ): Intent { - return if (useV3) { - v3Intent( - context, - MediaSendFlowActivityContract.Args( - mode = MediaSendFlowActivityContract.Mode.SingleRecipient, - recipientId = recipientId.toMediaRecipientId(), - initialMedia = media, - initialMessage = message, - isReply = isReply - ) + return v3Intent( + context, + MediaSendFlowActivityContract.Args( + mode = MediaSendFlowActivityContract.Mode.SingleRecipient, + recipientId = recipientId.toMediaRecipientId(), + initialMedia = media, + initialMessage = message, + isReply = isReply ) - } else { - MediaSelectionActivity.gallery(context, messageSendType, media, recipientId, message, isReply) - } + ) } @JvmStatic fun editor( context: Context, - messageSendType: MessageSendType, media: List, recipientId: RecipientId, message: CharSequence? ): Intent { - return if (useV3) { - v3Intent( - context, - MediaSendFlowActivityContract.Args( - mode = MediaSendFlowActivityContract.Mode.SingleRecipient, - recipientId = recipientId.toMediaRecipientId(), - initialMedia = media, - initialMessage = message - ) + return v3Intent( + context, + MediaSendFlowActivityContract.Args( + mode = MediaSendFlowActivityContract.Mode.SingleRecipient, + recipientId = recipientId.toMediaRecipientId(), + initialMedia = media, + initialMessage = message ) - } else { - MediaSelectionActivity.editor(context, messageSendType, media, recipientId, message) - } + ) } @JvmStatic fun editor(context: Context, media: List): Intent { - return if (useV3) { - v3Intent( - context, - MediaSendFlowActivityContract.Args( - mode = MediaSendFlowActivityContract.Mode.ChooseAfterMediaSelection, - initialMedia = media - ) + return v3Intent( + context, + MediaSendFlowActivityContract.Args( + mode = MediaSendFlowActivityContract.Mode.ChooseAfterMediaSelection, + initialMedia = media ) - } else { - MediaSelectionActivity.editor(context, media) - } + ) } @JvmStatic fun share( context: Context, - messageSendType: MessageSendType, media: List, recipientSearchKeys: List, message: CharSequence?, asTextStory: Boolean ): Intent { - return if (useV3) { - v3Intent( - context, - MediaSendFlowActivityContract.Args( - mode = MediaSendFlowActivityContract.Mode.MultiRecipient, - additionalRecipients = recipientSearchKeys.map { MediaSendRecipient(it.recipientId.toMediaRecipientId(), it.isStory) }, - initialMedia = media, - initialMessage = message, - isStory = recipientSearchKeys.any { it.isStory }, - asTextStory = asTextStory - ) + return v3Intent( + context, + MediaSendFlowActivityContract.Args( + mode = MediaSendFlowActivityContract.Mode.MultiRecipient, + additionalRecipients = recipientSearchKeys.map { MediaSendRecipient(it.recipientId.toMediaRecipientId(), it.isStory) }, + initialMedia = media, + initialMessage = message, + isStory = recipientSearchKeys.any { it.isStory }, + asTextStory = asTextStory ) - } else { - MediaSelectionActivity.share(context, messageSendType, media, recipientSearchKeys, message, asTextStory) - } + ) } /** diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendPageFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendPageFragment.java deleted file mode 100644 index f08e3b705d..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendPageFragment.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.thoughtcrime.securesms.mediasend; - -import android.net.Uri; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -/** - * A page that sits in the {@link MediaSendFragmentPagerAdapter}. - */ -public interface MediaSendPageFragment { - - @NonNull Uri getUri(); - - void setUri(@NonNull Uri uri); - - @Nullable Object saveState(); - - void restoreState(@NonNull Object state); - - void notifyHidden(); -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/OrderEnforcer.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/OrderEnforcer.java deleted file mode 100644 index 65974ac0c1..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/OrderEnforcer.java +++ /dev/null @@ -1,91 +0,0 @@ -package org.thoughtcrime.securesms.mediasend; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Stack; - -@SuppressWarnings("ConstantConditions") -public class OrderEnforcer { - - private final Map stages = new LinkedHashMap<>(); - - public OrderEnforcer(@NonNull E... stages) { - for (E stage : stages) { - this.stages.put(stage, new StageDetails()); - } - } - - public synchronized void run(@NonNull E stage, Runnable r) { - if (isCompletedThrough(stage)) { - r.run(); - } else { - stages.get(stage).addAction(r); - } - } - - public synchronized void markCompleted(@NonNull E stage) { - stages.get(stage).markCompleted(); - - for (E s : stages.keySet()) { - StageDetails details = stages.get(s); - - if (details.isCompleted()) { - while (details.hasAction()) { - details.popAction().run(); - } - } else { - break; - } - } - } - - public synchronized void reset() { - for (StageDetails details : stages.values()) { - details.reset(); - } - } - - private boolean isCompletedThrough(@NonNull E stage) { - for (E s : stages.keySet()) { - if (s.equals(stage)) { - return stages.get(s).isCompleted(); - } else if (!stages.get(s).isCompleted()) { - return false; - } - } - return false; - } - - private static class StageDetails { - private boolean completed = false; - private Stack actions = new Stack<>(); - - boolean hasAction() { - return !actions.isEmpty(); - } - - @Nullable Runnable popAction() { - return actions.pop(); - } - - void addAction(@NonNull Runnable runnable) { - actions.push(runnable); - } - - void reset() { - actions.clear(); - completed = false; - } - - boolean isCompleted() { - return completed; - } - - void markCompleted() { - completed = true; - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/RotationListener.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/RotationListener.kt deleted file mode 100644 index 8d21f5de27..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/RotationListener.kt +++ /dev/null @@ -1,49 +0,0 @@ -package org.thoughtcrime.securesms.mediasend - -import android.content.Context -import android.view.OrientationEventListener -import android.view.Surface -import io.reactivex.rxjava3.subjects.BehaviorSubject -import io.reactivex.rxjava3.subjects.Subject - -/** - * Utilizes the OrientationEventListener to determine relative surface rotation. - * - * @param context A context, which will be held on to for the lifespan of the listener. - */ -class RotationListener( - context: Context -) : OrientationEventListener(context) { - - private val subject: Subject = BehaviorSubject.create() - - /** - * Observes the stream of orientation changes. This can emit a lot of data, as it does - * not perform any duplication. - */ - val observable = subject - .doOnSubscribe { enable() } - .doOnTerminate { disable() } - - override fun onOrientationChanged(orientation: Int) { - subject.onNext( - when { - orientation == ORIENTATION_UNKNOWN -> Rotation.ROTATION_0 - orientation > 315 || orientation < 45 -> Rotation.ROTATION_0 - orientation < 135 -> Rotation.ROTATION_270 - orientation < 225 -> Rotation.ROTATION_180 - else -> Rotation.ROTATION_90 - } - ) - } - - /** - * Expresses the rotation as a handy enum. - */ - enum class Rotation(val surfaceRotation: Int) { - ROTATION_0(Surface.ROTATION_0), - ROTATION_90(Surface.ROTATION_90), - ROTATION_180(Surface.ROTATION_180), - ROTATION_270(Surface.ROTATION_270) - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/SimpleAnimationListener.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/SimpleAnimationListener.java deleted file mode 100644 index d16be50e9e..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/SimpleAnimationListener.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.thoughtcrime.securesms.mediasend; - -import android.view.animation.Animation; - -/** - * Basic implementation of {@link android.view.animation.Animation.AnimationListener} with empty - * implementation so you don't have to override every method. - */ -public class SimpleAnimationListener implements Animation.AnimationListener { - @Override - public void onAnimationStart(Animation animation) { - } - - @Override - public void onAnimationEnd(Animation animation) { - } - - @Override - public void onAnimationRepeat(Animation animation) { - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXFlashToggleView.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXFlashToggleView.java deleted file mode 100644 index e1aeb43f86..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXFlashToggleView.java +++ /dev/null @@ -1,160 +0,0 @@ -package org.thoughtcrime.securesms.mediasend.camerax; - -import android.content.Context; -import android.os.Bundle; -import android.os.Parcelable; -import android.util.AttributeSet; - -import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; -import androidx.appcompat.widget.AppCompatImageView; -import androidx.camera.core.ImageCapture; - -import org.thoughtcrime.securesms.R; - -import java.util.Arrays; -import java.util.List; - -public final class CameraXFlashToggleView extends AppCompatImageView { - - private static final String STATE_FLASH_INDEX = "flash.toggle.state.flash.index"; - private static final String STATE_SUPPORT_AUTO = "flash.toggle.state.support.auto"; - private static final String STATE_PARENT = "flash.toggle.state.parent"; - - private static final int[] FLASH_AUTO = { R.attr.state_flash_auto }; - private static final int[] FLASH_OFF = { R.attr.state_flash_off }; - private static final int[] FLASH_ON = { R.attr.state_flash_on }; - private static final int[][] FLASH_ENUM = { FLASH_AUTO, FLASH_OFF, FLASH_ON }; - private static final List FLASH_MODES = Arrays.asList(FlashMode.AUTO, FlashMode.OFF, FlashMode.ON); - private static final FlashMode FLASH_FALLBACK = FlashMode.OFF; - - private boolean supportsFlashModeAuto = true; - private int flashIndex; - private OnFlashModeChangedListener flashModeChangedListener; - - public CameraXFlashToggleView(Context context) { - this(context, null); - } - - public CameraXFlashToggleView(Context context, @Nullable AttributeSet attrs) { - this(context, attrs, 0); - } - - public CameraXFlashToggleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - - super.setOnClickListener((v) -> setFlash(FLASH_MODES.get((flashIndex + 1) % FLASH_ENUM.length).getFlashMode())); - } - - @Override - public int[] onCreateDrawableState(int extraSpace) { - final int[] extra = FLASH_ENUM[flashIndex]; - final int[] drawableState = super.onCreateDrawableState(extraSpace + extra.length); - mergeDrawableStates(drawableState, extra); - return drawableState; - } - - @Override - public void setOnClickListener(@Nullable OnClickListener l) { - throw new IllegalStateException("This View does not support custom click listeners."); - } - - public void setAutoFlashEnabled(boolean isAutoEnabled) { - supportsFlashModeAuto = isAutoEnabled; - setFlash(FLASH_MODES.get(flashIndex).getFlashMode()); - } - - public void setFlash(@ImageCapture.FlashMode int mode) { - FlashMode flashMode = FlashMode.fromImageCaptureFlashMode(mode); - - flashIndex = resolveFlashIndex(FLASH_MODES.indexOf(flashMode), supportsFlashModeAuto); - refreshDrawableState(); - notifyListener(); - } - - public void setOnFlashModeChangedListener(@Nullable OnFlashModeChangedListener listener) { - this.flashModeChangedListener = listener; - } - - @Override - protected Parcelable onSaveInstanceState() { - Parcelable parentState = super.onSaveInstanceState(); - Bundle bundle = new Bundle(); - - bundle.putParcelable(STATE_PARENT, parentState); - bundle.putInt(STATE_FLASH_INDEX, flashIndex); - bundle.putBoolean(STATE_SUPPORT_AUTO, supportsFlashModeAuto); - return bundle; - } - - @Override - protected void onRestoreInstanceState(Parcelable state) { - if (state instanceof Bundle) { - Bundle savedState = (Bundle) state; - - supportsFlashModeAuto = savedState.getBoolean(STATE_SUPPORT_AUTO); - setFlash(FLASH_MODES.get( - resolveFlashIndex(savedState.getInt(STATE_FLASH_INDEX), supportsFlashModeAuto)).getFlashMode() - ); - - super.onRestoreInstanceState(savedState.getParcelable(STATE_PARENT)); - } else { - super.onRestoreInstanceState(state); - } - } - - private void notifyListener() { - if (flashModeChangedListener == null) return; - - flashModeChangedListener.flashModeChanged(FLASH_MODES.get(flashIndex).getFlashMode()); - } - - private static int resolveFlashIndex(int desiredFlashIndex, boolean supportsFlashModeAuto) { - if (isIllegalFlashIndex(desiredFlashIndex)) { - throw new IllegalArgumentException("Unsupported index: " + desiredFlashIndex); - } - if (isUnsupportedFlashMode(desiredFlashIndex, supportsFlashModeAuto)) { - return FLASH_MODES.indexOf(FLASH_FALLBACK); - } - return desiredFlashIndex; - } - - private static boolean isIllegalFlashIndex(int desiredFlashIndex) { - return desiredFlashIndex < 0 || desiredFlashIndex > FLASH_ENUM.length; - } - - private static boolean isUnsupportedFlashMode(int desiredFlashIndex, boolean supportsFlashModeAuto) { - return FLASH_MODES.get(desiredFlashIndex) == FlashMode.AUTO && !supportsFlashModeAuto; - } - - public interface OnFlashModeChangedListener { - void flashModeChanged(int flashMode); - } - - private enum FlashMode { - - AUTO(ImageCapture.FLASH_MODE_AUTO), - OFF(ImageCapture.FLASH_MODE_OFF), - ON(ImageCapture.FLASH_MODE_ON); - - private final @ImageCapture.FlashMode int flashMode; - - FlashMode(@ImageCapture.FlashMode int flashMode) { - this.flashMode = flashMode; - } - - @ImageCapture.FlashMode int getFlashMode() { - return flashMode; - } - - private static FlashMode fromImageCaptureFlashMode(@ImageCapture.FlashMode int flashMode) { - for (FlashMode mode : values()) { - if (mode.getFlashMode() == flashMode) { - return mode; - } - } - - throw new AssertionError(); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/FastCameraModels.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/FastCameraModels.java deleted file mode 100644 index bf2f36b61a..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/FastCameraModels.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.thoughtcrime.securesms.mediasend.camerax; - -import androidx.annotation.NonNull; - -import java.util.HashSet; -import java.util.Set; - -/** - * A set of {@link android.os.Build#MODEL} that are known to both benefit from - * {@link androidx.camera.core.ImageCapture.CaptureMode#CAPTURE_MODE_MAXIMIZE_QUALITY} and execute it quickly. - * - */ -public class FastCameraModels { - - private static final Set MODELS = new HashSet() {{ - add("Pixel 2"); - add("Pixel 2 XL"); - add("Pixel 3"); - add("Pixel 3 XL"); - add("Pixel 3a"); - add("Pixel 3a XL"); - add("SM-S911U1"); - }}; - - /** - * @param model Should be a {@link android.os.Build#MODEL}. - */ - public static boolean contains(@NonNull String model) { - return MODELS.contains(model); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/HudCommand.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/HudCommand.kt deleted file mode 100644 index 307aa48570..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/HudCommand.kt +++ /dev/null @@ -1,12 +0,0 @@ -package org.thoughtcrime.securesms.mediasend.v2 - -sealed class HudCommand { - object StartDraw : HudCommand() - object StartCropAndRotate : HudCommand() - object SaveMedia : HudCommand() - - object GoToText : HudCommand() - object GoToReview : HudCommand() - - object ResumeEntryTransition : HudCommand() -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionActivity.kt deleted file mode 100644 index 541a64fede..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionActivity.kt +++ /dev/null @@ -1,524 +0,0 @@ -package org.thoughtcrime.securesms.mediasend.v2 - -import android.content.Context -import android.content.Intent -import android.content.pm.ActivityInfo -import android.os.Bundle -import android.view.KeyEvent -import android.view.WindowManager -import android.widget.Toast -import androidx.activity.OnBackPressedCallback -import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatDelegate -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.ComposeView -import androidx.lifecycle.ViewModelProvider -import androidx.navigation.findNavController -import androidx.navigation.fragment.NavHostFragment -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import org.signal.core.models.media.Media -import org.signal.core.ui.WindowBreakpoint -import org.signal.core.ui.compose.theme.SignalTheme -import org.signal.core.ui.getWindowBreakpoint -import org.signal.core.util.Debouncer -import org.signal.core.util.OVERRIDE_TRANSITION_CLOSE_COMPAT -import org.signal.core.util.concurrent.LifecycleDisposable -import org.signal.core.util.getParcelableArrayListExtraCompat -import org.signal.core.util.getParcelableExtraCompat -import org.signal.core.util.logging.Log -import org.signal.core.util.overrideActivityTransitionCompat -import org.signal.emoji.EmojiEventListener -import org.signal.mediasend.MediaSendRoute -import org.signal.mediasend.MediaValidator -import org.signal.mediasend.screens.capture.MediaCaptureBottomBar -import org.signal.mediasend.screens.capture.MediaCaptureScreenEvents -import org.thoughtcrime.securesms.PassphraseRequiredActivity -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey -import org.thoughtcrime.securesms.conversation.MessageSendType -import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardEvent -import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardEventViewModel -import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageFragment -import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchFragment -import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult -import org.thoughtcrime.securesms.mediasend.v2.review.MediaReviewFragment -import org.thoughtcrime.securesms.mediasend.v2.text.TextStoryPostCreationFragment -import org.thoughtcrime.securesms.recipients.RecipientId -import org.thoughtcrime.securesms.stories.Stories -import org.thoughtcrime.securesms.util.navigation.safeNavigate - -class MediaSelectionActivity : - PassphraseRequiredActivity(), - MediaReviewFragment.Callback, - TextStoryPostCreationFragment.Callback, - EmojiKeyboardPageFragment.Callback, - EmojiEventListener, - EmojiSearchFragment.Callback { - - private var selectedCaptureScreen: MediaSendRoute.Capture by mutableStateOf(MediaSendRoute.Capture.Camera) - - private var isOnCaptureScreen: Boolean by mutableStateOf(false) - - lateinit var viewModel: MediaSelectionViewModel - - private val lifecycleDisposable = LifecycleDisposable() - - private val addMessageCommandViewModel: EmojiKeyboardEventViewModel by viewModels() - - private val destination: MediaSelectionDestination - get() = MediaSelectionDestination.fromBundle(requireNotNull(intent.getBundleExtra(DESTINATION))) - - override val textStoryDestinations: Set - get() = (destination.getRecipientSearchKeyList() + destination.getRecipientSearchKey()).filterNotNull().toSet() - - override val isAddToGroupStoryFlow: Boolean - get() = intent.getBooleanExtra(IS_ADD_TO_GROUP_STORY_FLOW, false) - - override val textStoryDraftText: CharSequence? - get() = if (shareToTextStory) draftText else null - - private val isStory: Boolean - get() = intent.getBooleanExtra(IS_STORY, false) - - private val shareToTextStory: Boolean - get() = intent.getBooleanExtra(AS_TEXT_STORY, false) - - private val draftText: CharSequence? - get() = intent.getCharSequenceExtra(MESSAGE) - - private val debouncer = Debouncer(200) - - override fun attachBaseContext(newBase: Context) { - delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_YES - super.attachBaseContext(newBase) - } - - override fun onPreCreate() { - val sendType: MessageSendType = requireNotNull(intent.getParcelableExtraCompat(MESSAGE_SEND_TYPE, MessageSendType::class.java)) - val initialMedia: List = intent.getParcelableArrayListExtraCompat(MEDIA, Media::class.java) ?: listOf() - val message: CharSequence? = if (shareToTextStory) null else draftText - val isReply: Boolean = intent.getBooleanExtra(IS_REPLY, false) - val isAddToGroupStoryFlow: Boolean = intent.getBooleanExtra(IS_ADD_TO_GROUP_STORY_FLOW, false) - - val factory = MediaSelectionViewModel.Factory(destination, sendType, initialMedia, message, isReply, isStory, isAddToGroupStoryFlow, MediaSelectionRepository(this)) - viewModel = ViewModelProvider(this, factory)[MediaSelectionViewModel::class.java] - } - - override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { - setContentView(R.layout.media_selection_activity) - - if (resources.getWindowBreakpoint() !is WindowBreakpoint.Small) { - requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED - } - - val toggleBar: ComposeView = findViewById(R.id.toggle_bar) - toggleBar.setContent { - val state by viewModel.state.observeAsState() - - val canDisplayStorySwitch = remember(state?.selectedMedia) { - state?.selectedMedia?.let { canDisplayStorySwitch(it) } ?: false - } - - val canDisplayMediaPreview = remember(state?.selectedMedia) { - state?.selectedMedia?.let { canDisplayMediaPreview(it) } ?: false - } - - SignalTheme { - if (isOnCaptureScreen) { - MediaCaptureBottomBar( - canDisplayToggleSwitch = canDisplayStorySwitch, - canDisplayMediaBar = canDisplayMediaPreview, - selectedCaptureScreen = selectedCaptureScreen, - selectedMedia = state?.selectedMedia ?: emptyList(), - onEvent = { event -> - when (event) { - MediaCaptureScreenEvents.ShowCamera -> debouncer.publish { popTextStoryPostCreationFragment() } - MediaCaptureScreenEvents.ShowTextStory -> viewModel.sendCommand(HudCommand.GoToText) - MediaCaptureScreenEvents.NextClicked -> viewModel.sendCommand(HudCommand.GoToReview) - is MediaCaptureScreenEvents.Camera, - is MediaCaptureScreenEvents.ParentStateChanged, - is MediaCaptureScreenEvents.SelectedCaptureScreenChanged -> Unit - } - }, - modifier = Modifier.navigationBarsPadding() - ) - } - } - } - - if (savedInstanceState == null) { - val navHostFragment = NavHostFragment.create(R.navigation.media) - - supportFragmentManager - .beginTransaction() - .replace(R.id.fragment_container, navHostFragment, NAV_HOST_TAG) - .commitNowAllowingStateLoss() - - navigateToStartDestination() - } else { - viewModel.onRestoreState(this, savedInstanceState) - } - - (supportFragmentManager.findFragmentByTag(NAV_HOST_TAG) as NavHostFragment).navController.addOnDestinationChangedListener { _, d, _ -> - when (d.id) { - R.id.mediaCaptureFragment -> { - selectedCaptureScreen = MediaSendRoute.Capture.Camera - isOnCaptureScreen = true - requestedOrientation = if (resources.getWindowBreakpoint() is WindowBreakpoint.Small) { - ActivityInfo.SCREEN_ORIENTATION_PORTRAIT - } else { - ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED - } - } - - R.id.textStoryPostCreationFragment -> { - selectedCaptureScreen = MediaSendRoute.Capture.TextStory - isOnCaptureScreen = true - requestedOrientation = if (resources.getWindowBreakpoint() is WindowBreakpoint.Small) { - ActivityInfo.SCREEN_ORIENTATION_PORTRAIT - } else { - ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED - } - } - - else -> { - isOnCaptureScreen = false - requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED - } - } - - // Hard-cut rotation while capturing (like Pixel Camera) instead of the system's smooth rotate. - window.attributes = window.attributes.apply { - rotationAnimation = if (isOnCaptureScreen) { - WindowManager.LayoutParams.ROTATION_ANIMATION_JUMPCUT - } else { - WindowManager.LayoutParams.ROTATION_ANIMATION_ROTATE - } - } - } - - lifecycleDisposable.bindTo(this) - lifecycleDisposable += viewModel.mediaErrors - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::handleError) - - lifecycleDisposable += viewModel.videoTrimmedEvents - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { Toast.makeText(this, R.string.MediaReviewFragment__video_trimmed_to_fit, Toast.LENGTH_SHORT).show() } - - onBackPressedDispatcher.addCallback(OnBackPressed()) - - if (savedInstanceState == null && intent.getBooleanExtra(IS_FOR_QUICK_RESTORE, false)) { - QuickRestoreInfoDialog.show(supportFragmentManager) - } - } - - private fun handleError(error: MediaValidator.FilterError) { - when (error) { - MediaValidator.FilterError.None -> return - is MediaValidator.FilterError.ItemTooLarge -> Toast.makeText(this, R.string.MediaReviewFragment__one_or_more_items_were_too_large, Toast.LENGTH_SHORT).show() - is MediaValidator.FilterError.ItemInvalidType -> Toast.makeText(this, R.string.MediaReviewFragment__one_or_more_items_were_invalid, Toast.LENGTH_SHORT).show() - MediaValidator.FilterError.TooManyItems -> Toast.makeText(this, R.string.MediaReviewFragment__too_many_items_selected, Toast.LENGTH_SHORT).show() - is MediaValidator.FilterError.NoItems -> { - error.cause?.let { handleError(it) } - onNoMediaSelected() - } - } - - viewModel.clearMediaErrors() - } - - private fun popTextStoryPostCreationFragment() { - val navController = findNavController(R.id.fragment_container) - if (navController.currentDestination?.id == R.id.textStoryPostCreationFragment) { - navController.popBackStack() - } - } - - private fun canDisplayMediaPreview(selectedMedia: List): Boolean { - return Stories.isFeatureEnabled() && - isCameraFirst() && - selectedMedia.isNotEmpty() && - (destination == MediaSelectionDestination.ChooseAfterMediaSelection || destination is MediaSelectionDestination.SingleStory) - } - - private fun canDisplayStorySwitch(selectedMedia: List): Boolean { - return Stories.isFeatureEnabled() && - isCameraFirst() && - selectedMedia.isEmpty() && - (destination == MediaSelectionDestination.ChooseAfterMediaSelection || destination is MediaSelectionDestination.SingleStory) - } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - viewModel.onSaveState(outState) - } - - override fun onSentWithResult(mediaSendActivityResult: MediaSendActivityResult) { - setResult( - RESULT_OK, - Intent().apply { - putExtra(MediaSendActivityResult.EXTRA_RESULT, mediaSendActivityResult) - } - ) - - finish() - overrideActivityTransitionCompat(OVERRIDE_TRANSITION_CLOSE_COMPAT, R.anim.stationary, R.anim.camera_slide_to_bottom) - } - - override fun onSentWithoutResult() { - val intent = Intent() - setResult(RESULT_OK, intent) - - finish() - overrideActivityTransitionCompat(OVERRIDE_TRANSITION_CLOSE_COMPAT, R.anim.stationary, R.anim.camera_slide_to_bottom) - } - - override fun onSendError(error: Throwable) { - setResult(RESULT_CANCELED) - - // TODO [alex] - Toast - Log.w(TAG, "Failed to send message.", error) - - finish() - overrideActivityTransitionCompat(OVERRIDE_TRANSITION_CLOSE_COMPAT, R.anim.stationary, R.anim.camera_slide_to_bottom) - } - - override fun onNoMediaSelected() { - Log.w(TAG, "No media selected. Exiting.") - - setResult(RESULT_CANCELED) - finish() - overrideActivityTransitionCompat(OVERRIDE_TRANSITION_CLOSE_COMPAT, R.anim.stationary, R.anim.camera_slide_to_bottom) - } - - override fun onPopFromReview() { - if (isCameraFirst()) { - viewModel.removeCameraFirstCapture() - } - - if (!navigateToStartDestination()) { - finish() - } - } - - private fun navigateToStartDestination(navHostFragment: NavHostFragment? = null): Boolean { - val hostFragment: NavHostFragment = navHostFragment ?: supportFragmentManager.findFragmentByTag(NAV_HOST_TAG) as NavHostFragment - - val startDestination: Int = intent.getIntExtra(START_ACTION, -1) - return if (startDestination > 0) { - hostFragment.navController.safeNavigate( - startDestination, - Bundle().apply { - putBoolean("first", true) - } - ) - - true - } else { - false - } - } - - private fun isCameraFirst(): Boolean = intent.getIntExtra(START_ACTION, -1) == R.id.action_directly_to_mediaCaptureFragment - - override fun openEmojiSearch() { - addMessageCommandViewModel.onEvent(EmojiKeyboardEvent.OpenEmojiSearch) - } - - override fun onEmojiSelected(emoji: String?) { - addMessageCommandViewModel.onEvent(EmojiKeyboardEvent.EmojiInsert(emoji)) - } - - override fun onKeyEvent(keyEvent: KeyEvent?) { - addMessageCommandViewModel.onEvent(EmojiKeyboardEvent.EmojiKeyEvent(keyEvent)) - } - - override fun closeEmojiSearch() { - addMessageCommandViewModel.onEvent(EmojiKeyboardEvent.CloseEmojiSearch) - } - - private inner class OnBackPressed : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - val navController = this@MediaSelectionActivity.findNavController(R.id.fragment_container) - - if (shareToTextStory && navController.currentDestination?.id == R.id.textStoryPostCreationFragment) { - finish() - } - - if (!navController.popBackStack()) { - finish() - } - } - } - - companion object { - private val TAG = Log.tag(MediaSelectionActivity::class.java) - - private const val NAV_HOST_TAG = "NAV_HOST" - - private const val START_ACTION = "start.action" - private const val MESSAGE_SEND_TYPE = "message.send.type" - private const val MEDIA = "media" - private const val MESSAGE = "message" - private const val DESTINATION = "destination" - private const val IS_REPLY = "is_reply" - private const val IS_STORY = "is_story" - private const val AS_TEXT_STORY = "as_text_story" - private const val IS_ADD_TO_GROUP_STORY_FLOW = "is_add_to_group_story_flow" - private const val IS_FOR_QUICK_RESTORE = "is_for_quick_restore" - - @JvmStatic - fun camera(context: Context): Intent { - return camera(context, false) - } - - @JvmStatic - fun camera(context: Context, isStory: Boolean): Intent { - return buildIntent( - context = context, - startAction = R.id.action_directly_to_mediaCaptureFragment, - isStory = isStory - ) - } - - fun cameraForQuickRestore(context: Context): Intent { - return buildIntent( - context = context, - startAction = R.id.action_directly_to_mediaCaptureFragment, - isForQuickRestore = true - ) - } - - fun addToGroupStory( - context: Context, - recipientId: RecipientId - ): Intent { - return buildIntent( - context = context, - startAction = R.id.action_directly_to_mediaCaptureFragment, - isStory = true, - isAddToGroupStoryFlow = true, - destination = MediaSelectionDestination.SingleStory(recipientId) - ) - } - - @JvmStatic - fun camera( - context: Context, - messageSendType: MessageSendType, - recipientId: RecipientId, - isReply: Boolean - ): Intent { - return buildIntent( - context = context, - startAction = R.id.action_directly_to_mediaCaptureFragment, - messageSendType = messageSendType, - destination = MediaSelectionDestination.SingleRecipient(recipientId), - isReply = isReply - ) - } - - @JvmStatic - fun gallery( - context: Context, - messageSendType: MessageSendType, - media: List, - recipientId: RecipientId, - message: CharSequence?, - isReply: Boolean - ): Intent { - return buildIntent( - context = context, - startAction = R.id.action_directly_to_mediaGalleryFragment, - messageSendType = messageSendType, - media = media, - destination = MediaSelectionDestination.SingleRecipient(recipientId), - message = message, - isReply = isReply - ) - } - - @JvmStatic - fun editor( - context: Context, - messageSendType: MessageSendType, - media: List, - recipientId: RecipientId, - message: CharSequence? - ): Intent { - return buildIntent( - context = context, - messageSendType = messageSendType, - media = media, - destination = MediaSelectionDestination.SingleRecipient(recipientId), - message = message - ) - } - - @JvmStatic - fun editor( - context: Context, - media: List - ): Intent { - return buildIntent( - context = context, - media = media - ) - } - - @JvmStatic - fun share( - context: Context, - messageSendType: MessageSendType, - media: List, - recipientSearchKeys: List, - message: CharSequence?, - asTextStory: Boolean - ): Intent { - return buildIntent( - context = context, - messageSendType = messageSendType, - media = media, - destination = MediaSelectionDestination.MultipleRecipients(recipientSearchKeys), - message = message, - asTextStory = asTextStory, - startAction = if (asTextStory) R.id.action_directly_to_textPostCreationFragment else -1, - isStory = recipientSearchKeys.any { it.isStory } - ) - } - - private fun buildIntent( - context: Context, - startAction: Int = -1, - messageSendType: MessageSendType = MessageSendType.SignalMessageSendType, - media: List = listOf(), - destination: MediaSelectionDestination = MediaSelectionDestination.ChooseAfterMediaSelection, - message: CharSequence? = null, - isReply: Boolean = false, - isStory: Boolean = false, - asTextStory: Boolean = false, - isAddToGroupStoryFlow: Boolean = false, - isForQuickRestore: Boolean = false - ): Intent { - return Intent(context, MediaSelectionActivity::class.java).apply { - putExtra(START_ACTION, startAction) - putExtra(MESSAGE_SEND_TYPE, messageSendType) - putParcelableArrayListExtra(MEDIA, ArrayList(media)) - putExtra(MESSAGE, message) - putExtra(DESTINATION, destination.toBundle()) - putExtra(IS_REPLY, isReply) - putExtra(IS_STORY, isStory) - putExtra(AS_TEXT_STORY, asTextStory) - putExtra(IS_ADD_TO_GROUP_STORY_FLOW, isAddToGroupStoryFlow) - putExtra(IS_FOR_QUICK_RESTORE, isForQuickRestore) - } - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionDestination.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionDestination.kt deleted file mode 100644 index 362cc2a547..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionDestination.kt +++ /dev/null @@ -1,101 +0,0 @@ -package org.thoughtcrime.securesms.mediasend.v2 - -import android.os.Bundle -import org.signal.core.util.getParcelableArrayListCompat -import org.signal.core.util.getParcelableCompat -import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey -import org.thoughtcrime.securesms.recipients.RecipientId - -sealed class MediaSelectionDestination { - - object Wallpaper : MediaSelectionDestination() { - override fun toBundle(): Bundle { - return Bundle().apply { - putBoolean(WALLPAPER, true) - } - } - } - - object Avatar : MediaSelectionDestination() { - override fun toBundle(): Bundle { - return Bundle().apply { - putBoolean(AVATAR, true) - } - } - } - - object ChooseAfterMediaSelection : MediaSelectionDestination() { - override fun toBundle(): Bundle { - return Bundle.EMPTY - } - } - - class SingleRecipient(private val id: RecipientId) : MediaSelectionDestination() { - override fun getRecipientSearchKey(): ContactSearchKey.RecipientSearchKey = ContactSearchKey.RecipientSearchKey(id, false) - - override fun toBundle(): Bundle { - return Bundle().apply { - putParcelable(RECIPIENT, id) - } - } - } - - class SingleStory(private val id: RecipientId) : MediaSelectionDestination() { - override fun getRecipientSearchKey(): ContactSearchKey.RecipientSearchKey = ContactSearchKey.RecipientSearchKey(id, true) - - override fun toBundle(): Bundle { - return Bundle().apply { - putParcelable(STORY, id) - } - } - } - - class MultipleRecipients(val recipientSearchKeys: List) : MediaSelectionDestination() { - - companion object { - fun fromParcel(parcelables: List): MultipleRecipients { - return MultipleRecipients(parcelables) - } - } - - override fun getRecipientSearchKey(): ContactSearchKey.RecipientSearchKey? { - return if (recipientSearchKeys.size == 1) { - recipientSearchKeys[0] - } else { - super.getRecipientSearchKey() - } - } - - override fun getRecipientSearchKeyList(): List = recipientSearchKeys - - override fun toBundle(): Bundle { - return Bundle().apply { - putParcelableArrayList(RECIPIENT_LIST, ArrayList(recipientSearchKeys.map { it.requireRecipientSearchKey() })) - } - } - } - - open fun getRecipientSearchKey(): ContactSearchKey.RecipientSearchKey? = null - open fun getRecipientSearchKeyList(): List = emptyList() - - abstract fun toBundle(): Bundle - - companion object { - private const val WALLPAPER = "wallpaper" - private const val AVATAR = "avatar" - private const val RECIPIENT = "recipient" - private const val STORY = "story" - private const val RECIPIENT_LIST = "recipient_list" - - fun fromBundle(bundle: Bundle): MediaSelectionDestination { - return when { - bundle.containsKey(WALLPAPER) -> Wallpaper - bundle.containsKey(AVATAR) -> Avatar - bundle.containsKey(RECIPIENT) -> SingleRecipient(requireNotNull(bundle.getParcelableCompat(RECIPIENT, RecipientId::class.java))) - bundle.containsKey(STORY) -> SingleStory(requireNotNull(bundle.getParcelableCompat(STORY, RecipientId::class.java))) - bundle.containsKey(RECIPIENT_LIST) -> MultipleRecipients.fromParcel(requireNotNull(bundle.getParcelableArrayListCompat(RECIPIENT_LIST, ContactSearchKey.RecipientSearchKey::class.java))) - else -> ChooseAfterMediaSelection - } - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionNavigator.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionNavigator.kt deleted file mode 100644 index 2dd40f00be..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionNavigator.kt +++ /dev/null @@ -1,30 +0,0 @@ -package org.thoughtcrime.securesms.mediasend.v2 - -import androidx.navigation.NavController -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.util.navigation.safeNavigate - -class MediaSelectionNavigator( - private val toCamera: Int = -1, - private val toGallery: Int = -1 -) { - fun goToReview(navController: NavController) { - navController.popBackStack(R.id.mediaReviewFragment, false) - } - - fun goToCamera(navController: NavController) { - if (toCamera == -1) return - - navController.safeNavigate(toCamera) - } - - fun goToGallery(navController: NavController) { - if (toGallery == -1) return - - navController.safeNavigate(toGallery) - } - - fun isPreviousScreenMediaReview(navController: NavController): Boolean { - return navController.previousBackStackEntry?.destination?.id == R.id.mediaReviewFragment - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionState.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionState.kt deleted file mode 100644 index 0b2bd413ac..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionState.kt +++ /dev/null @@ -1,79 +0,0 @@ -package org.thoughtcrime.securesms.mediasend.v2 - -import android.net.Uri -import org.signal.core.models.media.Media -import org.signal.mediasend.MediaConstraints -import org.signal.mediasend.SentMediaQuality -import org.signal.mediasend.screens.edit.video.VideoTrimData -import org.thoughtcrime.securesms.conversation.MessageSendType -import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.mms.TranscodingConfigProvider -import org.thoughtcrime.securesms.recipients.Recipient -import org.thoughtcrime.securesms.stories.Stories -import org.thoughtcrime.securesms.util.MediaUtil -import org.thoughtcrime.securesms.util.RemoteConfig -import org.thoughtcrime.securesms.video.TranscodingConfig -import kotlin.time.Duration -import kotlin.time.Duration.Companion.seconds - -data class MediaSelectionState( - val sendType: MessageSendType, - val selectedMedia: List = listOf(), - val focusedMedia: Media? = null, - val recipient: Recipient? = null, - val quality: SentMediaQuality = SignalStore.settings.sentMediaQuality, - val message: CharSequence? = null, - val viewOnceToggleState: ViewOnceToggleState = ViewOnceToggleState.default, - val isTouchEnabled: Boolean = true, - val isSent: Boolean = false, - val isPreUploadEnabled: Boolean = false, - val isMeteredConnection: Boolean = false, - val editorStateMap: Map = mapOf(), - val cameraFirstCapture: Media? = null, - val isStory: Boolean, - val storySendRequirements: Stories.MediaTransform.SendRequirements = Stories.MediaTransform.SendRequirements.CAN_NOT_SEND, - val suppressEmptyError: Boolean = true, - val transcodingConfigs: List = TranscodingConfigProvider.getConfigsForMediaQuality(SentMediaQuality.fromCode(quality.code)) -) { - - val isVideoTrimmingVisible: Boolean = focusedMedia != null && MediaUtil.isVideoType(focusedMedia.contentType) && MediaConstraints.isVideoTranscodeAvailable() && !focusedMedia.isVideoGif - - val maxSelection = RemoteConfig.maxAttachmentCount - - val canSend = !isSent && selectedMedia.isNotEmpty() - - fun getOrCreateVideoTrimData(uri: Uri): VideoTrimData { - return editorStateMap[uri] as? VideoTrimData ?: VideoTrimData() - } - - fun calculateMaxVideoDurationUs(videoDuration: Duration): Long { - return if (isStory && !MediaConstraints.isVideoTranscodeAvailable()) { - Stories.MAX_VIDEO_DURATION_MILLIS - } else { - TranscodingConfig.calculateMaxVideoUploadDurationInSeconds(transcodingConfigs, videoDuration).seconds.inWholeMicroseconds - } - } - - enum class ViewOnceToggleState(val code: Int) { - INFINITE(0), - ONCE(1); - - fun next(): ViewOnceToggleState { - return when (this) { - INFINITE -> ONCE - ONCE -> INFINITE - } - } - - companion object { - val default = INFINITE - - fun fromCode(code: Int): ViewOnceToggleState { - return when (code) { - 1 -> ONCE - else -> INFINITE - } - } - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionViewModel.kt deleted file mode 100644 index 73b90f30b6..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionViewModel.kt +++ /dev/null @@ -1,673 +0,0 @@ -package org.thoughtcrime.securesms.mediasend.v2 - -import android.content.Context -import android.net.Uri -import android.os.Bundle -import android.os.Parcel -import androidx.lifecycle.LiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import com.google.common.io.ByteStreams -import io.reactivex.rxjava3.core.Maybe -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.disposables.Disposable -import io.reactivex.rxjava3.kotlin.plusAssign -import io.reactivex.rxjava3.kotlin.subscribeBy -import io.reactivex.rxjava3.schedulers.Schedulers -import io.reactivex.rxjava3.subjects.BehaviorSubject -import io.reactivex.rxjava3.subjects.PublishSubject -import io.reactivex.rxjava3.subjects.Subject -import org.signal.core.models.media.Media -import org.signal.core.util.Util -import org.signal.core.util.contentproviders.BlobProvider -import org.signal.core.util.getParcelableArrayListCompat -import org.signal.core.util.getParcelableCompat -import org.signal.core.util.logging.Log -import org.signal.mediasend.MediaConstraints -import org.signal.mediasend.MediaValidator -import org.signal.mediasend.SentMediaQuality -import org.signal.mediasend.screens.edit.video.VideoTrimData -import org.thoughtcrime.securesms.components.mention.MentionAnnotation -import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey -import org.thoughtcrime.securesms.conversation.MessageSendType -import org.thoughtcrime.securesms.conversation.MessageStyler -import org.thoughtcrime.securesms.dependencies.AppDependencies -import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult -import org.thoughtcrime.securesms.mms.PushMediaConstraints -import org.thoughtcrime.securesms.mms.TranscodingConfigProvider -import org.thoughtcrime.securesms.recipients.Recipient -import org.thoughtcrime.securesms.scribbles.ImageEditorFragment -import org.thoughtcrime.securesms.stories.Stories -import org.thoughtcrime.securesms.util.MediaUtil -import org.thoughtcrime.securesms.util.livedata.Store -import java.util.Collections -import kotlin.math.max -import kotlin.time.Duration -import kotlin.time.Duration.Companion.microseconds -import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.Duration.Companion.seconds - -/** - * ViewModel which maintains the list of selected media and other shared values. - */ -class MediaSelectionViewModel( - val destination: MediaSelectionDestination, - sendType: MessageSendType, - initialMedia: List, - initialMessage: CharSequence?, - val isReply: Boolean, - isStory: Boolean, - val isAddToGroupStoryFlow: Boolean, - private val repository: MediaSelectionRepository, - private val identityChangesSince: Long = System.currentTimeMillis() -) : ViewModel() { - - private val TAG = Log.tag(MediaSelectionViewModel::class.java) - - private val selectedMediaSubject: Subject> = BehaviorSubject.create() - - private val store: Store = Store( - MediaSelectionState( - sendType = sendType, - message = initialMessage, - isStory = isStory - ) - ) - - val isContactSelectionRequired = destination == MediaSelectionDestination.ChooseAfterMediaSelection - - val state: LiveData = store.stateLiveData - - private val internalHudCommands = PublishSubject.create() - - val mediaErrors: BehaviorSubject = BehaviorSubject.createDefault(MediaValidator.FilterError.None) - val hudCommands: Observable = internalHudCommands - - private val _videoTrimmedEvents = PublishSubject.create() - val videoTrimmedEvents: Observable = _videoTrimmedEvents - - private val disposables = CompositeDisposable() - - private val isMeteredDisposable: Disposable = repository.isMetered.subscribe { metered -> - store.update { - it.copy( - isMeteredConnection = metered, - isPreUploadEnabled = shouldPreUpload(metered) - ) - } - } - - private var lastMediaDrag: Pair = Pair(0, 0) - - init { - val recipientSearchKey = destination.getRecipientSearchKey() - if (recipientSearchKey != null) { - store.update(Recipient.live(recipientSearchKey.recipientId).liveData) { r, s -> - s.copy( - recipient = r, - isPreUploadEnabled = shouldPreUpload(s.isMeteredConnection) - ) - } - } - - if (initialMedia.isNotEmpty()) { - addMedia(initialMedia.toSet()) - } - - disposables += selectedMediaSubject - .flatMapSingle { media -> - Single.fromCallable { - Stories.MediaTransform.getSendRequirements(media) - }.subscribeOn(Schedulers.io()) - } - .subscribeBy { requirements -> - store.update { - it.copy(storySendRequirements = requirements) - } - } - } - - override fun onCleared() { - isMeteredDisposable.dispose() - disposables.clear() - } - - fun kick() { - store.update { it } - } - - fun sendCommand(hudCommand: HudCommand) { - internalHudCommands.onNext(hudCommand) - } - - fun setTouchEnabled(isEnabled: Boolean) { - store.update { it.copy(isTouchEnabled = isEnabled) } - } - - fun setSuppressEmptyError(isSuppressed: Boolean) { - store.update { it.copy(suppressEmptyError = isSuppressed) } - } - - fun addMedia(media: Media) { - addMedia(setOf(media)) - } - - fun isStory(): Boolean { - return store.state.isStory - } - - fun getStorySendRequirements(): Stories.MediaTransform.SendRequirements { - return store.state.storySendRequirements - } - - fun addMedia(media: Set) { - val newSelectionList: List = linkedSetOf().apply { - addAll(store.state.selectedMedia) - addAll(media) - }.toList() - - disposables.add( - repository - .populateAndFilterMedia(newSelectionList, getMediaConstraints(), store.state.maxSelection, store.state.isStory) - .subscribe { filterResult -> - if (filterResult.filteredMedia.isNotEmpty()) { - val existingState = store.state - val initializedVideoEditorStates = filterResult.filteredMedia.filterNot { media -> existingState.editorStateMap.containsKey(media.uri) } - .filter { media -> MediaUtil.isNonGifVideo(media) } - .associate { video: Media -> - val duration = video.duration.milliseconds.inWholeMicroseconds - val maxDuration = existingState.calculateMaxVideoDurationUs(video.duration.milliseconds) - if (MediaConstraints.isVideoTranscodeAvailable() && duration >= maxDuration) { - video.uri to VideoTrimData(true, duration, 0, maxDuration) - } else { - video.uri to VideoTrimData(false, duration, 0, duration) - } - } - - store.update { - val updatedCameraFirstCapture = if (it.cameraFirstCapture != null) { - filterResult.filteredMedia.find { filtered -> filtered.uri == it.cameraFirstCapture.uri } - } else { - null - } - - it.copy( - selectedMedia = filterResult.filteredMedia, - focusedMedia = it.focusedMedia ?: filterResult.filteredMedia.first(), - editorStateMap = it.editorStateMap + initializedVideoEditorStates, - cameraFirstCapture = if (filterResult.filteredMedia.size > 1) null else updatedCameraFirstCapture ?: it.cameraFirstCapture - ) - } - - if (initializedVideoEditorStates.any { (_, data) -> data.isDurationEdited }) { - _videoTrimmedEvents.onNext(Unit) - } - - selectedMediaSubject.onNext(filterResult.filteredMedia) - - val newMedia = filterResult.filteredMedia.toSet().intersect(media).toList() - startUpload(newMedia) - } - - if (filterResult.filterError != null) { - mediaErrors.onNext(filterResult.filterError) - } - } - ) - } - - fun swapMedia(originalStart: Int, end: Int): Boolean { - var start: Int = originalStart - - if (lastMediaDrag.first == start && lastMediaDrag.second == end) { - return true - } else if (lastMediaDrag.first == start) { - start = lastMediaDrag.second - } - - val snapshot = store.state - - if (end >= snapshot.selectedMedia.size || end < 0 || start >= snapshot.selectedMedia.size || start < 0) { - return false - } - - lastMediaDrag = Pair(originalStart, end) - - val newMediaList = snapshot.selectedMedia.toMutableList() - - if (start < end) { - for (i in start until end) { - Collections.swap(newMediaList, i, i + 1) - } - } else { - for (i in start downTo end + 1) { - Collections.swap(newMediaList, i, i - 1) - } - } - - store.update { - it.copy( - selectedMedia = newMediaList - ) - } - - return true - } - - fun isValidMediaDragPosition(position: Int): Boolean { - return position >= 0 && position < store.state.selectedMedia.size - } - - fun onMediaDragFinished() { - lastMediaDrag = Pair(0, 0) - } - - fun isSelectedMediaEmpty(): Boolean { - return store.state.selectedMedia.isEmpty() - } - - fun removeMedia(media: Media, suppressEmptyError: Boolean = store.state.suppressEmptyError) { - removeMedia(setOf(media), suppressEmptyError) - } - - fun removeMedia(media: Set, suppressEmptyError: Boolean = store.state.suppressEmptyError) { - val snapshot = store.state - val newMediaList = snapshot.selectedMedia - media - val newFocus = when { - newMediaList.isEmpty() -> null - snapshot.focusedMedia in media -> { - val oldFocusIndex = snapshot.selectedMedia.indexOf(snapshot.focusedMedia) - newMediaList[Util.clamp(oldFocusIndex, 0, newMediaList.size - 1)] - } - else -> snapshot.focusedMedia - } - - store.update { - it.copy( - selectedMedia = newMediaList, - focusedMedia = newFocus, - editorStateMap = it.editorStateMap - media.map { it.uri }, - cameraFirstCapture = if (it.cameraFirstCapture in media) null else it.cameraFirstCapture - ) - } - - if (newMediaList.isEmpty() && !store.state.suppressEmptyError) { - mediaErrors.onNext(MediaValidator.FilterError.NoItems()) - } - - selectedMediaSubject.onNext(newMediaList) - repository.deleteBlobs(media.toList()) - - Log.d(TAG, "User removed ${media.forEach { it.uri }} from message.") - cancelUpload(media) - } - - fun addCameraFirstCapture(media: Media) { - store.update { state -> - state.copy(cameraFirstCapture = media) - } - addMedia(media) - } - - fun removeCameraFirstCapture() { - val cameraFirstCapture: Media? = store.state.cameraFirstCapture - if (cameraFirstCapture != null) { - setSuppressEmptyError(true) - removeMedia(cameraFirstCapture, suppressEmptyError = true) - } - } - - fun onPageChanged(media: Media) { - store.update { it.copy(focusedMedia = media) } - } - - fun onPageChanged(position: Int) { - store.update { - if (position >= it.selectedMedia.size) { - it.copy(focusedMedia = null) - } else { - val focusedMedia: Media = it.selectedMedia[position] - it.copy(focusedMedia = focusedMedia) - } - } - } - - fun getMediaConstraints(): MediaConstraints { - return PushMediaConstraints(null) - } - - /** - * A recording is assumed to be wanted in its entirety, so if it is longer than high quality allows we fall back to - * standard quality rather than have the editor truncate it to fit. - */ - fun onVideoRecorded(duration: Duration) { - if (store.state.quality != SentMediaQuality.HIGH) { - return - } - - val maxDuration = TranscodingConfigProvider.getMaxVideoDurationSeconds(SentMediaQuality.HIGH, duration).seconds - if (duration > maxDuration) { - Log.i(TAG, "Recording of $duration exceeds the $maxDuration allowed at high quality. Falling back to standard quality.") - setSentMediaQuality(SentMediaQuality.STANDARD) - } - } - - fun setSentMediaQuality(sentMediaQuality: SentMediaQuality) { - if (sentMediaQuality == store.state.quality) { - return - } - - store.update { it.copy(quality = sentMediaQuality, isPreUploadEnabled = false, transcodingConfigs = TranscodingConfigProvider.getConfigsForMediaQuality(sentMediaQuality)) } - repository.uploadRepository.cancelAllUploads() - - var videoTrimmed = false - store.state.selectedMedia.forEach { mediaItem -> - if (MediaUtil.isVideoType(mediaItem.contentType) && MediaConstraints.isVideoTranscodeAvailable()) { - val uri = mediaItem.uri - val before = store.state.getOrCreateVideoTrimData(uri) - onEditVideoDuration(totalDurationUs = before.totalInputDurationUs, startTimeUs = before.startTimeUs, endTimeUs = before.endTimeUs, touchEnabled = true, uri = uri) - val after = store.state.getOrCreateVideoTrimData(uri) - if (after.getDuration() < before.getDuration()) { - videoTrimmed = true - } - } - } - - if (videoTrimmed) { - _videoTrimmedEvents.onNext(Unit) - } - } - - fun setMessage(text: CharSequence?) { - store.update { it.copy(message = text) } - } - - fun incrementViewOnceState() { - store.update { it.copy(viewOnceToggleState = it.viewOnceToggleState.next()) } - } - - fun onEditVideoDuration(totalDurationUs: Long, startTimeUs: Long, endTimeUs: Long, touchEnabled: Boolean, uri: Uri? = store.state.focusedMedia?.uri) { - if (uri == null) return - store.update { - val data = it.getOrCreateVideoTrimData(uri) - val clampedStartTime = max(startTimeUs, 0) - - val unedited = !data.isDurationEdited - val durationEdited = clampedStartTime > 0 || endTimeUs < totalDurationUs - val isEntireDuration = startTimeUs == 0L && endTimeUs == totalDurationUs - val endMoved = !isEntireDuration && data.endTimeUs != endTimeUs - val maxVideoDurationUs: Long = it.calculateMaxVideoDurationUs((endTimeUs - clampedStartTime).microseconds) - val preserveStartTime = unedited || !endMoved - val videoTrimData = data.copy(isDurationEdited = durationEdited, totalInputDurationUs = totalDurationUs, startTimeUs = clampedStartTime, endTimeUs = endTimeUs) - val updatedData = clampToMaxClipDuration(videoTrimData, maxVideoDurationUs, preserveStartTime) - - if (updatedData != videoTrimData) { - Log.d(TAG, "Video attachment trim clamped from ${videoTrimData.startTimeUs}, ${videoTrimData.endTimeUs} to ${updatedData.startTimeUs}, ${updatedData.endTimeUs}") - } - - if (unedited && durationEdited) { - Log.d(TAG, "Canceling attachment upload because the duration has been edited for the first time..") - cancelUpload(MediaBuilder.buildMedia(uri)) - } - - if (updatedData != data) { - Log.d(TAG, "Updating video attachment trim data for $uri") - it.copy( - isTouchEnabled = touchEnabled, - editorStateMap = it.editorStateMap + (uri to updatedData) - ) - } else { - Log.d(TAG, "Preserving video attachment trim data for $uri") - it.copy(isTouchEnabled = touchEnabled) - } - } - } - - /** - * Toggles whether the focused video's audio track is stripped when it is sent. - */ - fun toggleVideoMuted() { - val uri = store.state.focusedMedia?.uri ?: return - val data = store.state.getOrCreateVideoTrimData(uri) - val updatedData = data.copy(isMuted = !data.isMuted) - - store.update { - it.copy(editorStateMap = it.editorStateMap + (uri to updatedData)) - } - - Log.d(TAG, "Canceling attachment upload because the audio was muted/unmuted.") - cancelUpload(MediaBuilder.buildMedia(uri)) - } - - fun getEditorState(uri: Uri): Any? { - return store.state.editorStateMap[uri] - } - - fun setEditorState(uri: Uri, state: Any) { - store.update { - it.copy(editorStateMap = it.editorStateMap + (uri to state)) - } - } - - fun send( - selectedContacts: List = emptyList(), - scheduledDate: Long? = null - ): Maybe = send(selectedContacts, scheduledDate ?: -1) - - fun send( - selectedContacts: List = emptyList(), - scheduledDate: Long - ): Maybe { - return UntrustedRecords.checkForBadIdentityRecords(selectedContacts.toSet(), identityChangesSince).andThen( - repository.send( - selectedMedia = store.state.selectedMedia, - stateMap = store.state.editorStateMap, - quality = store.state.quality, - message = store.state.message, - isViewOnce = isViewOnceEnabled(), - singleContact = destination.getRecipientSearchKey(), - contacts = selectedContacts.ifEmpty { destination.getRecipientSearchKeyList() }, - mentions = MentionAnnotation.getMentionsFromAnnotations(store.state.message), - bodyRanges = MessageStyler.getStyling(store.state.message), - sendType = store.state.sendType, - scheduledTime = scheduledDate - ) - ) - } - - private fun isViewOnceEnabled(): Boolean { - return store.state.selectedMedia.size == 1 && - store.state.viewOnceToggleState == MediaSelectionState.ViewOnceToggleState.ONCE - } - - private fun startUpload(media: List) { - if (!store.state.isPreUploadEnabled) { - return - } - - val filteredPreUploadMedia = if (destination is MediaSelectionDestination.SingleRecipient || !Stories.isFeatureEnabled()) { - media.filter { !MediaUtil.isDocumentType(it.contentType) } - } else { - media.filter { Stories.MediaTransform.canPreUploadMedia(it) } - } - - repository.uploadRepository.startUpload(filteredPreUploadMedia, store.state.recipient) - } - - private fun cancelUpload(media: Media) { - cancelUpload(setOf(media)) - } - - private fun cancelUpload(media: Set) { - repository.uploadRepository.cancelUpload(media) - } - - private fun shouldPreUpload(metered: Boolean): Boolean { - return !metered && !isContactSelectionRequired - } - - fun onSaveState(outState: Bundle) { - val snapshot = store.state - - outState.putParcelableArrayList(STATE_SELECTION, ArrayList(snapshot.selectedMedia)) - outState.putParcelable(STATE_FOCUSED, snapshot.focusedMedia) - outState.putInt(STATE_QUALITY, snapshot.quality.code) - outState.putCharSequence(STATE_MESSAGE, snapshot.message) - outState.putInt(STATE_VIEW_ONCE, snapshot.viewOnceToggleState.code) - outState.putBoolean(STATE_TOUCH_ENABLED, snapshot.isTouchEnabled) - outState.putBoolean(STATE_SENT, snapshot.isSent) - outState.putParcelable(STATE_CAMERA_FIRST_CAPTURE, snapshot.cameraFirstCapture) - - val editorStates: List = store.state.editorStateMap.entries.map { it.toBundleStateEntry() } - outState.putInt(STATE_EDITOR_COUNT, editorStates.size) - if (editorStates.isNotEmpty()) { - val parcel = Parcel.obtain() - editorStates.forEach { it.writeToParcel(parcel, 0) } - val serializedEditorState: ByteArray = parcel.marshall() - parcel.recycle() - val blobUri = AppDependencies.blobs.forData(serializedEditorState).createForSingleUseInMemory() - outState.putParcelable(STATE_EDITORS, blobUri) - } - } - - fun hasSelectedMedia(): Boolean { - return store.state.selectedMedia.isNotEmpty() - } - - fun clearMediaErrors() { - mediaErrors.onNext(MediaValidator.FilterError.None) - } - - fun onRestoreState(context: Context, savedInstanceState: Bundle) { - val selection: List = savedInstanceState.getParcelableArrayListCompat(STATE_SELECTION, Media::class.java) ?: emptyList() - val focused: Media? = savedInstanceState.getParcelableCompat(STATE_FOCUSED, Media::class.java) - val quality: SentMediaQuality = SentMediaQuality.fromCode(savedInstanceState.getInt(STATE_QUALITY)) - val message: CharSequence? = savedInstanceState.getCharSequence(STATE_MESSAGE) - val viewOnce: MediaSelectionState.ViewOnceToggleState = MediaSelectionState.ViewOnceToggleState.fromCode(savedInstanceState.getInt(STATE_VIEW_ONCE)) - val touchEnabled: Boolean = savedInstanceState.getBoolean(STATE_TOUCH_ENABLED) - val sent: Boolean = savedInstanceState.getBoolean(STATE_SENT) - val cameraFirstCapture: Media? = savedInstanceState.getParcelableCompat(STATE_CAMERA_FIRST_CAPTURE, Media::class.java) - val editorCount: Int = savedInstanceState.getInt(STATE_EDITOR_COUNT, 0) - val blobUri: Uri? = savedInstanceState.getParcelableCompat(STATE_EDITORS, Uri::class.java) - val blobProvider: BlobProvider = AppDependencies.blobs - val editorStates: List = if (editorCount > 0 && blobUri != null && blobProvider.hasStream(context, blobUri)) { - val accumulator: MutableList = mutableListOf() - val blob: ByteArray = ByteStreams.toByteArray(blobProvider.getStream(context, blobUri)) - val parcel: Parcel = Parcel.obtain() - parcel.unmarshall(blob, 0, blob.size) - parcel.setDataPosition(0) - for (index in 0 until editorCount) { - val bundle = parcel.readBundle(this::class.java.classLoader) - if (bundle != null && bundle != Bundle.EMPTY) { - accumulator.add(bundle) - } - } - parcel.recycle() - accumulator - } else { - emptyList() - } - val editorStateMap = editorStates.associate { it.toAssociation() } - - selectedMediaSubject.onNext(selection) - - store.update { state -> - state.copy( - selectedMedia = selection, - focusedMedia = focused, - quality = quality, - message = message, - viewOnceToggleState = viewOnce, - isTouchEnabled = touchEnabled, - isSent = sent, - cameraFirstCapture = cameraFirstCapture, - editorStateMap = editorStateMap - ) - } - } - - private fun Bundle.toAssociation(): Pair { - val key: Uri = requireNotNull(getParcelableCompat(BUNDLE_URI, Uri::class.java)) - - val value: Any = if (getBoolean(BUNDLE_IS_IMAGE)) { - ImageEditorFragment.Data(this) - } else { - VideoTrimData.fromBundle(this) - } - - return key to value - } - - private fun Map.Entry.toBundleStateEntry(): Bundle { - return when (val value = this.value) { - is ImageEditorFragment.Data -> { - value.bundle.apply { - putParcelable(BUNDLE_URI, key) - putBoolean(BUNDLE_IS_IMAGE, true) - } - } - - is VideoTrimData -> { - value.toBundle().apply { - putParcelable(BUNDLE_URI, key) - putBoolean(BUNDLE_IS_IMAGE, false) - } - } - - else -> { - throw IllegalStateException() - } - } - } - - companion object { - private const val STATE_PREFIX = "selection.view.model" - - private const val BUNDLE_URI = "$STATE_PREFIX.uri" - private const val BUNDLE_IS_IMAGE = "$STATE_PREFIX.is_image" - private const val STATE_SELECTION = "$STATE_PREFIX.selection" - private const val STATE_FOCUSED = "$STATE_PREFIX.focused" - private const val STATE_QUALITY = "$STATE_PREFIX.quality" - private const val STATE_MESSAGE = "$STATE_PREFIX.message" - private const val STATE_VIEW_ONCE = "$STATE_PREFIX.viewOnce" - private const val STATE_TOUCH_ENABLED = "$STATE_PREFIX.touchEnabled" - private const val STATE_SENT = "$STATE_PREFIX.sent" - private const val STATE_CAMERA_FIRST_CAPTURE = "$STATE_PREFIX.camera_first_capture" - private const val STATE_EDITORS = "$STATE_PREFIX.editors" - private const val STATE_EDITOR_COUNT = "$STATE_PREFIX.editor_count" - - @JvmStatic - fun clampToMaxClipDuration(data: VideoTrimData, maxVideoDurationUs: Long, preserveStartTime: Boolean): VideoTrimData { - if (!MediaConstraints.isVideoTranscodeAvailable()) { - return data - } - - if ((data.endTimeUs - data.startTimeUs) <= maxVideoDurationUs) { - return data - } - - return data.copy( - isDurationEdited = true, - startTimeUs = if (!preserveStartTime) data.endTimeUs - maxVideoDurationUs else data.startTimeUs, - endTimeUs = if (preserveStartTime) data.startTimeUs + maxVideoDurationUs else data.endTimeUs - ) - } - } - - class Factory( - private val destination: MediaSelectionDestination, - private val sendType: MessageSendType, - private val initialMedia: List, - private val initialMessage: CharSequence?, - private val isReply: Boolean, - private val isStory: Boolean, - private val isAddToGroupStoryFlow: Boolean, - private val repository: MediaSelectionRepository - ) : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - return requireNotNull(modelClass.cast(MediaSelectionViewModel(destination, sendType, initialMedia, initialMessage, isReply, isStory, isAddToGroupStoryFlow, repository))) - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/capture/MediaCaptureEvent.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/capture/MediaCaptureEvent.kt deleted file mode 100644 index 9a27b033a4..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/capture/MediaCaptureEvent.kt +++ /dev/null @@ -1,12 +0,0 @@ -package org.thoughtcrime.securesms.mediasend.v2.capture - -import org.signal.core.models.media.Media -import org.thoughtcrime.securesms.recipients.Recipient - -sealed interface MediaCaptureEvent { - data class MediaCaptureRendered(val media: Media) : MediaCaptureEvent - data class UsernameScannedFromQrCode(val recipient: Recipient, val username: String) : MediaCaptureEvent - data object DeviceLinkScannedFromQrCode : MediaCaptureEvent - data object MediaCaptureRenderFailed : MediaCaptureEvent - data class ReregistrationScannedFromQrCode(val data: String) : MediaCaptureEvent -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/capture/MediaCaptureFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/capture/MediaCaptureFragment.kt deleted file mode 100644 index 745fb3afc1..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/capture/MediaCaptureFragment.kt +++ /dev/null @@ -1,192 +0,0 @@ -package org.thoughtcrime.securesms.mediasend.v2.capture - -import android.os.Bundle -import android.view.View -import android.widget.Toast -import androidx.activity.OnBackPressedCallback -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.navigation.fragment.findNavController -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import org.signal.core.ui.permissions.Permissions -import org.signal.core.util.SeekableFileDescriptor -import org.signal.core.util.concurrent.LifecycleDisposable -import org.signal.core.util.logging.Log -import org.signal.mediasend.MediaConstraints -import org.signal.mediasend.screens.capture.CameraFragment -import org.signal.mediasend.screens.capture.CameraXFragment -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity -import org.thoughtcrime.securesms.mediasend.v2.HudCommand -import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionNavigator -import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionViewModel -import org.thoughtcrime.securesms.registration.olddevice.QuickTransferOldDeviceActivity -import org.thoughtcrime.securesms.stories.Stories -import org.thoughtcrime.securesms.util.CommunicationActions -import org.thoughtcrime.securesms.util.navigation.safeNavigate -import java.util.concurrent.TimeUnit -import kotlin.time.Duration.Companion.milliseconds - -private val TAG = Log.tag(MediaCaptureFragment::class.java) - -/** - * Fragment which displays the proper camera fragment. - */ -class MediaCaptureFragment : Fragment(R.layout.fragment_container), CameraFragment.Controller { - - private val sharedViewModel: MediaSelectionViewModel by viewModels( - ownerProducer = { requireActivity() } - ) - - private val viewModel: MediaCaptureViewModel by viewModels( - factoryProducer = { MediaCaptureViewModel.Factory(MediaCaptureRepository(requireContext())) } - ) - - private lateinit var captureChildFragment: CameraFragment - private lateinit var navigator: MediaSelectionNavigator - - private val lifecycleDisposable = LifecycleDisposable() - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - captureChildFragment = CameraXFragment.newInstance(sharedViewModel.isContactSelectionRequired) as CameraFragment - - navigator = MediaSelectionNavigator( - toGallery = R.id.action_mediaCaptureFragment_to_mediaGalleryFragment - ) - - childFragmentManager - .beginTransaction() - .replace(R.id.fragment_container, captureChildFragment as Fragment) - .commitNowAllowingStateLoss() - - lifecycleDisposable += viewModel.events.subscribe { event -> - when (event) { - MediaCaptureEvent.MediaCaptureRenderFailed -> { - Log.w(TAG, "Failed to render captured media.") - Toast.makeText(requireContext(), R.string.MediaSendActivity_camera_unavailable, Toast.LENGTH_SHORT).show() - } - - is MediaCaptureEvent.MediaCaptureRendered -> { - if (isFirst()) { - sharedViewModel.addCameraFirstCapture(event.media) - } else { - sharedViewModel.addMedia(event.media) - } - - navigator.goToReview(findNavController()) - } - - is MediaCaptureEvent.UsernameScannedFromQrCode -> { - MaterialAlertDialogBuilder(requireContext()) - .setTitle(getString(R.string.MediaCaptureFragment_username_dialog_title, event.username)) - .setMessage(getString(R.string.MediaCaptureFragment_username_dialog_body, event.username)) - .setPositiveButton(R.string.MediaCaptureFragment_username_dialog_go_to_chat_button) { d, _ -> - CommunicationActions.startConversation(requireContext(), event.recipient, "") - requireActivity().finish() - } - .setNegativeButton(android.R.string.cancel, null) - .show() - } - - is MediaCaptureEvent.DeviceLinkScannedFromQrCode -> { - MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.MediaCaptureFragment_device_link_dialog_title) - .setMessage(R.string.MediaCaptureFragment_it_looks_like_youre_trying) - .setPositiveButton(R.string.MediaCaptureFragment_device_link_dialog_continue) { d, _ -> - startActivity(AppSettingsActivity.linkedDevices(requireContext())) - requireActivity().finish() - } - .setNegativeButton(android.R.string.cancel, null) - .show() - } - - is MediaCaptureEvent.ReregistrationScannedFromQrCode -> { - startActivity(QuickTransferOldDeviceActivity.intent(requireContext(), event.data)) - requireActivity().finish() - } - } - } - - sharedViewModel.state.observe(viewLifecycleOwner) { state -> - captureChildFragment.presentHud(state.selectedMedia.size) - } - - lifecycleDisposable.bindTo(viewLifecycleOwner) - lifecycleDisposable += sharedViewModel.hudCommands.subscribe { command -> - if (command == HudCommand.GoToText) { - findNavController().safeNavigate(R.id.action_mediaCaptureFragment_to_textStoryPostCreationFragment) - } else if (command == HudCommand.GoToReview) { - navigator.goToReview(findNavController()) - } - } - - if (isFirst() || sharedViewModel.isSelectedMediaEmpty()) { - requireActivity().onBackPressedDispatcher.addCallback( - viewLifecycleOwner, - object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - requireActivity().finish() - } - } - ) - } - } - - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { - Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults) - } - - override fun onResume() { - super.onResume() - captureChildFragment.fadeInControls() - } - - override fun onImageCaptured(data: ByteArray, width: Int, height: Int) { - viewModel.onImageCaptured(data, width, height) - } - - override fun onVideoCaptured(fd: SeekableFileDescriptor, durationMs: Long) { - sharedViewModel.onVideoRecorded(durationMs.milliseconds) - viewModel.onVideoCaptured(fd) - } - - override fun onVideoCaptureError() { - Log.w(TAG, "Video capture error.") - context?.let { context -> - Toast.makeText(context, R.string.MediaSendActivity_camera_unavailable, Toast.LENGTH_SHORT).show() - } - } - - override fun onGalleryClicked() { - val controller = findNavController() - captureChildFragment.fadeOutControls { - navigator.goToGallery(controller) - } - } - - override fun onCameraCloseClicked() { - // TODO [media-send] - Alex R to supply dialog. - requireActivity().finish() - } - - override fun onQrCodeFound(data: String) { - viewModel.onQrCodeFound(data) - } - - override fun getMediaConstraints(): MediaConstraints { - return sharedViewModel.getMediaConstraints() - } - - override fun getMaxVideoDuration(): Int { - return if (sharedViewModel.isStory()) TimeUnit.MILLISECONDS.toSeconds(Stories.MAX_VIDEO_DURATION_MILLIS).toInt() else -1 - } - - private fun isFirst(): Boolean { - return arguments?.getBoolean("first") == true - } - - companion object { - const val CAPTURE_RESULT = "capture_result" - const val CAPTURE_RESULT_OK = "capture_result_ok" - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/capture/MediaCaptureRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/capture/MediaCaptureRepository.kt deleted file mode 100644 index 68a9339f59..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/capture/MediaCaptureRepository.kt +++ /dev/null @@ -1,95 +0,0 @@ -package org.thoughtcrime.securesms.mediasend.v2.capture - -import android.content.Context -import android.net.Uri -import org.signal.core.models.media.Media -import org.signal.core.util.ContentTypeUtil -import org.signal.core.util.SeekableFileDescriptor -import org.signal.core.util.closeQuietly -import org.signal.core.util.concurrent.SignalExecutors -import org.signal.core.util.contentproviders.BlobProvider -import org.thoughtcrime.securesms.dependencies.AppDependencies -import org.thoughtcrime.securesms.video.videoconverter.utils.VideoConstants -import java.io.FileInputStream -import java.io.IOException - -class MediaCaptureRepository(context: Context) { - - private val context: Context = context.applicationContext - - fun renderImageToMedia(data: ByteArray, width: Int, height: Int, onMediaRendered: (Media) -> Unit, onFailedToRender: () -> Unit) { - SignalExecutors.BOUNDED.execute { - val media: Media? = renderCaptureToMedia( - dataSupplier = { data }, - getLength = { data.size.toLong() }, - createBlobBuilder = { blobProvider, bytes, _ -> blobProvider.forData(bytes) }, - mimeType = ContentTypeUtil.IMAGE_JPEG, - width = width, - height = height - ) - - if (media != null) { - onMediaRendered(media) - } else { - onFailedToRender() - } - } - } - - /** Takes ownership of [fileDescriptor], closing it once the recording has been copied. */ - fun renderVideoToMedia(fileDescriptor: SeekableFileDescriptor, onMediaRendered: (Media) -> Unit, onFailedToRender: () -> Unit) { - SignalExecutors.BOUNDED.execute { - val media: Media? = renderCaptureToMedia( - dataSupplier = { FileInputStream(fileDescriptor.fileDescriptor) }, - getLength = { it.channel.size() }, - createBlobBuilder = BlobProvider::forData, - mimeType = VideoConstants.RECORDED_VIDEO_CONTENT_TYPE, - width = 0, - height = 0 - ) - - fileDescriptor.closeQuietly() - - if (media != null) { - onMediaRendered(media) - } else { - onFailedToRender() - } - } - } - - private fun renderCaptureToMedia( - dataSupplier: () -> T, - getLength: (T) -> Long, - createBlobBuilder: (BlobProvider, T, Long) -> BlobProvider.BlobBuilder, - mimeType: String, - width: Int, - height: Int - ): Media? { - return try { - val data: T = dataSupplier() - val length: Long = getLength(data) - val uri: Uri = createBlobBuilder(AppDependencies.blobs, data, length) - .withMimeType(mimeType) - .createForSingleSessionOnDisk(context) - - Media( - uri = uri, - contentType = mimeType, - date = System.currentTimeMillis(), - width = width, - height = height, - size = length, - duration = 0, - isBorderless = false, - isVideoGif = false, - bucketId = Media.ALL_MEDIA_BUCKET_ID, - caption = null, - transformProperties = null, - fileName = null - ) - } catch (e: IOException) { - return null - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/capture/MediaCaptureViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/capture/MediaCaptureViewModel.kt deleted file mode 100644 index 7fb0aa7b6a..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/capture/MediaCaptureViewModel.kt +++ /dev/null @@ -1,109 +0,0 @@ -package org.thoughtcrime.securesms.mediasend.v2.capture - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign -import io.reactivex.rxjava3.schedulers.Schedulers -import io.reactivex.rxjava3.subjects.PublishSubject -import io.reactivex.rxjava3.subjects.Subject -import org.signal.core.models.media.Media -import org.signal.core.util.SeekableFileDescriptor -import org.signal.core.util.logging.Log -import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.profiles.manage.UsernameRepository -import org.thoughtcrime.securesms.recipients.Recipient -import org.thoughtcrime.securesms.registration.data.QuickRegistrationRepository -import java.util.concurrent.TimeUnit - -class MediaCaptureViewModel(private val repository: MediaCaptureRepository) : ViewModel() { - - companion object { - private val TAG = Log.tag(MediaCaptureViewModel::class.java) - } - - private val internalEvents: Subject = PublishSubject.create() - private val qrData: Subject = PublishSubject.create() - - val events: Observable = internalEvents.observeOn(AndroidSchedulers.mainThread()) - val disposables = CompositeDisposable() - - init { - disposables += qrData - .throttleFirst(5, TimeUnit.SECONDS) - .filter { UsernameRepository.isValidLink(it) } - .subscribeOn(Schedulers.io()) - .flatMapSingle { url -> - UsernameRepository.fetchUsernameAndAciFromLink(url) - .map { result -> - when (result) { - is UsernameRepository.UsernameLinkConversionResult.Success -> QrScanResult.Success(result.username.toString(), Recipient.externalUsername(result.aci, result.username.toString())) - is UsernameRepository.UsernameLinkConversionResult.Invalid, - is UsernameRepository.UsernameLinkConversionResult.NotFound, - is UsernameRepository.UsernameLinkConversionResult.NetworkError -> QrScanResult.Failure - } - } - } - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { data -> - if (data is QrScanResult.Success) { - internalEvents.onNext(MediaCaptureEvent.UsernameScannedFromQrCode(data.recipient, data.username)) - } else { - Log.w(TAG, "Failed to scan QR code.") - } - } - - disposables += qrData - .throttleFirst(5, TimeUnit.SECONDS) - .filter { it.startsWith("sgnl://linkdevice") && SignalStore.account.isPrimaryDevice } - .subscribe { data -> - internalEvents.onNext(MediaCaptureEvent.DeviceLinkScannedFromQrCode) - } - - if (SignalStore.account.isRegistered) { - disposables += qrData - .throttleFirst(5, TimeUnit.SECONDS) - .filter { it.startsWith("sgnl://rereg") && QuickRegistrationRepository.isValidReRegistrationQr(it) && SignalStore.account.isPrimaryDevice } - .subscribe { data -> - internalEvents.onNext(MediaCaptureEvent.ReregistrationScannedFromQrCode(data)) - } - } - } - - override fun onCleared() { - disposables.dispose() - } - - fun onImageCaptured(data: ByteArray, width: Int, height: Int) { - repository.renderImageToMedia(data, width, height, this::onMediaRendered, this::onMediaRenderFailed) - } - - fun onVideoCaptured(fd: SeekableFileDescriptor) { - repository.renderVideoToMedia(fd, this::onMediaRendered, this::onMediaRenderFailed) - } - - fun onQrCodeFound(data: String) { - qrData.onNext(data) - } - - private fun onMediaRendered(media: Media) { - internalEvents.onNext(MediaCaptureEvent.MediaCaptureRendered(media)) - } - - private fun onMediaRenderFailed() { - internalEvents.onNext(MediaCaptureEvent.MediaCaptureRenderFailed) - } - - private sealed class QrScanResult { - data class Success(val username: String, val recipient: Recipient) : QrScanResult() - object Failure : QrScanResult() - } - - class Factory(private val repository: MediaCaptureRepository) : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - return requireNotNull(modelClass.cast(MediaCaptureViewModel(repository))) - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/documents/MediaReviewDocumentPageFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/documents/MediaReviewDocumentPageFragment.kt deleted file mode 100644 index f3827a04c3..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/documents/MediaReviewDocumentPageFragment.kt +++ /dev/null @@ -1,63 +0,0 @@ -package org.thoughtcrime.securesms.mediasend.v2.documents - -import android.os.Bundle -import android.view.View -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import org.signal.core.models.media.Media -import org.signal.core.util.getParcelableCompat -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.mediasend.MediaSendDocumentFragment -import org.thoughtcrime.securesms.mediasend.v2.HudCommand -import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionViewModel - -private const val DOCUMENT_TAG = "media.send.document.fragment" - -/** - * Fragment which ensures we fire off ResumeEntryTransition when viewing a document. - */ -class MediaReviewDocumentPageFragment : Fragment(R.layout.fragment_container) { - - private lateinit var mediaSendDocumentFragment: MediaSendDocumentFragment - - private val sharedViewModel: MediaSelectionViewModel by viewModels(ownerProducer = { requireActivity() }) - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - mediaSendDocumentFragment = ensureFragment() - sharedViewModel.sendCommand(HudCommand.ResumeEntryTransition) - } - - private fun ensureFragment(): MediaSendDocumentFragment { - val fragmentInManager: MediaSendDocumentFragment? = childFragmentManager.findFragmentByTag(DOCUMENT_TAG) as? MediaSendDocumentFragment - - return if (fragmentInManager != null) { - fragmentInManager - } else { - val mediaSendDocumentFragment = MediaSendDocumentFragment.newInstance(requireMedia()) - - childFragmentManager.beginTransaction() - .replace( - R.id.fragment_container, - mediaSendDocumentFragment, - DOCUMENT_TAG - ) - .commitAllowingStateLoss() - - mediaSendDocumentFragment - } - } - - private fun requireMedia(): Media = requireNotNull(requireArguments().getParcelableCompat(ARG_MEDIA, Media::class.java)) - - companion object { - private const val ARG_MEDIA = "arg.media" - - fun newInstance(media: Media): Fragment { - return MediaReviewDocumentPageFragment().apply { - arguments = Bundle().apply { - putParcelable(ARG_MEDIA, media) - } - } - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaGalleryFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaGalleryFragment.kt index 3d4d1fcc42..3ddbfa0a8e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaGalleryFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaGalleryFragment.kt @@ -31,7 +31,6 @@ import org.thoughtcrime.securesms.components.recyclerview.GridDividerDecoration import org.thoughtcrime.securesms.conversation.ManageContextMenu import org.thoughtcrime.securesms.databinding.V2MediaGalleryFragmentBinding import org.thoughtcrime.securesms.mediasend.MediaRepository -import org.thoughtcrime.securesms.mediasend.v2.review.MediaGalleryGridItemTouchListener import org.thoughtcrime.securesms.util.Material3OnScrollHelper import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaGalleryGridItemTouchListener.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaGalleryGridItemTouchListener.kt similarity index 98% rename from app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaGalleryGridItemTouchListener.kt rename to app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaGalleryGridItemTouchListener.kt index f7accb903e..06a26c4c68 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaGalleryGridItemTouchListener.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaGalleryGridItemTouchListener.kt @@ -1,9 +1,9 @@ /* - * Copyright 2025 Signal Messenger, LLC + * Copyright 2026 Signal Messenger, LLC * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.mediasend.v2.review +package org.thoughtcrime.securesms.mediasend.v2.gallery import android.content.Context import android.content.res.Resources diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaGallerySelectableItem.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaGallerySelectableItem.kt index c5741e2347..1726f64cc5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaGallerySelectableItem.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaGallerySelectableItem.kt @@ -23,7 +23,6 @@ import org.signal.core.util.DimensionUnit import org.signal.core.util.logging.Log import org.signal.glide.decryptableuri.DecryptableUri import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.mediasend.v2.review.MediaGalleryGridItemTouchListener import org.thoughtcrime.securesms.mms.PartAuthority import org.thoughtcrime.securesms.util.MediaUtil import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaSelectionGalleryFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaSelectionGalleryFragment.kt deleted file mode 100644 index 9d383259a5..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaSelectionGalleryFragment.kt +++ /dev/null @@ -1,151 +0,0 @@ -package org.thoughtcrime.securesms.mediasend.v2.gallery - -import android.os.Bundle -import android.view.View -import androidx.activity.OnBackPressedCallback -import androidx.core.content.ContextCompat -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.navigation.fragment.findNavController -import androidx.recyclerview.widget.ItemTouchHelper -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import org.signal.core.models.media.Media -import org.signal.core.ui.permissions.Permissions -import org.signal.core.util.concurrent.LifecycleDisposable -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionNavigator -import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionViewModel -import org.thoughtcrime.securesms.mediasend.v2.review.MediaSelectionItemTouchHelper -import org.signal.core.ui.R as CoreUiR - -private const val MEDIA_GALLERY_TAG = "MEDIA_GALLERY" - -class MediaSelectionGalleryFragment : Fragment(R.layout.fragment_container), MediaGalleryFragment.Callbacks { - - private lateinit var mediaGalleryFragment: MediaGalleryFragment - - private val navigator = MediaSelectionNavigator( - toCamera = R.id.action_mediaGalleryFragment_to_mediaCaptureFragment - ) - - private val sharedViewModel: MediaSelectionViewModel by viewModels( - ownerProducer = { requireActivity() } - ) - - private val lifecycleDisposable = LifecycleDisposable() - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - val args = arguments - val isFirst = when { - args == null -> false - args.containsKey("suppressEmptyError") -> args.getBoolean("suppressEmptyError") - args.containsKey("first") -> args.getBoolean("first") - else -> false - } - - lifecycleDisposable.bindTo(this) - sharedViewModel.setSuppressEmptyError(isFirst) - mediaGalleryFragment = ensureMediaGalleryFragment() - - mediaGalleryFragment.bindSelectedMediaItemDragHelper(ItemTouchHelper(MediaSelectionItemTouchHelper(sharedViewModel))) - - sharedViewModel.state.observe(viewLifecycleOwner) { state -> - mediaGalleryFragment.onViewStateUpdated( - MediaGalleryFragment.ViewState( - selectedMedia = state.selectedMedia, - chatColor = state.recipient?.chatColors?.asSingleColor() ?: ContextCompat.getColor(requireContext(), CoreUiR.color.signal_light_colorPrimary) - ) - ) - } - - lifecycleDisposable += sharedViewModel.mediaErrors - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - mediaGalleryFragment.onMediaErrorOccurred() - } - - requireActivity().onBackPressedDispatcher.addCallback( - viewLifecycleOwner, - object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - onBackPressed() - } - } - ) - } - - private fun ensureMediaGalleryFragment(): MediaGalleryFragment { - val fragmentInManager: MediaGalleryFragment? = childFragmentManager.findFragmentByTag(MEDIA_GALLERY_TAG) as? MediaGalleryFragment - - return if (fragmentInManager != null) { - fragmentInManager - } else { - val mediaGalleryFragment = MediaGalleryFragment() - - childFragmentManager.beginTransaction() - .replace( - R.id.fragment_container, - mediaGalleryFragment, - MEDIA_GALLERY_TAG - ) - .commitNowAllowingStateLoss() - - mediaGalleryFragment - } - } - - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { - Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults) - } - - override fun isMultiselectEnabled(): Boolean { - return true - } - - override fun onMediaSelected(media: Media) { - sharedViewModel.addMedia(media) - } - - override fun onMediaUnselected(media: Media) { - sharedViewModel.removeMedia(media) - } - - override fun onMediaSelected(media: Set) { - sharedViewModel.addMedia(media) - } - - override fun onMediaUnselected(media: Set) { - sharedViewModel.removeMedia(media) - } - - override fun onSelectedMediaClicked(media: Media) { - sharedViewModel.onPageChanged(media) - navigator.goToReview(findNavController()) - } - - override fun onNavigateToCamera() { - val controller = findNavController() - navigator.goToCamera(controller) - } - - override fun onSubmit() { - navigator.goToReview(findNavController()) - } - - override fun onToolbarNavigationClicked() { - onBackPressed() - } - - fun onBackPressed() { - if (arguments?.containsKey("first") == true) { - requireActivity().finish() - return - } - - if (navigator.isPreviousScreenMediaReview(findNavController()) && sharedViewModel.isSelectedMediaEmpty()) { - requireActivity().finish() - } else { - findNavController().popBackStack() - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gif/MediaReviewGifPageFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gif/MediaReviewGifPageFragment.kt deleted file mode 100644 index 2aece3bfa0..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gif/MediaReviewGifPageFragment.kt +++ /dev/null @@ -1,66 +0,0 @@ -package org.thoughtcrime.securesms.mediasend.v2.gif - -import android.net.Uri -import android.os.Bundle -import android.view.View -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import org.signal.core.util.getParcelableCompat -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.mediasend.MediaSendGifFragment -import org.thoughtcrime.securesms.mediasend.v2.HudCommand -import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionViewModel - -private const val GIF_TAG = "media.send.gif.fragment" - -/** - * Fragment which ensures we fire off ResumeEntryTransition when viewing a non-video gif. - */ -class MediaReviewGifPageFragment : Fragment(R.layout.fragment_container) { - - private lateinit var mediaSendGifFragment: MediaSendGifFragment - - private val sharedViewModel: MediaSelectionViewModel by viewModels(ownerProducer = { requireActivity() }) - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - mediaSendGifFragment = ensureGifFragment() - sharedViewModel.sendCommand(HudCommand.ResumeEntryTransition) - } - - private fun ensureGifFragment(): MediaSendGifFragment { - val fragmentInManager: MediaSendGifFragment? = childFragmentManager.findFragmentByTag(GIF_TAG) as? MediaSendGifFragment - - return if (fragmentInManager != null) { - sharedViewModel.sendCommand(HudCommand.ResumeEntryTransition) - fragmentInManager - } else { - val mediaSendGifFragment = MediaSendGifFragment.newInstance( - requireUri() - ) - - childFragmentManager.beginTransaction() - .replace( - R.id.fragment_container, - mediaSendGifFragment, - GIF_TAG - ) - .commitAllowingStateLoss() - - mediaSendGifFragment - } - } - - private fun requireUri(): Uri = requireNotNull(requireArguments().getParcelableCompat(ARG_URI, Uri::class.java)) - - companion object { - private const val ARG_URI = "arg.uri" - - fun newInstance(uri: Uri): Fragment { - return MediaReviewGifPageFragment().apply { - arguments = Bundle().apply { - putParcelable(ARG_URI, uri) - } - } - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/images/MediaReviewImagePageFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/images/MediaReviewImagePageFragment.kt deleted file mode 100644 index 145acd3041..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/images/MediaReviewImagePageFragment.kt +++ /dev/null @@ -1,165 +0,0 @@ -package org.thoughtcrime.securesms.mediasend.v2.images - -import android.net.Uri -import android.os.Bundle -import android.view.View -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import io.reactivex.rxjava3.disposables.Disposable -import org.signal.core.util.getParcelableCompat -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.mediasend.v2.HudCommand -import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionViewModel -import org.thoughtcrime.securesms.scribbles.ImageEditorFragment -import org.thoughtcrime.securesms.scribbles.ImageEditorHudV2 -import java.util.concurrent.TimeUnit - -private const val IMAGE_EDITOR_TAG = "image.editor.fragment" - -private val MODE_DELAY = TimeUnit.MILLISECONDS.toMillis(300) - -/** - * Displays the chosen image within the image editor. Also manages the "touch enabled" state of the shared - * view model. We utilize delays here to help with Animation choreography. - */ -class MediaReviewImagePageFragment : Fragment(R.layout.fragment_container), ImageEditorFragment.Controller { - - private val sharedViewModel: MediaSelectionViewModel by viewModels(ownerProducer = { requireActivity() }) - - private var imageEditorFragment: ImageEditorFragment? = null - private var hudCommandDisposable: Disposable = Disposable.disposed() - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - imageEditorFragment = ensureImageEditorFragment() - } - - override fun onPause() { - super.onPause() - - hudCommandDisposable.dispose() - } - - override fun onResume() { - super.onResume() - - hudCommandDisposable = sharedViewModel.hudCommands.subscribe { command -> - if (isResumed) { - when (command) { - HudCommand.StartDraw -> { - sharedViewModel.setTouchEnabled(false) - requireView().postDelayed( - { - imageEditorFragment?.setMode(ImageEditorHudV2.Mode.DRAW) - }, - MODE_DELAY - ) - } - HudCommand.StartCropAndRotate -> { - sharedViewModel.setTouchEnabled(false) - requireView().postDelayed( - { - imageEditorFragment?.setMode(ImageEditorHudV2.Mode.CROP) - }, - MODE_DELAY - ) - } - HudCommand.SaveMedia -> imageEditorFragment?.onSave() - else -> Unit - } - } - } - } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - - imageEditorFragment?.let { - sharedViewModel.setEditorState(requireUri(), requireNotNull(it.saveState())) - } - } - - private fun ensureImageEditorFragment(): ImageEditorFragment { - val fragmentInManager: ImageEditorFragment? = childFragmentManager.findFragmentByTag(IMAGE_EDITOR_TAG) as? ImageEditorFragment - - return if (fragmentInManager != null) { - fragmentInManager - } else { - val imageEditorFragment = ImageEditorFragment.newInstance( - requireUri() - ) - - childFragmentManager.beginTransaction() - .replace( - R.id.fragment_container, - imageEditorFragment, - IMAGE_EDITOR_TAG - ) - .commitAllowingStateLoss() - - imageEditorFragment - } - } - - private fun requireUri(): Uri = requireNotNull(requireArguments().getParcelableCompat(ARG_URI, Uri::class.java)) - - override fun onTouchEventsNeeded(needed: Boolean) { - if (isResumed) { - if (!needed) { - requireView().postDelayed( - { - sharedViewModel.setTouchEnabled(true) - }, - MODE_DELAY - ) - } else { - sharedViewModel.setTouchEnabled(false) - } - } - } - - override fun onRequestFullScreen(fullScreen: Boolean, hideKeyboard: Boolean) = Unit - - override fun onDoneEditing() { - imageEditorFragment?.setMode(ImageEditorHudV2.Mode.NONE) - - if (isResumed) { - imageEditorFragment?.let { - sharedViewModel.setEditorState(requireUri(), requireNotNull(it.saveState())) - } - } - } - - override fun onCancelEditing() { - restoreState() - } - - override fun onMainImageLoaded() { - sharedViewModel.sendCommand(HudCommand.ResumeEntryTransition) - } - - override fun onMainImageFailedToLoad() { - sharedViewModel.sendCommand(HudCommand.ResumeEntryTransition) - } - - override fun restoreState() { - val data = sharedViewModel.getEditorState(requireUri()) as? ImageEditorFragment.Data - - if (data != null) { - imageEditorFragment?.restoreState(data) - } else { - imageEditorFragment?.onClearAll() - } - } - - companion object { - private const val ARG_URI = "arg.uri" - - fun newInstance(uri: Uri): Fragment { - return MediaReviewImagePageFragment().apply { - arguments = Bundle().apply { - putParcelable(ARG_URI, uri) - } - } - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewAddItem.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewAddItem.kt deleted file mode 100644 index f8f88bd581..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewAddItem.kt +++ /dev/null @@ -1,36 +0,0 @@ -package org.thoughtcrime.securesms.mediasend.v2.review - -import android.view.View -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory -import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter -import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel -import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder - -typealias OnAddMediaItemClicked = () -> Unit - -object MediaReviewAddItem { - - fun register(mappingAdapter: MappingAdapter, onAddMediaItemClicked: OnAddMediaItemClicked) { - mappingAdapter.registerFactory(Model::class.java, LayoutFactory({ ViewHolder(it, onAddMediaItemClicked) }, R.layout.v2_media_review_add_media_item)) - } - - object Model : MappingModel { - override fun areItemsTheSame(newItem: Model): Boolean { - return true - } - - override fun areContentsTheSame(newItem: Model): Boolean { - return true - } - } - - class ViewHolder(itemView: View, onAddMediaItemClicked: OnAddMediaItemClicked) : MappingViewHolder(itemView) { - - init { - itemView.setOnClickListener { onAddMediaItemClicked() } - } - - override fun bind(model: Model) = Unit - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewAnimatorController.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewAnimatorController.kt deleted file mode 100644 index fe52627a14..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewAnimatorController.kt +++ /dev/null @@ -1,47 +0,0 @@ -package org.thoughtcrime.securesms.mediasend.v2.review - -import android.animation.Animator -import android.animation.ObjectAnimator -import android.animation.ValueAnimator -import android.view.View -import android.view.animation.Interpolator -import androidx.core.animation.doOnEnd -import org.thoughtcrime.securesms.mediasend.v2.MediaAnimations -import org.thoughtcrime.securesms.util.visible - -object MediaReviewAnimatorController { - - fun getFadeInAnimator(view: View, isEnabled: Boolean = true): Animator { - view.visible = true - view.isEnabled = isEnabled - - return ObjectAnimator.ofFloat(view, "alpha", view.alpha, 1f).apply { - interpolator = MediaAnimations.interpolator - } - } - - fun getFadeOutAnimator(view: View, isEnabled: Boolean = false): Animator { - view.isEnabled = isEnabled - - val animator = ObjectAnimator.ofFloat(view, "alpha", view.alpha, 0f).apply { - interpolator = MediaAnimations.interpolator - } - - animator.doOnEnd { view.visible = false } - - return animator - } - - fun getHeightAnimator(view: View, start: Int, end: Int, interpolator: Interpolator = MediaAnimations.interpolator): Animator { - return ValueAnimator.ofInt(start, end).apply { - setInterpolator(interpolator) - addUpdateListener { - val animatedValue = it.animatedValue as Int - val layoutParams = view.layoutParams - layoutParams.height = animatedValue - view.layoutParams = layoutParams - } - duration = 120 - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewFragment.kt deleted file mode 100644 index 4cf60d77d9..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewFragment.kt +++ /dev/null @@ -1,892 +0,0 @@ -package org.thoughtcrime.securesms.mediasend.v2.review - -import android.animation.Animator -import android.animation.AnimatorSet -import android.content.Context -import android.content.res.ColorStateList -import android.graphics.Color -import android.graphics.Rect -import android.net.Uri -import android.os.Bundle -import android.provider.OpenableColumns -import android.text.SpannableString -import android.view.Gravity -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.ProgressBar -import android.widget.TextView -import android.widget.ViewSwitcher -import androidx.activity.OnBackPressedCallback -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.content.ContextCompat -import androidx.core.graphics.drawable.DrawableCompat -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.updateLayoutParams -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.navigation.fragment.findNavController -import androidx.recyclerview.widget.ItemTouchHelper -import androidx.recyclerview.widget.RecyclerView -import androidx.viewpager2.widget.ViewPager2 -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.imageview.ShapeableImageView -import org.signal.core.models.media.Media -import org.signal.core.ui.BottomSheetUtil -import org.signal.core.ui.permissions.Permissions -import org.signal.core.util.bytes -import org.signal.core.util.concurrent.LifecycleDisposable -import org.signal.core.util.concurrent.SimpleTask -import org.signal.core.util.isNotNullOrBlank -import org.signal.core.util.logging.Log -import org.signal.mediasend.MediaConstraints -import org.signal.mediasend.SentMediaQuality -import org.signal.mediasend.screens.edit.video.VideoThumbnailsRangeSelectorView -import org.signal.mediasend.screens.edit.video.VideoTrimData -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey -import org.thoughtcrime.securesms.conversation.MessageSendType -import org.thoughtcrime.securesms.conversation.ReenableScheduledMessagesDialogFragment -import org.thoughtcrime.securesms.conversation.ScheduleMessageContextMenu -import org.thoughtcrime.securesms.conversation.ScheduleMessageDialogCallback -import org.thoughtcrime.securesms.conversation.ScheduleMessageTimePickerBottomSheet -import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardActivity -import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs -import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.media.DecryptableUriMediaInput -import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult -import org.thoughtcrime.securesms.mediasend.v2.HudCommand -import org.thoughtcrime.securesms.mediasend.v2.MediaAnimations -import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionNavigator -import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionState -import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionViewModel -import org.thoughtcrime.securesms.mediasend.v2.UntrustedRecords -import org.thoughtcrime.securesms.mediasend.v2.stories.StoriesMultiselectForwardActivity -import org.thoughtcrime.securesms.recipients.Recipient -import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet -import org.thoughtcrime.securesms.scribbles.ImageEditorFragment -import org.thoughtcrime.securesms.util.MediaUtil -import org.thoughtcrime.securesms.util.SystemWindowInsetsSetter -import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter -import org.thoughtcrime.securesms.util.fragments.requireListener -import org.thoughtcrime.securesms.util.views.TouchInterceptingFrameLayout -import org.thoughtcrime.securesms.util.visible -import org.thoughtcrime.securesms.video.TranscodingConfig -import org.thoughtcrime.securesms.video.TranscodingQuality -import java.io.IOException -import java.util.Locale -import java.util.concurrent.TimeUnit -import kotlin.math.roundToInt -import kotlin.time.Duration.Companion.microseconds -import org.signal.core.ui.R as CoreUiR -import org.signal.mediasend.R as MediaSendR - -/** - * Allows the user to view and edit selected media. - */ -class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment), ScheduleMessageTimePickerBottomSheet.ScheduleCallback, ScheduleMessageDialogCallback, VideoThumbnailsRangeSelectorView.RangeDragListener, SafetyNumberBottomSheet.Callbacks { - - private val sharedViewModel: MediaSelectionViewModel by viewModels( - ownerProducer = { requireActivity() } - ) - - private lateinit var callback: Callback - - private lateinit var drawToolButton: View - private lateinit var cropAndRotateButton: View - private lateinit var qualityButton: ImageView - private lateinit var muteVideoAudioButton: ImageView - private lateinit var saveButton: View - private lateinit var sendButton: ImageView - private lateinit var addMediaButton: View - private lateinit var viewOnceButton: ViewSwitcher - private lateinit var emojiButton: ShapeableImageView - private lateinit var addMessageButton: TextView - private lateinit var recipientDisplay: TextView - private lateinit var pager: ViewPager2 - private lateinit var controls: ConstraintLayout - private lateinit var selectionRecycler: RecyclerView - private lateinit var controlsShade: View - private lateinit var videoTimeLine: VideoThumbnailsRangeSelectorView - private lateinit var videoSizeHint: TextView - private lateinit var videoTimelinePlaceholder: View - private lateinit var progress: ProgressBar - private lateinit var progressWrapper: TouchInterceptingFrameLayout - - private val exclusionZone = listOf(Rect()) - private val navigator = MediaSelectionNavigator( - toGallery = R.id.action_mediaReviewFragment_to_mediaGalleryFragment - ) - - private var animatorSet: AnimatorSet? = null - private var disposables: LifecycleDisposable = LifecycleDisposable() - private var sentMediaQuality: SentMediaQuality = SignalStore.settings.sentMediaQuality - private var viewOnceToggleState: MediaSelectionState.ViewOnceToggleState = MediaSelectionState.ViewOnceToggleState.default - private var scheduledSendTime: Long? = null - private var readyToSend = true - - private val multiselectLauncher = registerForActivityResult(MultiselectForwardActivity.SelectionContract()) { keys -> - if (keys.isNotEmpty()) { - Log.d(TAG, "Performing send from multi-select activity result.") - performSend(keys) - } else { - readyToSend = true - } - } - - private val storiesLauncher = registerForActivityResult(StoriesMultiselectForwardActivity.SelectionContract()) { keys -> - if (keys.isNotEmpty()) { - Log.d(TAG, "Performing send from stories activity result.") - performSend(keys) - } else { - readyToSend = true - } - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - postponeEnterTransition() - - SystemWindowInsetsSetter.attach(view, viewLifecycleOwner, WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()) - - disposables.bindTo(viewLifecycleOwner) - - parentFragmentManager.setFragmentResultListener(AddMessageDialogFragment.REQUEST_KEY, viewLifecycleOwner) { _, bundle -> - if (bundle.getBoolean(AddMessageDialogFragment.RESULT_INCREMENT_VIEW_ONCE_STATE)) { - sharedViewModel.setMessage(null) - sharedViewModel.incrementViewOnceState() - } else { - sharedViewModel.setMessage(bundle.getCharSequence(AddMessageDialogFragment.RESULT_MESSAGE, null)) - } - } - - callback = requireListener() - - drawToolButton = view.findViewById(R.id.draw_tool) - cropAndRotateButton = view.findViewById(R.id.crop_and_rotate_tool) - qualityButton = view.findViewById(R.id.quality_selector) - muteVideoAudioButton = view.findViewById(R.id.mute_video_audio) - saveButton = view.findViewById(R.id.save_to_media) - sendButton = view.findViewById(R.id.send) - addMediaButton = view.findViewById(R.id.add_media) - viewOnceButton = view.findViewById(R.id.view_once_toggle) - emojiButton = view.findViewById(R.id.emoji_button) - addMessageButton = view.findViewById(R.id.add_a_message) - recipientDisplay = view.findViewById(R.id.recipient) - pager = view.findViewById(R.id.media_pager) - controls = view.findViewById(R.id.controls) - selectionRecycler = view.findViewById(R.id.selection_recycler) - controlsShade = view.findViewById(R.id.controls_shade) - progress = view.findViewById(R.id.progress) - progressWrapper = view.findViewById(R.id.progress_wrapper) - videoTimeLine = view.findViewById(R.id.video_timeline) - videoSizeHint = view.findViewById(R.id.video_size_hint) - videoTimelinePlaceholder = view.findViewById(R.id.timeline_placeholder) - - DrawableCompat.setTint(progress.indeterminateDrawable, Color.WHITE) - progressWrapper.setOnInterceptTouchEventListener { true } - - val pagerAdapter = MediaReviewFragmentPagerAdapter(this) - - disposables += sharedViewModel.hudCommands.subscribe { - when (it) { - HudCommand.ResumeEntryTransition -> startPostponedEnterTransition() - else -> Unit - } - } - - pager.adapter = pagerAdapter - - controls.addOnLayoutChangeListener { v, left, _, right, _, _, _, _, _ -> - val outRect: Rect = exclusionZone[0] - videoTimeLine.getHitRect(outRect) - outRect.left = left - outRect.right = right - ViewCompat.setSystemGestureExclusionRects(v, exclusionZone) - } - - drawToolButton.setOnClickListener { - sharedViewModel.sendCommand(HudCommand.StartDraw) - } - - cropAndRotateButton.setOnClickListener { - sharedViewModel.sendCommand(HudCommand.StartCropAndRotate) - } - - qualityButton.setOnClickListener { - QualitySelectorBottomSheet().show(parentFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) - } - - muteVideoAudioButton.setOnClickListener { - sharedViewModel.toggleVideoMuted() - } - - saveButton.setOnClickListener { - sharedViewModel.sendCommand(HudCommand.SaveMedia) - } - - sendButton.setOnClickListener { - if (!readyToSend) { - Log.d(TAG, "Attachment send button not currently enabled. Ignoring click event.") - return@setOnClickListener - } else { - Log.d(TAG, "Attachment send button enabled. Processing click event.") - readyToSend = false - } - - val viewOnce: Boolean = sharedViewModel.state.value?.viewOnceToggleState == MediaSelectionState.ViewOnceToggleState.ONCE - - if (sharedViewModel.isContactSelectionRequired) { - val args = MultiselectForwardFragmentArgs( - title = R.string.MediaReviewFragment__send_to, - storySendRequirements = sharedViewModel.getStorySendRequirements(), - isSearchEnabled = !sharedViewModel.isStory(), - isViewOnce = viewOnce - ) - - if (sharedViewModel.isStory()) { - val snapshot = sharedViewModel.state.value - - if (snapshot != null) { - readyToSend = false - SimpleTask.run(viewLifecycleOwner.lifecycle, { - snapshot.selectedMedia.take(2).map { media -> - val editorData = snapshot.editorStateMap[media.uri] - if (MediaUtil.isImageType(media.contentType) && editorData != null && editorData is ImageEditorFragment.Data) { - val model = editorData.readModel() - if (model != null) { - ImageEditorFragment.renderToSingleSessionBlob(requireContext(), model) - } else { - media.uri - } - } else { - media.uri - } - } - }, { - storiesLauncher.launch(StoriesMultiselectForwardActivity.Args(args, it)) - }) - } else { - storiesLauncher.launch(StoriesMultiselectForwardActivity.Args(args, emptyList())) - } - scheduledSendTime = null - } else { - multiselectLauncher.launch(args) - } - } else if (sharedViewModel.isAddToGroupStoryFlow) { - MaterialAlertDialogBuilder(requireContext()) - .setMessage(getString(R.string.MediaReviewFragment__add_to_the_group_story, sharedViewModel.state.value!!.recipient!!.getDisplayName(requireContext()))) - .setPositiveButton(R.string.MediaReviewFragment__add_to_story) { _, _ -> - Log.d(TAG, "Performing send add to group story dialog.") - performSend() - } - .setNegativeButton(android.R.string.cancel) { _, _ -> - readyToSend = true - } - .setOnCancelListener { - readyToSend = true - } - .setOnDismissListener { - readyToSend = true - } - .show() - scheduledSendTime = null - } else { - Log.d(TAG, "Performing send from send button.") - performSend() - } - } - if (!sharedViewModel.isStory()) { - sendButton.setOnLongClickListener { - ScheduleMessageContextMenu.show(it, (requireView() as ViewGroup)) { time: Long -> - if (time == -1L) { - scheduledSendTime = null - ScheduleMessageTimePickerBottomSheet.showSchedule(childFragmentManager) - } else { - startScheduledSend(time) - } - } - true - } - } - - addMediaButton.setOnClickListener { - launchGallery() - } - - viewOnceButton.setOnClickListener { - sharedViewModel.incrementViewOnceState() - } - - emojiButton.setOnClickListener { - sharedViewModel.state.value?.let { state -> - AddMessageDialogFragment.show( - parentFragmentManager, - state.message, - true, - state.selectedMedia.size == 1 && !state.isStory && !MediaUtil.isDocumentType(state.focusedMedia?.contentType), - sharedViewModel.destination.getRecipientSearchKey()?.recipientId - ) - } - } - - addMessageButton.setOnClickListener { - sharedViewModel.state.value?.let { state -> - AddMessageDialogFragment.show( - parentFragmentManager, - state.message, - false, - state.selectedMedia.size == 1 && !state.isStory && !MediaUtil.isDocumentType(state.focusedMedia?.contentType), - sharedViewModel.destination.getRecipientSearchKey()?.recipientId - ) - } - } - - if (sharedViewModel.isReply) { - addMessageButton.setText(R.string.MediaReviewFragment__add_a_reply) - } - - pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { - override fun onPageSelected(position: Int) { - qualityButton.alpha = 0f - saveButton.alpha = 0f - sharedViewModel.onPageChanged(position) - } - }) - - if (MediaConstraints.isVideoTranscodeAvailable()) { - videoTimeLine.registerEditorOnRangeChangeListener(this) - } - - val selectionAdapter = MappingAdapter(false) - MediaReviewAddItem.register(selectionAdapter) { - launchGallery() - } - MediaReviewSelectedItem.register(selectionAdapter) { media, isSelected -> - if (isSelected) { - sharedViewModel.removeMedia(media) - } else { - sharedViewModel.onPageChanged(media) - } - } - selectionRecycler.adapter = selectionAdapter - ItemTouchHelper(MediaSelectionItemTouchHelper(sharedViewModel)).attachToRecyclerView(selectionRecycler) - - sharedViewModel.state.observe(viewLifecycleOwner) { state -> - pagerAdapter.submitMedia(state.selectedMedia) - - selectionAdapter.submitList( - state.selectedMedia.map { - val trimStartTimeUs = (state.editorStateMap[it.uri] as? VideoTrimData)?.startTimeUs ?: 0L - MediaReviewSelectedItem.Model(it, state.focusedMedia == it, trimStartTimeUs) - } + MediaReviewAddItem.Model - ) - - presentSendButton(readyToSend, state.sendType, state.recipient) - presentPager(state) - presentAddMessageEntry(state.viewOnceToggleState, state.message) - presentImageQualityToggle(state) - presentMuteVideoAudioToggle(state) - if (state.quality != sentMediaQuality) { - presentQualityToggleToast(state) - } - sentMediaQuality = state.quality - - viewOnceButton.displayedChild = if (state.viewOnceToggleState == MediaSelectionState.ViewOnceToggleState.ONCE) 1 else 0 - if (state.viewOnceToggleState != viewOnceToggleState && - state.viewOnceToggleState == MediaSelectionState.ViewOnceToggleState.ONCE && - state.selectedMedia.size == 1 - ) { - presentViewOnceToggleToast(MediaUtil.isNonGifVideo(state.selectedMedia[0])) - } - viewOnceToggleState = state.viewOnceToggleState - - presentVideoTimeline(state) - presentVideoSizeHint(state) - - computeViewStateAndAnimate(state) - } - - requireActivity().onBackPressedDispatcher.addCallback( - viewLifecycleOwner, - object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - callback.onPopFromReview() - } - } - ) - } - - private fun presentViewOnceToggleToast(isVideo: Boolean) { - val description = if (isVideo) { - getString(R.string.MediaReviewFragment__video_set_to_view_once) - } else { - getString(R.string.MediaReviewFragment__photo_set_to_view_once) - } - - MediaReviewToastPopupWindow.show(controls, CoreUiR.drawable.symbol_view_once_24, description) - } - - private fun presentQualityToggleToast(state: MediaSelectionState) { - val mediaList = state.selectedMedia - if (mediaList.isEmpty()) { - return - } - - val description = if (mediaList.size == 1) { - val media: Media = mediaList[0] - if (MediaUtil.isNonGifVideo(media)) { - if (state.quality == SentMediaQuality.HIGH) { - getString(MediaSendR.string.MediaReviewFragment__video_set_to_high_quality) - } else { - getString(MediaSendR.string.MediaReviewFragment__video_set_to_standard_quality) - } - } else if (MediaUtil.isImageType(media.contentType)) { - if (state.quality == SentMediaQuality.HIGH) { - getString(MediaSendR.string.MediaReviewFragment__photo_set_to_high_quality) - } else { - getString(MediaSendR.string.MediaReviewFragment__photo_set_to_standard_quality) - } - } else { - Log.i(TAG, "Could not display quality toggle toast for attachment of type: ${media.contentType}") - return - } - } else { - if (state.quality == SentMediaQuality.HIGH) { - resources.getQuantityString(MediaSendR.plurals.MediaReviewFragment__items_set_to_high_quality, mediaList.size, mediaList.size) - } else { - resources.getQuantityString(MediaSendR.plurals.MediaReviewFragment__items_set_to_standard_quality, mediaList.size, mediaList.size) - } - } - - val icon = when (state.quality) { - SentMediaQuality.HIGH -> CoreUiR.drawable.symbol_quality_high_24 - else -> CoreUiR.drawable.symbol_quality_high_slash_24 - } - - MediaReviewToastPopupWindow.show(controls, icon, description) - } - - override fun onResume() { - super.onResume() - sharedViewModel.kick() - } - - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { - Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults) - } - - private fun launchGallery() { - val controller = findNavController() - navigator.goToGallery(controller) - } - - private fun performSend(selection: List = listOf()) { - Log.d(TAG, "Performing attachment send.") - readyToSend = false - progressWrapper.visible = true - progressWrapper.animate() - .setStartDelay(300) - .setInterpolator(MediaAnimations.interpolator) - .alpha(1f) - - sharedViewModel - .send(selection.filterIsInstance(), scheduledSendTime) - .subscribe( - { result -> - callback.onSentWithResult(result) - readyToSend = true - }, - { error -> - if (error is UntrustedRecords.UntrustedRecordsException) { - Log.w(TAG, "Send failed due to untrusted identities.") - hideSendProgress() - SafetyNumberBottomSheet - .forIdentityRecordsAndDestinations(error.untrustedRecords, error.destinations.toList()) - .show(childFragmentManager) - } else { - callback.onSendError(error) - } - readyToSend = true - }, - { - callback.onSentWithoutResult() - readyToSend = true - } - ) - } - - private fun hideSendProgress() { - progressWrapper.animate().cancel() - progressWrapper.alpha = 0f - progressWrapper.visible = false - } - - override fun sendAnywayAfterSafetyNumberChangedInBottomSheet(destinations: List) { - performSend(destinations) - } - - override fun onMessageResentAfterSafetyNumberChangeInBottomSheet() { - error("Unsupported, we do not hand in a message id.") - } - - override fun onCanceled() = Unit - - private fun presentAddMessageEntry(viewOnceState: MediaSelectionState.ViewOnceToggleState, message: CharSequence?) { - when (viewOnceState) { - MediaSelectionState.ViewOnceToggleState.INFINITE -> { - addMessageButton.gravity = Gravity.CENTER_VERTICAL - addMessageButton.text = SpannableString(message.takeIf { it.isNotNullOrBlank() } ?: getString(R.string.MediaReviewFragment__add_a_message)) - addMessageButton.isClickable = true - } - MediaSelectionState.ViewOnceToggleState.ONCE -> { - addMessageButton.gravity = Gravity.CENTER - addMessageButton.setText(R.string.MediaReviewFragment__view_once_message) - addMessageButton.isClickable = false - } - } - } - - private fun presentImageQualityToggle(state: MediaSelectionState) { - qualityButton.updateLayoutParams { - if (MediaUtil.isImageAndNotGif(state.focusedMedia?.contentType ?: "")) { - startToStart = ConstraintLayout.LayoutParams.UNSET - startToEnd = cropAndRotateButton.id - } else { - startToStart = ConstraintLayout.LayoutParams.PARENT_ID - startToEnd = ConstraintLayout.LayoutParams.UNSET - } - } - qualityButton.setImageResource( - when (state.quality) { - SentMediaQuality.STANDARD -> CoreUiR.drawable.symbol_quality_high_slash_24 - SentMediaQuality.HIGH -> CoreUiR.drawable.symbol_quality_high_24 - } - ) - } - - private fun presentMuteVideoAudioToggle(state: MediaSelectionState) { - val muted = state.focusedMedia?.uri?.let { state.getOrCreateVideoTrimData(it).isMuted } == true - muteVideoAudioButton.setImageResource( - if (muted) CoreUiR.drawable.symbol_speaker_slash_24 else CoreUiR.drawable.symbol_speaker_24 - ) - } - - private fun presentSendButton(enabled: Boolean, sendType: MessageSendType, recipient: Recipient?) { - val sendButtonBackgroundTint = when { - !enabled -> ContextCompat.getColor(requireContext(), R.color.core_grey_50) - recipient != null -> recipient.chatColors.asSingleColor() - sendType.usesSignalTransport -> ContextCompat.getColor(requireContext(), CoreUiR.color.signal_colorOnSecondaryContainer) - else -> ContextCompat.getColor(requireContext(), R.color.core_grey_50) - } - - val sendButtonForegroundDrawable = when { - recipient != null -> ContextCompat.getDrawable(requireContext(), R.drawable.symbol_send_fill_24) - else -> ContextCompat.getDrawable(requireContext(), R.drawable.symbol_arrow_end_24) - } - - val sendButtonForegroundTint = when { - !enabled -> ContextCompat.getColor(requireContext(), CoreUiR.color.signal_colorSecondaryContainer) - recipient != null -> ContextCompat.getColor(requireContext(), CoreUiR.color.signal_colorOnCustom) - else -> ContextCompat.getColor(requireContext(), CoreUiR.color.signal_colorSecondaryContainer) - } - - sendButton.setImageDrawable(sendButtonForegroundDrawable) - sendButton.setColorFilter(sendButtonForegroundTint) - ViewCompat.setBackgroundTintList(sendButton, ColorStateList.valueOf(sendButtonBackgroundTint)) - } - - private fun presentPager(state: MediaSelectionState) { - pager.isUserInputEnabled = state.isTouchEnabled - - val indexOfSelectedItem = state.selectedMedia.indexOf(state.focusedMedia) - - if (pager.currentItem == indexOfSelectedItem) { - return - } - - if (indexOfSelectedItem != -1) { - pager.setCurrentItem(indexOfSelectedItem, false) - } else { - pager.setCurrentItem(0, false) - } - } - - private fun presentVideoTimeline(state: MediaSelectionState) { - val mediaItem = state.focusedMedia ?: return - if (!MediaUtil.isVideoType(mediaItem.contentType) || !MediaConstraints.isVideoTranscodeAvailable()) { - return - } - val uri = mediaItem.uri - val updatedInputInTimeline = videoTimeLine.setInput(uri, DecryptableUriMediaInput) - if (updatedInputInTimeline) { - videoTimeLine.unregisterDragListener() - } - val size: Long = tryGetUriSize(requireContext(), uri, Long.MAX_VALUE) - val maxSend = sharedViewModel.getMediaConstraints().editorVideoMaxSize - if (size > maxSend) { - videoTimeLine.setTimeLimit(TranscodingConfig.calculateMaxVideoUploadDurationInSeconds(state.transcodingConfigs, state.getOrCreateVideoTrimData(uri).totalInputDurationUs.microseconds), TimeUnit.SECONDS) - } - - if (state.isTouchEnabled) { - val data = state.getOrCreateVideoTrimData(uri) - - if (data.totalInputDurationUs > 0) { - videoTimeLine.setRange(data.startTimeUs, data.endTimeUs) - } - } - } - - private fun presentVideoSizeHint(state: MediaSelectionState) { - val focusedMedia = state.focusedMedia ?: return - val trimData = state.getOrCreateVideoTrimData(focusedMedia.uri) - - videoSizeHint.text = if (state.isVideoTrimmingVisible) { - val seconds = trimData.getDuration().inWholeSeconds - val bytes = TranscodingQuality.createFromQualityTiers(state.transcodingConfigs, trimData.getDuration().inWholeMilliseconds).byteCountEstimate - String.format(Locale.getDefault(), "%d:%02d • %s", seconds / 60, seconds % 60, bytes.bytes.toUnitString()) - } else { - null - } - } - - private fun computeViewStateAndAnimate(state: MediaSelectionState) { - this.animatorSet?.cancel() - - val animators = mutableListOf() - - animators.addAll(computeAddMessageAnimators(state)) - animators.addAll(computeEmojiButtonAnimators(state)) - animators.addAll(computeViewOnceButtonAnimators(state)) - animators.addAll(computeAddMediaButtonsAnimators(state)) - animators.addAll(computeSendButtonAnimators(state)) - animators.addAll(computeSaveButtonAnimators(state)) - animators.addAll(computeQualityButtonAnimators(state)) - animators.addAll(computeMuteVideoAudioButtonAnimators(state)) - animators.addAll(computeCropAndRotateButtonAnimators(state)) - animators.addAll(computeDrawToolButtonAnimators(state)) - animators.addAll(computeRecipientDisplayAnimators(state)) - animators.addAll(computeControlsShadeAnimators(state)) - animators.addAll(computeVideoTimelineAnimator(state)) - - val animatorSet = AnimatorSet() - animatorSet.playTogether(animators) - animatorSet.start() - - this.animatorSet = animatorSet - } - - private fun computeControlsShadeAnimators(state: MediaSelectionState): List { - val animators = mutableListOf() - animators += if (state.isTouchEnabled) { - MediaReviewAnimatorController.getFadeInAnimator(controlsShade) - } else { - MediaReviewAnimatorController.getFadeOutAnimator(controlsShade) - } - - animators += if (state.isVideoTrimmingVisible) { - MediaReviewAnimatorController.getHeightAnimator(videoTimelinePlaceholder, videoTimelinePlaceholder.height, resources.getDimension(R.dimen.video_timeline_height_expanded).roundToInt()) - } else { - MediaReviewAnimatorController.getHeightAnimator(videoTimelinePlaceholder, videoTimelinePlaceholder.height, resources.getDimension(R.dimen.video_timeline_height_collapsed).roundToInt()) - } - - return animators - } - - private fun computeVideoTimelineAnimator(state: MediaSelectionState): List { - val animators = mutableListOf() - - if (state.isVideoTrimmingVisible) { - animators += MediaReviewAnimatorController.getFadeInAnimator(videoTimeLine).apply { - startDelay = 100 - duration = 500 - } - } else { - animators += MediaReviewAnimatorController.getFadeOutAnimator(videoTimeLine).apply { - duration = 400 - } - } - - animators += if (state.isVideoTrimmingVisible && state.isTouchEnabled) { - MediaReviewAnimatorController.getFadeInAnimator(videoSizeHint).apply { - startDelay = 100 - duration = 500 - } - } else { - MediaReviewAnimatorController.getFadeOutAnimator(videoSizeHint).apply { - duration = 400 - } - } - - return animators - } - - private fun computeAddMessageAnimators(state: MediaSelectionState): List { - return if (!state.isTouchEnabled) { - listOf( - MediaReviewAnimatorController.getFadeOutAnimator(addMessageButton) - ) - } else { - listOf( - MediaReviewAnimatorController.getFadeInAnimator(addMessageButton) - ) - } - } - - private fun computeViewOnceButtonAnimators(state: MediaSelectionState): List { - return if (state.isTouchEnabled && state.selectedMedia.size == 1 && !state.isStory && !MediaUtil.isDocumentType(state.focusedMedia?.contentType)) { - listOf(MediaReviewAnimatorController.getFadeInAnimator(viewOnceButton)) - } else { - listOf(MediaReviewAnimatorController.getFadeOutAnimator(viewOnceButton)) - } - } - - private fun computeEmojiButtonAnimators(state: MediaSelectionState): List { - return if (state.isTouchEnabled && state.viewOnceToggleState != MediaSelectionState.ViewOnceToggleState.ONCE) { - listOf(MediaReviewAnimatorController.getFadeInAnimator(emojiButton)) - } else { - listOf(MediaReviewAnimatorController.getFadeOutAnimator(emojiButton)) - } - } - - private fun computeAddMediaButtonsAnimators(state: MediaSelectionState): List { - return when { - !state.isTouchEnabled || state.viewOnceToggleState == MediaSelectionState.ViewOnceToggleState.ONCE || MediaUtil.isDocumentType(state.focusedMedia?.contentType) -> { - listOf( - MediaReviewAnimatorController.getFadeOutAnimator(addMediaButton), - MediaReviewAnimatorController.getFadeOutAnimator(selectionRecycler) - ) - } - state.selectedMedia.size > 1 -> { - listOf( - MediaReviewAnimatorController.getFadeOutAnimator(addMediaButton), - MediaReviewAnimatorController.getFadeInAnimator(selectionRecycler) - ) - } - else -> { - listOf( - MediaReviewAnimatorController.getFadeInAnimator(addMediaButton), - MediaReviewAnimatorController.getFadeOutAnimator(selectionRecycler) - ) - } - } - } - - private fun computeSendButtonAnimators(state: MediaSelectionState): List { - return if (state.isTouchEnabled) { - listOf( - MediaReviewAnimatorController.getFadeInAnimator(sendButton, isEnabled = state.canSend) - ) - } else { - listOf( - MediaReviewAnimatorController.getFadeOutAnimator(sendButton, isEnabled = state.canSend) - ) - } - } - - private fun computeSaveButtonAnimators(state: MediaSelectionState): List { - return if (state.isTouchEnabled && !MediaUtil.isVideo(state.focusedMedia?.contentType) && !MediaUtil.isDocumentType(state.focusedMedia?.contentType)) { - listOf( - MediaReviewAnimatorController.getFadeInAnimator(saveButton) - ) - } else { - listOf( - MediaReviewAnimatorController.getFadeOutAnimator(saveButton) - ) - } - } - - private fun computeQualityButtonAnimators(state: MediaSelectionState): List { - return if (state.isTouchEnabled && !state.isStory && !MediaUtil.isDocumentType(state.focusedMedia?.contentType)) { - listOf(MediaReviewAnimatorController.getFadeInAnimator(qualityButton)) - } else { - listOf(MediaReviewAnimatorController.getFadeOutAnimator(qualityButton)) - } - } - - private fun computeMuteVideoAudioButtonAnimators(state: MediaSelectionState): List { - val focusedMedia = state.focusedMedia - return if (state.isTouchEnabled && MediaConstraints.isMuteVideoAudioAvailable() && focusedMedia != null && MediaUtil.isNonGifVideo(focusedMedia)) { - listOf(MediaReviewAnimatorController.getFadeInAnimator(muteVideoAudioButton)) - } else { - listOf(MediaReviewAnimatorController.getFadeOutAnimator(muteVideoAudioButton)) - } - } - - private fun computeCropAndRotateButtonAnimators(state: MediaSelectionState): List { - return if (state.isTouchEnabled && MediaUtil.isImageAndNotGif(state.focusedMedia?.contentType ?: "")) { - listOf(MediaReviewAnimatorController.getFadeInAnimator(cropAndRotateButton)) - } else { - listOf(MediaReviewAnimatorController.getFadeOutAnimator(cropAndRotateButton)) - } - } - - private fun computeDrawToolButtonAnimators(state: MediaSelectionState): List { - return if (state.isTouchEnabled && MediaUtil.isImageAndNotGif(state.focusedMedia?.contentType ?: "")) { - listOf(MediaReviewAnimatorController.getFadeInAnimator(drawToolButton)) - } else { - listOf(MediaReviewAnimatorController.getFadeOutAnimator(drawToolButton)) - } - } - - private fun computeRecipientDisplayAnimators(state: MediaSelectionState): List { - return if (state.isTouchEnabled && state.recipient != null) { - recipientDisplay.text = if (state.recipient.isSelf) requireContext().getString(R.string.note_to_self) else state.recipient.getDisplayName(requireContext()) - listOf(MediaReviewAnimatorController.getFadeInAnimator(recipientDisplay)) - } else { - listOf(MediaReviewAnimatorController.getFadeOutAnimator(recipientDisplay)) - } - } - - companion object { - private val TAG = Log.tag(MediaReviewFragment::class.java) - - @JvmStatic - private fun tryGetUriSize(context: Context, uri: Uri, defaultValue: Long): Long { - return try { - var size: Long = 0 - context.contentResolver.query(uri, null, null, null, null).use { cursor -> - if (cursor != null && cursor.moveToFirst() && cursor.getColumnIndex(OpenableColumns.SIZE) >= 0) { - size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE)) - } - } - if (size <= 0) { - size = MediaUtil.getMediaSize(context, uri) - } - size - } catch (e: IOException) { - Log.w(TAG, e) - defaultValue - } - } - } - - interface Callback { - fun onSentWithResult(mediaSendActivityResult: MediaSendActivityResult) - fun onSentWithoutResult() - fun onSendError(error: Throwable) - fun onNoMediaSelected() - fun onPopFromReview() - } - - override fun onScheduleSend(scheduledTime: Long) { - startScheduledSend(scheduledTime) - } - - override fun onSchedulePermissionsGranted(metricId: String?, scheduledDate: Long) { - scheduledSendTime = scheduledDate - sendButton.performClick() - } - - private fun startScheduledSend(scheduledTime: Long) { - if (ReenableScheduledMessagesDialogFragment.showIfNeeded(requireContext(), childFragmentManager, null, scheduledTime)) { - return - } - scheduledSendTime = scheduledTime - sendButton.performClick() - } - - override fun onRangeDrag(minValue: Long, maxValue: Long, duration: Long, end: Boolean) { - sharedViewModel.onEditVideoDuration(totalDurationUs = duration, startTimeUs = minValue, endTimeUs = maxValue, touchEnabled = end) - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewFragmentPagerAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewFragmentPagerAdapter.kt deleted file mode 100644 index 8e7898ac40..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewFragmentPagerAdapter.kt +++ /dev/null @@ -1,73 +0,0 @@ -package org.thoughtcrime.securesms.mediasend.v2.review - -import androidx.fragment.app.Fragment -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView -import androidx.viewpager2.adapter.FragmentStateAdapter -import org.signal.core.models.media.Media -import org.thoughtcrime.securesms.mediasend.v2.documents.MediaReviewDocumentPageFragment -import org.thoughtcrime.securesms.mediasend.v2.gif.MediaReviewGifPageFragment -import org.thoughtcrime.securesms.mediasend.v2.images.MediaReviewImagePageFragment -import org.thoughtcrime.securesms.mediasend.v2.videos.MediaReviewVideoPageFragment -import org.thoughtcrime.securesms.util.MediaUtil -import java.util.LinkedList - -class MediaReviewFragmentPagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) { - - private val mediaList: MutableList = mutableListOf() - - fun submitMedia(media: List) { - val oldMedia: List = LinkedList(mediaList) - mediaList.clear() - mediaList.addAll(media) - - DiffUtil - .calculateDiff(Callback(oldMedia, mediaList)) - .dispatchUpdatesTo(this) - } - - override fun getItemId(position: Int): Long { - if (position > mediaList.size || position < 0) { - return RecyclerView.NO_ID - } - - return mediaList[position].uri.hashCode().toLong() - } - - override fun containsItem(itemId: Long): Boolean { - return mediaList.any { it.uri.hashCode().toLong() == itemId } - } - - override fun getItemCount(): Int = mediaList.size - - override fun createFragment(position: Int): Fragment { - val mediaItem: Media = mediaList[position] - - return when { - MediaUtil.isGif(mediaItem.contentType) -> MediaReviewGifPageFragment.newInstance(mediaItem.uri) - MediaUtil.isImageType(mediaItem.contentType) -> MediaReviewImagePageFragment.newInstance(mediaItem.uri) - MediaUtil.isVideoType(mediaItem.contentType) -> MediaReviewVideoPageFragment.newInstance(mediaItem.uri, mediaItem.isVideoGif) - MediaUtil.isDocumentType(mediaItem.contentType) -> MediaReviewDocumentPageFragment.newInstance(mediaItem) - else -> { - throw UnsupportedOperationException("Can only render images and videos. Found mimetype: '" + mediaItem.contentType + "'") - } - } - } - - private class Callback( - private val oldList: List, - private val newList: List - ) : DiffUtil.Callback() { - override fun getOldListSize(): Int = oldList.size - - override fun getNewListSize(): Int = newList.size - - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - return oldList[oldItemPosition].uri == newList[newItemPosition].uri - } - - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - return oldList[oldItemPosition] == newList[newItemPosition] - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewSelectedItem.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewSelectedItem.kt deleted file mode 100644 index bf61e38195..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewSelectedItem.kt +++ /dev/null @@ -1,57 +0,0 @@ -package org.thoughtcrime.securesms.mediasend.v2.review - -import android.view.View -import android.widget.ImageView -import com.bumptech.glide.Glide -import org.signal.core.models.media.Media -import org.signal.glide.decryptableuri.DecryptableUri -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.util.MediaUtil -import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory -import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter -import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel -import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder -import org.thoughtcrime.securesms.util.visible - -typealias OnSelectedMediaClicked = (Media, Boolean) -> Unit - -object MediaReviewSelectedItem { - fun register(mappingAdapter: MappingAdapter, onSelectedMediaClicked: OnSelectedMediaClicked) { - mappingAdapter.registerFactory(Model::class.java, LayoutFactory({ ViewHolder(it, onSelectedMediaClicked) }, R.layout.v2_media_review_selected_item)) - } - - class Model(val media: Media, val isSelected: Boolean, val videoTrimStartTimeUs: Long = 0) : MappingModel { - override fun areItemsTheSame(newItem: Model): Boolean { - return media == newItem.media - } - - override fun areContentsTheSame(newItem: Model): Boolean { - return media == newItem.media && isSelected == newItem.isSelected && videoTrimStartTimeUs == newItem.videoTrimStartTimeUs - } - } - - class ViewHolder(itemView: View, private val onSelectedMediaClicked: OnSelectedMediaClicked) : MappingViewHolder(itemView) { - - private val imageView: ImageView = itemView.findViewById(R.id.media_review_selected_image) - private val playOverlay: ImageView = itemView.findViewById(R.id.media_review_play_overlay) - private val trashOverlay: ImageView = itemView.findViewById(R.id.media_review_trash_overlay) - - override fun bind(model: Model) { - Glide.with(imageView) - .load(DecryptableUri(model.media.uri, model.videoTrimStartTimeUs)) - .centerCrop() - .into(imageView) - - playOverlay.visible = MediaUtil.isNonGifVideo(model.media) && !model.isSelected - trashOverlay.visible = model.isSelected - - itemView.contentDescription = if (model.isSelected) { - context.getString(R.string.MediaReviewSelectedItem__tap_to_remove) - } else { - context.getString(R.string.MediaReviewSelectedItem__tap_to_select) - } - - itemView.setOnClickListener { onSelectedMediaClicked(model.media, model.isSelected) } - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewToastPopupWindow.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewToastPopupWindow.kt deleted file mode 100644 index 9c874d0059..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewToastPopupWindow.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2024 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.mediasend.v2.review - -import android.view.Gravity -import android.view.LayoutInflater -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.PopupWindow -import android.widget.TextView -import org.thoughtcrime.securesms.R -import kotlin.time.Duration.Companion.seconds - -/** - * Toast-style notification used in the media review flow. This exists so we can specify the location and animation of how it appears. - */ -class MediaReviewToastPopupWindow private constructor(parent: ViewGroup, iconResource: Int, descriptionText: String) : PopupWindow( - LayoutInflater.from(parent.context).inflate(R.layout.v2_media_review_quality_popup_window, parent, false), - ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT -) { - - private val icon: ImageView = contentView.findViewById(R.id.media_review_toast_popup_icon) - private val description: TextView = contentView.findViewById(R.id.media_review_toast_popup_description) - - init { - animationStyle = R.style.StickerPopupAnimation - icon.setImageResource(iconResource) - description.text = descriptionText - } - - private fun show(parent: ViewGroup) { - showAtLocation(parent, Gravity.CENTER, 0, 0) - contentView.postDelayed({ dismiss() }, DURATION) - } - - companion object { - private val DURATION = 3.seconds.inWholeMilliseconds - - @JvmStatic - fun show(parent: ViewGroup, icon: Int, description: String): MediaReviewToastPopupWindow { - val qualityToast = MediaReviewToastPopupWindow(parent, icon, description) - qualityToast.show(parent) - return qualityToast - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaSelectionItemTouchHelper.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaSelectionItemTouchHelper.java deleted file mode 100644 index 6839a31727..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaSelectionItemTouchHelper.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.thoughtcrime.securesms.mediasend.v2.review; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.ItemTouchHelper; -import androidx.recyclerview.widget.RecyclerView; - -import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionViewModel; - -/** - * A touch helper for handling drag + drop on the media rail in the media send flow. - */ -public class MediaSelectionItemTouchHelper extends ItemTouchHelper.Callback { - - private final MediaSelectionViewModel viewModel; - - public MediaSelectionItemTouchHelper(MediaSelectionViewModel viewModel) { - this.viewModel = viewModel; - } - - @Override - public boolean isLongPressDragEnabled() { - return true; - } - - @Override - public boolean isItemViewSwipeEnabled() { - return false; - } - - @Override - public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { - if (viewModel.isValidMediaDragPosition(viewHolder.getAdapterPosition())) { - int dragFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT; - return makeMovementFlags(dragFlags, 0); - } else { - return 0; - } - } - - @Override - public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) { - return viewModel.swapMedia(viewHolder.getAdapterPosition(), target.getAdapterPosition()); - } - - @Override - public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { - super.clearView(recyclerView, viewHolder); - viewModel.onMediaDragFinished(); - } - - @Override - public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/QualitySelectorBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/QualitySelectorBottomSheet.kt deleted file mode 100644 index b701629df2..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/QualitySelectorBottomSheet.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2024 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.mediasend.v2.review - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.fragment.app.viewModels -import org.signal.core.ui.compose.BottomSheets -import org.signal.core.ui.compose.ComposeBottomSheetDialogFragment -import org.signal.mediasend.screens.edit.QualitySelectorSheetContent -import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionViewModel - -/** - * Bottom sheet dialog to select the media quality (Standard vs. High) when sending media. - */ -class QualitySelectorBottomSheet : ComposeBottomSheetDialogFragment() { - private val sharedViewModel: MediaSelectionViewModel by viewModels(ownerProducer = { requireActivity() }) - - override val forceDarkTheme = true - - @Composable - override fun SheetContent() { - val state by sharedViewModel.state.observeAsState() - val quality = state?.quality - if (quality != null) { - Column { - Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxWidth()) { - BottomSheets.Handle(modifier = Modifier.padding(top = 6.dp)) - } - - QualitySelectorSheetContent(quality = quality, onQualitySelected = { - sharedViewModel.setSentMediaQuality(it) - dismiss() - }) - } - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationFragment.kt index 8f9eed4584..47709c6302 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationFragment.kt @@ -44,7 +44,6 @@ import org.thoughtcrime.securesms.databinding.StoriesTextPostCreationFragmentBin import org.thoughtcrime.securesms.linkpreview.LinkPreview import org.thoughtcrime.securesms.linkpreview.LinkPreviewState import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModelV2 -import org.thoughtcrime.securesms.mediasend.v2.review.MediaReviewFragment import org.thoughtcrime.securesms.mediasend.v2.stories.StoriesMultiselectForwardActivity import org.thoughtcrime.securesms.mediasend.v2.text.send.TextStoryPostSendResult import org.thoughtcrime.securesms.recipients.Recipient diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/videos/MediaReviewVideoPageFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/videos/MediaReviewVideoPageFragment.kt deleted file mode 100644 index 48e11e6787..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/videos/MediaReviewVideoPageFragment.kt +++ /dev/null @@ -1,113 +0,0 @@ -package org.thoughtcrime.securesms.mediasend.v2.videos - -import android.net.Uri -import android.os.Bundle -import android.view.View -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import kotlinx.coroutines.launch -import org.signal.core.util.getParcelableCompat -import org.signal.mediasend.screens.edit.video.VideoEditorFragment -import org.signal.mediasend.screens.edit.video.VideoEditorViewModel -import org.signal.mediasend.screens.edit.video.VideoThumbnailsRangeSelectorView -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.mediasend.v2.HudCommand -import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionViewModel - -private const val VIDEO_EDITOR_TAG = "video.editor.fragment" - -/** - * Page fragment which displays a single editable video (non-gif) to the user. Has an embedded MediaSendVideoFragment - * and adds some extra support for saving and restoring state, as well as saving a video to disk. - */ -class MediaReviewVideoPageFragment : Fragment(R.layout.fragment_container) { - - private val sharedViewModel: MediaSelectionViewModel by viewModels(ownerProducer = { requireActivity() }) - private val videoEditorViewModel: VideoEditorViewModel by viewModels(ownerProducer = { requireActivity() }) - - private lateinit var videoEditorFragment: VideoEditorFragment - private lateinit var videoTimeLine: VideoThumbnailsRangeSelectorView - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - videoTimeLine = requireActivity().findViewById(R.id.video_timeline) - - videoEditorFragment = ensureVideoEditorFragment() - - videoTimeLine.registerPlayerDragListener(object : VideoThumbnailsRangeSelectorView.PositionDragListener { - override fun onPositionDrag(position: Long) { - focusedUri()?.let { videoEditorViewModel.sendCommand(it, VideoEditorViewModel.Command.PositionDrag(position)) } - } - - override fun onEndPositionDrag(position: Long) { - focusedUri()?.let { videoEditorViewModel.sendCommand(it, VideoEditorViewModel.Command.EndPositionDrag(position)) } - } - }) - - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - videoEditorViewModel.events(requireUri()).collect { event -> - when (event) { - VideoEditorViewModel.Event.PlayerReady, VideoEditorViewModel.Event.PlayerError -> sharedViewModel.sendCommand(HudCommand.ResumeEntryTransition) - is VideoEditorViewModel.Event.TouchEventsNeeded -> sharedViewModel.setTouchEnabled(!event.needed) - is VideoEditorViewModel.Event.ActualPositionChanged -> videoTimeLine.setActualPosition(event.positionUs) - } - } - } - } - - sharedViewModel.state.observe(viewLifecycleOwner) { incomingState -> - videoEditorFragment.onStateUpdate( - incomingState.focusedMedia?.uri, - incomingState.isTouchEnabled, - incomingState::getOrCreateVideoTrimData - ) - } - } - - private fun focusedUri(): Uri? = sharedViewModel.state.value?.focusedMedia?.uri - - private fun ensureVideoEditorFragment(): VideoEditorFragment { - val fragmentInManager: VideoEditorFragment? = childFragmentManager.findFragmentByTag(VIDEO_EDITOR_TAG) as? VideoEditorFragment - - return if (fragmentInManager != null) { - fragmentInManager - } else { - val videoEditorFragment = VideoEditorFragment.newInstance( - requireUri(), - requireMaxAttachmentSize(), - requireIsVideoGif() - ) - - childFragmentManager.beginTransaction() - .replace( - R.id.fragment_container, - videoEditorFragment, - VIDEO_EDITOR_TAG - ) - .commitAllowingStateLoss() - - videoEditorFragment - } - } - - private fun requireUri(): Uri = requireNotNull(requireArguments().getParcelableCompat(ARG_URI, Uri::class.java)) - private fun requireMaxAttachmentSize(): Long = sharedViewModel.getMediaConstraints().getVideoMaxSize() - private fun requireIsVideoGif(): Boolean = requireNotNull(requireArguments().getBoolean(ARG_IS_VIDEO_GIF)) - - companion object { - private const val ARG_URI = "arg.uri" - private const val ARG_IS_VIDEO_GIF = "arg.is.video.gif" - - fun newInstance(uri: Uri, isVideoGif: Boolean): Fragment { - return MediaReviewVideoPageFragment().apply { - arguments = Bundle().apply { - putParcelable(ARG_URI, uri) - putBoolean(ARG_IS_VIDEO_GIF, isVideoGif) - } - } - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java b/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java index 95fd1861f3..30ff9b6026 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java @@ -265,7 +265,7 @@ public class AttachmentManager { .request(PermissionCompat.forImagesAndVideos()) .ifNecessary() .withPermanentDenialDialog(fragment.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio)) - .onAllGranted(() -> fragment.startActivityForResult(MediaSendLauncher.gallery(fragment.requireContext(), messageSendType, Collections.emptyList(), recipient.getId(), body, hasQuote), requestCode)) + .onAllGranted(() -> fragment.startActivityForResult(MediaSendLauncher.gallery(fragment.requireContext(), Collections.emptyList(), recipient.getId(), body, hasQuote), requestCode)) .execute(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java b/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java index 0f410fc18f..da9c108dd7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java @@ -59,7 +59,6 @@ import org.thoughtcrime.securesms.attachments.AttachmentSaver; import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.fonts.FontTypefaceProvider; import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.mediasend.MediaSendPageFragment; import org.thoughtcrime.securesms.mediasend.v2.MediaAnimations; import org.thoughtcrime.securesms.mms.PushMediaConstraints; import org.thoughtcrime.securesms.scribbles.stickers.AnalogClockStickerRenderer; @@ -85,7 +84,6 @@ import kotlin.Pair; import static android.app.Activity.RESULT_OK; public final class ImageEditorFragment extends Fragment implements ImageEditorHudV2.EventListener, - MediaSendPageFragment, TextEntryDialogFragment.Controller { @@ -313,25 +311,21 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), onBackPressedCallback); } - @Override public void setUri(@NonNull Uri uri) { this.imageUri = uri; } @NonNull - @Override public Uri getUri() { return imageUri; } - @Override public Object saveState() { Data data = new Data(); data.writeModel(imageEditorView.getModel()); return data; } - @Override public void restoreState(@NonNull Object state) { if (state instanceof Data) { @@ -351,10 +345,6 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu } } - @Override - public void notifyHidden() { - } - private void changeEntityColor(int selectedColor) { if (currentSelection != null) { Renderer renderer = currentSelection.getRenderer(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/sharing/v2/ShareActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/sharing/v2/ShareActivity.kt index 26d7f7ea21..eeea0913b0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sharing/v2/ShareActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/sharing/v2/ShareActivity.kt @@ -31,7 +31,6 @@ import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.SignalProgressDialog import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey import org.thoughtcrime.securesms.conversation.ConversationIntents -import org.thoughtcrime.securesms.conversation.MessageSendType import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs import org.thoughtcrime.securesms.mediasend.MediaSendLauncher.share @@ -326,7 +325,6 @@ class ShareActivity : PassphraseRequiredActivity(), MultiselectForwardFragment.C val intent = share( this, - MessageSendType.SignalMessageSendType, media, multiShareArgs.recipientSearchKeys.toList(), multiShareArgs.draftText, diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt b/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt index afcb7b553e..4dabb081f7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt @@ -1413,15 +1413,6 @@ object RemoteConfig { hotSwappable = true ) - /** Whether to utilize the new media-send feature module */ - @JvmStatic - @get:JvmName("useNewMediaSendFlow") - val useNewMediaSendFlow: Boolean by remoteBoolean( - key = "android.useNewMediaSendFlow.2", - defaultValue = false, - hotSwappable = true - ) - /** Whether to enable Jetpack telecom integration for 1:1 calls */ @JvmStatic @get:JvmName("useJetPackTelecom") diff --git a/app/src/main/res/anim/camera_slide_to_bottom.xml b/app/src/main/res/anim/camera_slide_to_bottom.xml deleted file mode 100644 index d50cccb14a..0000000000 --- a/app/src/main/res/anim/camera_slide_to_bottom.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/media_gallery_button_background.xml b/app/src/main/res/drawable-v21/media_gallery_button_background.xml deleted file mode 100644 index 472e952f18..0000000000 --- a/app/src/main/res/drawable-v21/media_gallery_button_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/camerax_flash_toggle.xml b/app/src/main/res/drawable/camerax_flash_toggle.xml deleted file mode 100644 index 317c2e154d..0000000000 --- a/app/src/main/res/drawable/camerax_flash_toggle.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_add_media_22.xml b/app/src/main/res/drawable/ic_add_media_22.xml deleted file mode 100644 index b5c27af88d..0000000000 --- a/app/src/main/res/drawable/ic_add_media_22.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/media_gallery_button_background.xml b/app/src/main/res/drawable/media_gallery_button_background.xml deleted file mode 100644 index 51f3b8bc4f..0000000000 --- a/app/src/main/res/drawable/media_gallery_button_background.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/media_selection_activity.xml b/app/src/main/res/layout/media_selection_activity.xml deleted file mode 100644 index c2fa283b4b..0000000000 --- a/app/src/main/res/layout/media_selection_activity.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/mediasend_document_fragment.xml b/app/src/main/res/layout/mediasend_document_fragment.xml deleted file mode 100644 index 8948323bfd..0000000000 --- a/app/src/main/res/layout/mediasend_document_fragment.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/mediasend_image_fragment.xml b/app/src/main/res/layout/mediasend_image_fragment.xml deleted file mode 100644 index c7be953f5a..0000000000 --- a/app/src/main/res/layout/mediasend_image_fragment.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/v2_media_review_add_media_item.xml b/app/src/main/res/layout/v2_media_review_add_media_item.xml deleted file mode 100644 index a228a694f0..0000000000 --- a/app/src/main/res/layout/v2_media_review_add_media_item.xml +++ /dev/null @@ -1,16 +0,0 @@ - - \ No newline at end of file diff --git a/app/src/main/res/layout/v2_media_review_fragment.xml b/app/src/main/res/layout/v2_media_review_fragment.xml deleted file mode 100644 index 3c9afb7967..0000000000 --- a/app/src/main/res/layout/v2_media_review_fragment.xml +++ /dev/null @@ -1,361 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/v2_media_review_quality_popup_window.xml b/app/src/main/res/layout/v2_media_review_quality_popup_window.xml deleted file mode 100644 index 97a74ce124..0000000000 --- a/app/src/main/res/layout/v2_media_review_quality_popup_window.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/v2_media_review_selected_item.xml b/app/src/main/res/layout/v2_media_review_selected_item.xml deleted file mode 100644 index 6b79bc5d01..0000000000 --- a/app/src/main/res/layout/v2_media_review_selected_item.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/navigation/media.xml b/app/src/main/res/navigation/media.xml deleted file mode 100644 index 332b965237..0000000000 --- a/app/src/main/res/navigation/media.xml +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 7a6a242f44..760f0d067d 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -233,12 +233,6 @@ - - - - - - diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 1239466a2a..e1fd59ecd9 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -226,8 +226,6 @@ 24dp - 44dp - 1dp 40dp 6dp 40dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b5016e77e4..ec3c3f0680 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -432,7 +432,6 @@ Send edit Compose message - Sorry, there was an error setting your attachment. Recipient is not a valid SMS or email address! Message is empty! @@ -1885,9 +1884,6 @@ Open camera - - Camera unavailable. - All media Camera @@ -6345,36 +6341,9 @@ Add to story Add a message - - Add a reply Send to - View once media - One or more items were too large - One or more items were invalid - Too many items selected - Your video was trimmed to fit within the size limit - - Video set to view once - - Photo set to view once - - Add a Media - - Brush and Pen - - Crop and Rotate - - Change Media Quality - - Mute Video Audio - - Save Media - - Toggle emoji keyboard Toggle View Once - - Send Media Finish adding a Message @@ -6394,28 +6363,10 @@ Send - Tap to remove - Tap to select - Discard Discard changes? You\'ll lose any changes you\'ve made to this photo. - - Found %1$s - - Start a chat with \"%1$s\" - - Go to chat - - - Link device? - - It looks like you\'re trying to link a Signal device. Tap continue and then tap \"Link a New Device\" and scan the QR code again. - - Continue - - My badges Featured badge Display badges on profile diff --git a/app/src/test/java/org/thoughtcrime/securesms/camera/OrderEnforcerTest.java b/app/src/test/java/org/thoughtcrime/securesms/camera/OrderEnforcerTest.java deleted file mode 100644 index 730257cb30..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/camera/OrderEnforcerTest.java +++ /dev/null @@ -1,105 +0,0 @@ -package org.thoughtcrime.securesms.camera; - -import org.junit.Test; -import org.thoughtcrime.securesms.mediasend.OrderEnforcer; - -import java.util.concurrent.atomic.AtomicInteger; - -import static org.junit.Assert.assertEquals; - -public class OrderEnforcerTest { - - @Test - public void markCompleted_singleEntry() { - AtomicInteger counter = new AtomicInteger(0); - - OrderEnforcer enforcer = new OrderEnforcer<>(Stage.A, Stage.B, Stage.C, Stage.D); - enforcer.run(Stage.A, new CountRunnable(counter)); - assertEquals(0, counter.get()); - - enforcer.markCompleted(Stage.A); - assertEquals(1, counter.get()); - } - - @Test - public void markCompleted_singleEntry_waterfall() { - AtomicInteger counter = new AtomicInteger(0); - - OrderEnforcer enforcer = new OrderEnforcer<>(Stage.A, Stage.B, Stage.C, Stage.D); - enforcer.run(Stage.C, new CountRunnable(counter)); - assertEquals(0, counter.get()); - - enforcer.markCompleted(Stage.A); - assertEquals(0, counter.get()); - - enforcer.markCompleted(Stage.C); - assertEquals(0, counter.get()); - - enforcer.markCompleted(Stage.B); - assertEquals(1, counter.get()); - } - - @Test - public void markCompleted_multipleEntriesPerStage_waterfall() { - AtomicInteger counter = new AtomicInteger(0); - - OrderEnforcer enforcer = new OrderEnforcer<>(Stage.A, Stage.B, Stage.C, Stage.D); - - enforcer.run(Stage.A, new CountRunnable(counter)); - enforcer.run(Stage.A, new CountRunnable(counter)); - assertEquals(0, counter.get()); - - enforcer.run(Stage.B, new CountRunnable(counter)); - enforcer.run(Stage.B, new CountRunnable(counter)); - assertEquals(0, counter.get()); - - enforcer.run(Stage.C, new CountRunnable(counter)); - enforcer.run(Stage.C, new CountRunnable(counter)); - assertEquals(0, counter.get()); - - enforcer.run(Stage.D, new CountRunnable(counter)); - enforcer.run(Stage.D, new CountRunnable(counter)); - assertEquals(0, counter.get()); - - enforcer.markCompleted(Stage.A); - assertEquals(counter.get(), 2); - - enforcer.markCompleted(Stage.D); - assertEquals(counter.get(), 2); - - enforcer.markCompleted(Stage.B); - assertEquals(counter.get(), 4); - - enforcer.markCompleted(Stage.C); - assertEquals(counter.get(), 8); - } - - @Test - public void run_alreadyCompleted() { - AtomicInteger counter = new AtomicInteger(0); - - OrderEnforcer enforcer = new OrderEnforcer<>(Stage.A, Stage.B, Stage.C, Stage.D); - enforcer.markCompleted(Stage.A); - enforcer.markCompleted(Stage.B); - - enforcer.run(Stage.B, new CountRunnable(counter)); - assertEquals(1, counter.get()); - } - - private static class CountRunnable implements Runnable { - private final AtomicInteger counter; - - public CountRunnable(AtomicInteger counter) { - this.counter = counter; - } - - @Override - public void run() { - counter.incrementAndGet(); - } - } - - private enum Stage { - A, B, C, D - } -}