Add ability to take a photo in media-send feature module.

This commit is contained in:
Alex Hart
2026-06-25 17:32:20 -03:00
committed by Michelle Tang
parent 248a787d74
commit 2c35a6a3fe
24 changed files with 629 additions and 303 deletions
@@ -1171,7 +1171,7 @@ class MainActivity :
} else if (SignalStore.internal.useNewMediaActivity) {
mediaSendLauncher.launch(
MediaSendActivityContract.Args(
isCameraFirst = false,
isCameraFirst = true,
isStory = destination == MainNavigationListLocation.STORIES
)
)
@@ -6,11 +6,14 @@
package org.thoughtcrime.securesms.dependencies
import androidx.media3.exoplayer.ExoPlayer
import org.signal.core.util.contentproviders.BlobProvider
import org.signal.mediasend.MediaSendDependencies
import org.signal.mediasend.MediaSendQrRepository
import org.signal.mediasend.MediaSendRepository
import org.signal.mediasend.preupload.PreUploadRepository
import org.signal.video.exo.ExoPlayerPool
import org.thoughtcrime.securesms.mediasend.v3.MediaSendV3PreUploadRepository
import org.thoughtcrime.securesms.mediasend.v3.MediaSendV3QrRepository
import org.thoughtcrime.securesms.mediasend.v3.MediaSendV3Repository
object MediaSendDependenciesProvider : MediaSendDependencies.Provider {
@@ -18,5 +21,9 @@ object MediaSendDependenciesProvider : MediaSendDependencies.Provider {
override fun providePreUploadRepository(): PreUploadRepository = MediaSendV3PreUploadRepository
override fun provideQrRepository(): MediaSendQrRepository = MediaSendV3QrRepository
override fun provideExoPlayerPool(): ExoPlayerPool<ExoPlayer> = AppDependencies.exoPlayerPool
override fun provideBlobs(): BlobProvider = AppDependencies.blobs
}
@@ -1,24 +1,16 @@
package org.thoughtcrime.securesms.mediasend.v2.capture
import android.annotation.SuppressLint
import android.content.ContentUris
import android.content.Context
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import androidx.annotation.WorkerThread
import org.signal.core.models.media.Media
import org.signal.core.util.CursorUtil
import org.signal.core.util.ContentTypeUtil
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.contentproviders.BlobProvider
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.mediasend.MediaRepository
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.video.videoconverter.utils.VideoConstants
import java.io.FileDescriptor
import java.io.FileInputStream
import java.io.IOException
import java.util.LinkedList
class MediaCaptureRepository(context: Context) {
@@ -30,7 +22,7 @@ class MediaCaptureRepository(context: Context) {
dataSupplier = { data },
getLength = { data.size.toLong() },
createBlobBuilder = { blobProvider, bytes, _ -> blobProvider.forData(bytes) },
mimeType = MediaUtil.IMAGE_JPEG,
mimeType = ContentTypeUtil.IMAGE_JPEG,
width = width,
height = height
)
@@ -96,76 +88,4 @@ class MediaCaptureRepository(context: Context) {
return null
}
}
companion object {
@SuppressLint("VisibleForTests")
@WorkerThread
private fun getMediaInBucket(context: Context, bucketId: String, contentUri: Uri, isImage: Boolean): List<Media> {
val media: MutableList<Media> = LinkedList()
var selection: String? = MediaStore.Images.Media.BUCKET_ID + " = ? AND " + isNotPending()
var selectionArgs: Array<String>? = arrayOf(bucketId)
val sortBy = MediaStore.Images.Media.DATE_MODIFIED + " DESC"
val projection: Array<String> = if (isImage) {
arrayOf(MediaStore.Images.Media._ID, MediaStore.Images.Media.MIME_TYPE, MediaStore.Images.Media.DATE_MODIFIED, MediaStore.Images.Media.ORIENTATION, MediaStore.Images.Media.WIDTH, MediaStore.Images.Media.HEIGHT, MediaStore.Images.Media.SIZE)
} else {
arrayOf(MediaStore.Images.Media._ID, MediaStore.Images.Media.MIME_TYPE, MediaStore.Images.Media.DATE_MODIFIED, MediaStore.Images.Media.WIDTH, MediaStore.Images.Media.HEIGHT, MediaStore.Images.Media.SIZE, MediaStore.Video.Media.DURATION)
}
if (Media.ALL_MEDIA_BUCKET_ID == bucketId) {
selection = isNotPending()
selectionArgs = null
}
context.contentResolver.query(contentUri, projection, selection, selectionArgs, sortBy).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
val rowId = CursorUtil.requireLong(cursor, projection[0])
val uri = ContentUris.withAppendedId(contentUri, rowId)
val mimetype = CursorUtil.requireString(cursor, MediaStore.Images.Media.MIME_TYPE)
val date = CursorUtil.requireLong(cursor, MediaStore.Images.Media.DATE_MODIFIED)
val orientation = if (isImage) CursorUtil.requireInt(cursor, MediaStore.Images.Media.ORIENTATION) else 0
val width = CursorUtil.requireInt(cursor, getWidthColumn(orientation))
val height = CursorUtil.requireInt(cursor, getHeightColumn(orientation))
val size = CursorUtil.requireLong(cursor, MediaStore.Images.Media.SIZE)
val duration = if (!isImage) CursorUtil.requireInt(cursor, MediaStore.Video.Media.DURATION).toLong() else 0.toLong()
media.add(
MediaRepository.fixMimeType(
context,
Media(
uri = uri,
contentType = mimetype,
date = date,
width = width,
height = height,
size = size,
duration = duration,
isBorderless = false,
isVideoGif = false,
bucketId = bucketId,
caption = null,
transformProperties = null,
fileName = null
)
)
)
}
}
return media
}
private fun getWidthColumn(orientation: Int): String {
return if (orientation == 0 || orientation == 180) MediaStore.Images.Media.WIDTH else MediaStore.Images.Media.HEIGHT
}
private fun getHeightColumn(orientation: Int): String {
return if (orientation == 0 || orientation == 180) MediaStore.Images.Media.HEIGHT else MediaStore.Images.Media.WIDTH
}
@Suppress("DEPRECATION")
private fun isNotPending(): String {
return if (Build.VERSION.SDK_INT <= 28) MediaStore.Images.Media.DATA + " NOT NULL" else MediaStore.MediaColumns.IS_PENDING + " != 1"
}
}
}
@@ -63,6 +63,9 @@ class MediaSendV3Activity : PassphraseRequiredActivity() {
}
)
}
// TODO - QR HudCommands (GoToConversation, GoToLinkedDevices, GoToQuickTransfer) not yet wired up.
else -> Unit
}
}
)
@@ -0,0 +1,54 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.mediasend.v3
import kotlinx.coroutines.rx3.await
import org.signal.core.util.logging.Log
import org.signal.mediasend.MediaRecipientId
import org.signal.mediasend.MediaSendQrRepository
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.registration.data.QuickRegistrationRepository
object MediaSendV3QrRepository : MediaSendQrRepository {
private val TAG = Log.tag(MediaSendV3QrRepository::class)
override suspend fun checkQrData(qrData: String): MediaSendQrRepository.QrCheckResult {
return when {
UsernameRepository.isValidLink(qrData) -> handleUsernameLink(qrData)
qrData.startsWith("sgnl://linkdevice") -> handleLinkDevice()
qrData.startsWith("sgnl://rereg") && QuickRegistrationRepository.isValidReRegistrationQr(qrData) -> handleReReg(qrData)
else -> MediaSendQrRepository.QrCheckResult.None
}
}
private suspend fun handleUsernameLink(qrData: String): MediaSendQrRepository.QrCheckResult {
return when (val result = UsernameRepository.fetchUsernameAndAciFromLink(qrData).await()) {
is UsernameRepository.UsernameLinkConversionResult.Success -> {
val username = result.username.toString()
val recipient = Recipient.externalUsername(result.aci, result.username.toString())
MediaSendQrRepository.QrCheckResult.Username(
recipientId = MediaRecipientId(recipient.id.toLong()),
username = username
)
}
else -> {
Log.w(TAG, "Failed to scan QR code")
MediaSendQrRepository.QrCheckResult.None
}
}
}
private fun handleLinkDevice(): MediaSendQrRepository.QrCheckResult {
return MediaSendQrRepository.QrCheckResult.LinkDevice
}
private fun handleReReg(qrData: String): MediaSendQrRepository.QrCheckResult {
return MediaSendQrRepository.QrCheckResult.ReRegistration(qrData)
}
}
@@ -49,6 +49,8 @@ import org.thoughtcrime.securesms.util.RemoteConfig
import java.io.InputStream
import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
/**
@@ -195,6 +197,12 @@ object MediaSendV3Repository : MediaSendRepository {
return !RemoteConfig.cameraXMixedModelBlocklist.asListContains(Build.MODEL)
}
override fun getMediaConstraints(): MediaConstraints {
return PushMediaConstraints(null)
}
override var storyMaxVideoDuration: Duration = Stories.MAX_VIDEO_DURATION_MILLIS.milliseconds
private fun resolveSendType(sendType: Int): MessageSendType {
return when (sendType) {
else -> MessageSendType.SignalMessageSendType