Rip out Camera1.

This commit is contained in:
Alex Hart
2026-06-17 15:28:25 -03:00
committed by Greyson Parrelli
parent c8d2a06676
commit a7b4a5d93d
82 changed files with 337 additions and 1518 deletions
@@ -5,7 +5,6 @@
package org.thoughtcrime.securesms
import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
@@ -15,7 +14,6 @@ import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.widget.Toast
import androidx.activity.SystemBarStyle
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
@@ -162,7 +160,6 @@ import org.thoughtcrime.securesms.main.navigateToDetailLocation
import org.thoughtcrime.securesms.main.rememberDetailNavHostController
import org.thoughtcrime.securesms.main.rememberFocusRequester
import org.thoughtcrime.securesms.main.storiesNavGraphBuilder
import org.thoughtcrime.securesms.mediasend.camerax.CameraXRemoteConfig
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity
import org.thoughtcrime.securesms.mediasend.v3.mediaSendLauncher
import org.thoughtcrime.securesms.megaphone.Megaphone
@@ -197,7 +194,6 @@ import org.thoughtcrime.securesms.window.NavigationType
import org.thoughtcrime.securesms.window.rememberThreePaneScaffoldNavigatorDelegate
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState
import kotlin.time.Duration.Companion.minutes
import org.signal.core.ui.R as CoreUiR
class MainActivity :
PassphraseRequiredActivity(),
@@ -1176,24 +1172,7 @@ class MainActivity :
}
}
if (CameraXRemoteConfig.isSupported()) {
onGranted()
} else {
Permissions.with(this@MainActivity)
.request(Manifest.permission.CAMERA)
.ifNecessary()
.withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_capture_photos_and_video_allow_camera), CoreUiR.drawable.symbol_camera_24)
.withPermanentDenialDialog(
getString(R.string.CameraXFragment_signal_needs_camera_access_capture_photos),
null,
R.string.CameraXFragment_allow_access_camera,
R.string.CameraXFragment_to_capture_photos_videos,
supportFragmentManager
)
.onAllGranted(onGranted)
.onAnyDenied { Toast.makeText(this@MainActivity, R.string.CameraXFragment_signal_needs_camera_access_capture_photos, Toast.LENGTH_LONG).show() }
.execute()
}
onGranted()
}
inner class ToolbarCallback : MainToolbarCallback {
@@ -1,13 +1,11 @@
package org.thoughtcrime.securesms.avatar.picker
import android.Manifest
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.Gravity
import android.view.View
import android.widget.PopupMenu
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.Fragment
@@ -30,12 +28,10 @@ import org.thoughtcrime.securesms.avatar.vector.VectorAvatarCreationFragment
import org.thoughtcrime.securesms.components.ButtonStripItemView
import org.thoughtcrime.securesms.components.recyclerview.GridDividerDecoration
import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity
import org.thoughtcrime.securesms.mediasend.camerax.CameraXRemoteConfig
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.thoughtcrime.securesms.util.visible
import org.signal.core.ui.R as CoreUiR
/**
* Primary Avatar picker fragment, displays current user avatar and a list of recently used avatars and defaults.
@@ -223,22 +219,8 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
@Suppress("DEPRECATION")
private fun openCameraCapture() {
if (CameraXRemoteConfig.isSupported()) {
val intent = AvatarSelectionActivity.getIntentForCameraCapture(requireContext())
startActivityForResult(intent, REQUEST_CODE_SELECT_IMAGE)
} else {
Permissions.with(this)
.request(Manifest.permission.CAMERA)
.ifNecessary()
.onAllGranted {
val intent = AvatarSelectionActivity.getIntentForCameraCapture(requireContext())
startActivityForResult(intent, REQUEST_CODE_SELECT_IMAGE)
}
.withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_capture_photos_allow_camera), CoreUiR.drawable.symbol_camera_24)
.withPermanentDenialDialog(getString(R.string.AvatarSelectionBottomSheetDialogFragment__taking_a_photo_requires_the_camera_permission), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_capture_photos, getParentFragmentManager())
.onAnyDenied { Toast.makeText(requireContext(), R.string.AvatarSelectionBottomSheetDialogFragment__taking_a_photo_requires_the_camera_permission, Toast.LENGTH_SHORT).show() }
.execute()
}
val intent = AvatarSelectionActivity.getIntentForCameraCapture(requireContext())
startActivityForResult(intent, REQUEST_CODE_SELECT_IMAGE)
}
@Suppress("DEPRECATION")
@@ -17,6 +17,7 @@ import org.signal.core.util.UuidUtil
import org.signal.core.util.logging.Log
import org.signal.core.util.toByteArray
import org.signal.libsignal.zkgroup.backups.BackupLevel
import org.signal.mediasend.SentMediaQuality
import org.thoughtcrime.securesms.backup.v2.ExportState
import org.thoughtcrime.securesms.backup.v2.ImportState
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
@@ -471,19 +472,19 @@ object AccountDataArchiveProcessor {
}
}
private fun org.thoughtcrime.securesms.mms.SentMediaQuality.toRemoteSentMediaQuality(): AccountData.SentMediaQuality {
private fun SentMediaQuality.toRemoteSentMediaQuality(): AccountData.SentMediaQuality {
return when (this) {
org.thoughtcrime.securesms.mms.SentMediaQuality.STANDARD -> AccountData.SentMediaQuality.STANDARD
org.thoughtcrime.securesms.mms.SentMediaQuality.HIGH -> AccountData.SentMediaQuality.HIGH
SentMediaQuality.STANDARD -> AccountData.SentMediaQuality.STANDARD
SentMediaQuality.HIGH -> AccountData.SentMediaQuality.HIGH
}
}
private fun AccountData.SentMediaQuality?.toLocalSentMediaQuality(): org.thoughtcrime.securesms.mms.SentMediaQuality {
private fun AccountData.SentMediaQuality?.toLocalSentMediaQuality(): SentMediaQuality {
return when (this) {
AccountData.SentMediaQuality.HIGH -> org.thoughtcrime.securesms.mms.SentMediaQuality.HIGH
AccountData.SentMediaQuality.STANDARD -> org.thoughtcrime.securesms.mms.SentMediaQuality.STANDARD
AccountData.SentMediaQuality.UNKNOWN_QUALITY -> org.thoughtcrime.securesms.mms.SentMediaQuality.STANDARD
null -> org.thoughtcrime.securesms.mms.SentMediaQuality.STANDARD
AccountData.SentMediaQuality.HIGH -> SentMediaQuality.HIGH
AccountData.SentMediaQuality.STANDARD -> SentMediaQuality.STANDARD
AccountData.SentMediaQuality.UNKNOWN_QUALITY -> SentMediaQuality.STANDARD
null -> SentMediaQuality.STANDARD
}
}
@@ -27,7 +27,7 @@ import org.thoughtcrime.securesms.components.subsampling.AttachmentRegionDecoder
import org.signal.glide.decryptableuri.DecryptableUri;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.util.ActionRequestListener;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.signal.core.util.bitmaps.BitmapUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import java.io.IOException;
@@ -25,9 +25,9 @@ import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.SignalIcons
import org.signal.core.ui.compose.Texts
import org.signal.core.util.bytes
import org.signal.mediasend.SentMediaQuality
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
import org.thoughtcrime.securesms.mms.SentMediaQuality
import org.thoughtcrime.securesms.util.AttachmentUtil
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.thoughtcrime.securesms.webrtc.CallDataMode
@@ -1,6 +1,6 @@
package org.thoughtcrime.securesms.components.settings.app.data
import org.thoughtcrime.securesms.mms.SentMediaQuality
import org.signal.mediasend.SentMediaQuality
import org.thoughtcrime.securesms.webrtc.CallDataMode
data class DataAndStorageSettingsState(
@@ -6,11 +6,11 @@ import androidx.lifecycle.ViewModelProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import org.signal.mediasend.SentMediaQuality
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SettingsValues.ForceWebsocketMode
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.messages.IncomingMessageObserver
import org.thoughtcrime.securesms.mms.SentMediaQuality
import org.thoughtcrime.securesms.util.PlayServicesUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.webrtc.CallDataMode
@@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.components.settings.conversation
import android.Manifest
import android.app.ActivityOptions
import android.content.ActivityNotFoundException
import android.content.Context
@@ -96,7 +95,6 @@ import org.thoughtcrime.securesms.main.MainNavigationChatDetailRouter
import org.thoughtcrime.securesms.main.MainNavigationDetailLocation
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
import org.thoughtcrime.securesms.mediasend.camerax.CameraXRemoteConfig
import org.thoughtcrime.securesms.messagerequests.MessageRequestRepository
import org.thoughtcrime.securesms.nicknames.NicknameActivity
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
@@ -481,23 +479,8 @@ class ConversationSettingsFragment :
.setMessage(R.string.ConversationSettingsFragment__only_admins_of_this_group_can_add_to_its_story)
.setPositiveButton(android.R.string.ok) { d, _ -> d.dismiss() }
.show()
} else if (CameraXRemoteConfig.isSupported()) {
addToGroupStoryDelegate.addToStory(state.recipient.id)
} else {
Permissions.with(this@ConversationSettingsFragment)
.request(Manifest.permission.CAMERA)
.ifNecessary()
.withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_capture_photos_and_video_allow_camera), CoreUiR.drawable.symbol_camera_24)
.withPermanentDenialDialog(
getString(R.string.CameraXFragment_signal_needs_camera_access_capture_photos),
null,
R.string.CameraXFragment_allow_access_camera,
R.string.CameraXFragment_to_capture_photos_videos,
getParentFragmentManager()
)
.onAllGranted { addToGroupStoryDelegate.addToStory(state.recipient.id) }
.onAnyDenied { Toast.makeText(requireContext(), R.string.CameraXFragment_signal_needs_camera_access_capture_photos, Toast.LENGTH_LONG).show() }
.execute()
addToGroupStoryDelegate.addToStory(state.recipient.id)
}
},
onVideoClick = {
@@ -18,6 +18,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.signal.core.ui.compose.ComposeFragment
import org.signal.core.util.Util
import org.signal.core.util.bitmaps.BitmapUtil
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.isAbsent
import org.signal.core.util.logging.Log
@@ -42,7 +43,6 @@ import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.BitmapUtil
import org.thoughtcrime.securesms.util.MediaUtil
import java.util.Objects
import kotlin.random.Random
@@ -28,7 +28,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.signal.core.util.bitmaps.BitmapDecodingException;
import org.thoughtcrime.securesms.util.ImageCompressionUtil;
import org.thoughtcrime.securesms.util.SignalE164Util;
import org.thoughtcrime.securesms.util.SpanUtil;
@@ -25,14 +25,11 @@ import org.thoughtcrime.securesms.contactshare.Contact
import org.thoughtcrime.securesms.contactshare.ContactShareEditActivity
import org.thoughtcrime.securesms.conversation.MessageSendType
import org.thoughtcrime.securesms.conversation.colors.ChatColors
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityResultContracts.Callbacks
import org.thoughtcrime.securesms.giph.ui.GiphyActivity
import org.thoughtcrime.securesms.maps.PlacePickerActivity
import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult
import org.thoughtcrime.securesms.mediasend.camerax.CameraXRemoteConfig
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity
import org.thoughtcrime.securesms.recipients.RecipientId
import org.signal.core.ui.R as CoreUiR
/**
* This encapsulates the logic for interacting with other activities used throughout a conversation. The gist
@@ -76,22 +73,8 @@ class ConversationActivityResultContracts(private val fragment: Fragment, privat
}
fun launchCamera(recipientId: RecipientId, isReply: Boolean) {
if (CameraXRemoteConfig.isSupported()) {
cameraLauncher.launch(MediaSelectionInput(emptyList(), recipientId, null, isReply))
fragment.requireActivity().overridePendingTransition(R.anim.camera_slide_from_bottom, R.anim.stationary)
} else {
Permissions.with(fragment)
.request(Manifest.permission.CAMERA)
.ifNecessary()
.withRationaleDialog(fragment.getString(R.string.CameraXFragment_allow_access_camera), fragment.getString(R.string.CameraXFragment_to_capture_photos_and_video_allow_camera), CoreUiR.drawable.symbol_camera_24)
.withPermanentDenialDialog(fragment.getString(R.string.CameraXFragment_signal_needs_camera_access_capture_photos), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_capture_photos_videos, fragment.parentFragmentManager)
.onAllGranted {
cameraLauncher.launch(MediaSelectionInput(emptyList(), recipientId, null, isReply))
fragment.requireActivity().overridePendingTransition(R.anim.camera_slide_from_bottom, R.anim.stationary)
}
.onAnyDenied { Toast.makeText(fragment.requireContext(), R.string.CameraXFragment_signal_needs_camera_access_capture_photos, Toast.LENGTH_LONG).show() }
.execute()
}
cameraLauncher.launch(MediaSelectionInput(emptyList(), recipientId, null, isReply))
fragment.requireActivity().overridePendingTransition(R.anim.camera_slide_from_bottom, R.anim.stationary)
}
fun launchMediaEditor(mediaList: List<Media>, recipientId: RecipientId, text: CharSequence?) {
@@ -310,7 +310,7 @@ import org.thoughtcrime.securesms.mms.AudioSlide
import org.thoughtcrime.securesms.mms.DocumentSlide
import org.thoughtcrime.securesms.mms.GifSlide
import org.thoughtcrime.securesms.mms.ImageSlide
import org.thoughtcrime.securesms.mms.MediaConstraints
import org.thoughtcrime.securesms.mms.PushMediaConstraints
import org.thoughtcrime.securesms.mms.QuoteModel
import org.thoughtcrime.securesms.mms.Slide
import org.thoughtcrime.securesms.mms.SlideDeck
@@ -2169,7 +2169,7 @@ class ConversationFragment :
inputPanel.clickOnComposeInput()
}
is ShareOrDraftData.SetLocation -> attachmentManager.setLocation(data.location, MediaConstraints.getPushMediaConstraints())
is ShareOrDraftData.SetLocation -> attachmentManager.setLocation(data.location, PushMediaConstraints(null))
is ShareOrDraftData.SetEditMessage -> {
composeText.setDraftText(data.draftText)
@@ -37,6 +37,7 @@ import org.signal.core.util.SqlUtil
import org.signal.core.util.ThreadUtil
import org.signal.core.util.Util
import org.signal.core.util.UuidUtil
import org.signal.core.util.bitmaps.BitmapDecodingException
import org.signal.core.util.copyTo
import org.signal.core.util.count
import org.signal.core.util.delete
@@ -102,7 +103,6 @@ import org.thoughtcrime.securesms.mms.MediaStream
import org.thoughtcrime.securesms.mms.MmsException
import org.thoughtcrime.securesms.mms.PartAuthority
import org.thoughtcrime.securesms.stickers.StickerLocator
import org.thoughtcrime.securesms.util.BitmapDecodingException
import org.thoughtcrime.securesms.util.FileUtils
import org.thoughtcrime.securesms.util.ImageCompressionUtil
import org.thoughtcrime.securesms.util.MediaUtil
@@ -8,8 +8,8 @@ package org.thoughtcrime.securesms.database
import kotlinx.serialization.json.Json
import org.signal.core.models.media.TransformProperties
import org.signal.core.util.logging.Log
import org.signal.mediasend.SentMediaQuality
import org.signal.network.util.JsonUtil
import org.thoughtcrime.securesms.mms.SentMediaQuality
import java.io.IOException
import java.util.Optional
@@ -18,8 +18,8 @@ import org.signal.glide.common.io.GlideStreamConfig;
import org.signal.core.models.database.AttachmentId;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.signal.core.util.bitmaps.BitmapDecodingException;
import org.signal.core.util.bitmaps.BitmapUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import java.io.ByteArrayInputStream;
@@ -48,7 +48,7 @@ import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.ui.disappearingmessages.RecipientDisappearingMessagesActivity;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.signal.core.util.bitmaps.BitmapUtil;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.util.ViewUtil;
@@ -28,13 +28,14 @@ import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec;
import org.signal.glide.decryptableuri.DecryptableUri;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.signal.mediasend.MediaConstraints;
import org.thoughtcrime.securesms.mms.MediaStream;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.SentMediaQuality;
import org.signal.mediasend.SentMediaQuality;
import org.thoughtcrime.securesms.mms.PushMediaConstraints;
import org.thoughtcrime.securesms.service.AttachmentProgressService;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.signal.core.util.bitmaps.BitmapDecodingException;
import org.thoughtcrime.securesms.util.ImageCompressionUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.signal.core.util.MemoryFileDescriptor.MemoryFileException;
@@ -163,7 +164,7 @@ public final class AttachmentCompressionJob extends BaseJob {
return;
}
MediaConstraints mediaConstraints = MediaConstraints.getPushMediaConstraints(SentMediaQuality.fromCode(transformProperties.sentMediaQuality));
MediaConstraints mediaConstraints = new PushMediaConstraints(SentMediaQuality.fromCode(transformProperties.sentMediaQuality));
compress(database, mediaConstraints, databaseAttachment);
}
@@ -196,16 +197,16 @@ public final class AttachmentCompressionJob extends BaseJob {
} else if (MediaUtil.isVideo(attachment)) {
Log.i(TAG, "Compressing video.");
attachment = transcodeVideoIfNeededToDatabase(context, attachmentDatabase, attachment, constraints, EventBus.getDefault(), this::isCanceled);
if (!constraints.isSatisfied(context, attachment)) {
if (!isConstraintsSatisfied(context, attachment, constraints)) {
throw new UndeliverableMessageException("Size constraints could not be met on video!");
}
} else if (constraints.canResize(attachment)) {
} else if (constraints.canResize(attachment.contentType)) {
Log.i(TAG, "Compressing image.");
try (MediaStream converted = compressImage(context, attachment, constraints)) {
attachmentDatabase.updateAttachmentData(attachment, converted);
}
attachmentDatabase.markAttachmentAsTransformed(attachmentId, false);
} else if (constraints.isSatisfied(context, attachment)) {
} else if (isConstraintsSatisfied(context, attachment, constraints)) {
Log.i(TAG, "Not compressing.");
attachmentDatabase.markAttachmentAsTransformed(attachmentId, false);
} else {
@@ -376,6 +377,17 @@ public final class AttachmentCompressionJob extends BaseJob {
return attachment;
}
private static boolean isConstraintsSatisfied(@NonNull Context context,
@NonNull Attachment attachment,
@NonNull MediaConstraints mediaConstraints)
{
if (attachment.getUri() == null || attachment.contentType == null) {
return false;
}
return mediaConstraints.isSatisfied(context, attachment.getUri(), attachment.contentType, attachment.size);
}
/**
* Compresses the images. Given that we compress every image, this has the fun side effect of
* stripping all EXIF data.
@@ -7,10 +7,8 @@ package org.thoughtcrime.securesms.keyboard
import android.net.Uri
import androidx.annotation.WorkerThread
import androidx.core.util.component1
import androidx.core.util.component2
import org.signal.core.util.bitmaps.BitmapUtil
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.util.BitmapUtil
import java.util.concurrent.ExecutionException
import java.util.concurrent.TimeoutException
@@ -12,7 +12,7 @@ import androidx.lifecycle.LiveData;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.mms.SentMediaQuality;
import org.signal.mediasend.SentMediaQuality;
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
import org.thoughtcrime.securesms.util.Environment;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
@@ -49,7 +49,7 @@ import org.thoughtcrime.securesms.service.webrtc.links.ReadCallLinkResult;
import org.thoughtcrime.securesms.stickers.StickerRemoteUri;
import org.thoughtcrime.securesms.stickers.StickerUrl;
import org.thoughtcrime.securesms.util.AvatarUtil;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.signal.core.util.bitmaps.BitmapDecodingException;
import org.signal.core.util.ByteUnit;
import org.thoughtcrime.securesms.util.ImageCompressionUtil;
import org.thoughtcrime.securesms.util.LinkUtil;
@@ -12,7 +12,6 @@ import android.location.Address;
import android.location.Geocoder;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.animation.OvershootInterpolator;
@@ -25,7 +24,6 @@ import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
import com.google.accompanist.permissions.PermissionsUtilKt;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.MapView;
@@ -38,7 +36,7 @@ import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.location.SignalMapView;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.signal.core.util.bitmaps.BitmapUtil;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.MediaUtil;
@@ -6,7 +6,6 @@ import android.graphics.Point;
import android.net.Uri;
import android.os.Bundle;
import android.view.WindowManager;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -17,9 +16,11 @@ import androidx.fragment.app.FragmentTransaction;
import org.signal.core.models.media.Media;
import org.signal.imageeditor.core.model.EditorModel;
import org.signal.mediasend.MediaConstraints;
import org.signal.mediasend.capture.CameraFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.mediasend.v2.gallery.MediaGalleryFragment;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.PushMediaConstraints;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment;
@@ -27,9 +28,6 @@ import org.thoughtcrime.securesms.util.MediaUtil;
import java.io.FileDescriptor;
import java.util.Collections;
import java.util.Optional;
import io.reactivex.rxjava3.core.Flowable;
public class AvatarSelectionActivity extends AppCompatActivity implements CameraFragment.Controller, ImageEditorFragment.Controller, MediaGalleryFragment.Callbacks {
@@ -78,12 +76,6 @@ public class AvatarSelectionActivity extends AppCompatActivity implements Camera
}
}
@Override
public void onCameraError() {
Toast.makeText(this, androidx.biometric.R.string.default_error_msg, Toast.LENGTH_SHORT).show();
finish();
}
@Override
public void onImageCaptured(@NonNull byte[] data, int width, int height) {
Uri blobUri = BlobProvider.getInstance()
@@ -138,14 +130,9 @@ public class AvatarSelectionActivity extends AppCompatActivity implements Camera
throw new UnsupportedOperationException("Cannot select more than one photo");
}
@Override
public @NonNull Flowable<Optional<Media>> getMostRecentMediaItem() {
return Flowable.just(Optional.empty());
}
@Override
public @NonNull MediaConstraints getMediaConstraints() {
return MediaConstraints.getPushMediaConstraints();
return new PushMediaConstraints(null);
}
@Override
@@ -265,7 +252,7 @@ public class AvatarSelectionActivity extends AppCompatActivity implements Camera
return;
}
Fragment fragment = CameraFragment.newInstanceForAvatarCapture();
Fragment fragment = CameraXFragment.newInstanceForAvatarCapture();
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction()
.replace(R.id.fragment_container, fragment, IMAGE_CAPTURE);
@@ -1,265 +0,0 @@
package org.thoughtcrime.securesms.mediasend;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.view.Surface;
import androidx.annotation.NonNull;
import org.signal.core.util.logging.Log;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
class Camera1Controller {
private static final String TAG = Log.tag(Camera1Controller.class);
private final int screenWidth;
private final int screenHeight;
private final OrderEnforcer<Stage> enforcer;
private final EventListener eventListener;
private Camera camera;
private int cameraId;
private SurfaceTexture previewSurface;
private int screenRotation;
Camera1Controller(int preferredDirection, int screenWidth, int screenHeight, @NonNull EventListener eventListener) {
this.eventListener = eventListener;
this.enforcer = new OrderEnforcer<>(Stage.INITIALIZED, Stage.PREVIEW_STARTED);
this.cameraId = Camera.getNumberOfCameras() > 1 ? preferredDirection : Camera.CameraInfo.CAMERA_FACING_BACK;
this.screenWidth = screenWidth;
this.screenHeight = screenHeight;
}
void initialize() {
Log.d(TAG, "initialize()");
if (Camera.getNumberOfCameras() <= 0) {
Log.w(TAG, "Device doesn't have any cameras.");
onCameraUnavailable();
return;
}
try {
camera = Camera.open(cameraId);
} catch (Exception e) {
Log.w(TAG, "Failed to open camera.", e);
onCameraUnavailable();
return;
}
if (camera == null) {
Log.w(TAG, "Null camera instance.");
onCameraUnavailable();
return;
}
Camera.Parameters params = camera.getParameters();
Camera.Size previewSize = getClosestSize(camera.getParameters().getSupportedPreviewSizes(), screenWidth, screenHeight);
Camera.Size pictureSize = getClosestSize(camera.getParameters().getSupportedPictureSizes(), screenWidth, screenHeight);
final List<String> focusModes = params.getSupportedFocusModes();
Log.d(TAG, "Preview size: " + previewSize.width + "x" + previewSize.height + " Picture size: " + pictureSize.width + "x" + pictureSize.height);
params.setPreviewSize(previewSize.width, previewSize.height);
params.setPictureSize(pictureSize.width, pictureSize.height);
params.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
params.setColorEffect(Camera.Parameters.EFFECT_NONE);
params.setWhiteBalance(Camera.Parameters.WHITE_BALANCE_AUTO);
if (focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
} else if (focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) {
params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
}
camera.setParameters(params);
enforcer.markCompleted(Stage.INITIALIZED);
eventListener.onPropertiesAvailable(getProperties());
}
void release() {
Log.d(TAG, "release() called");
enforcer.run(Stage.INITIALIZED, () -> {
Log.d(TAG, "release() executing");
previewSurface = null;
camera.stopPreview();
camera.release();
enforcer.reset();
});
}
void linkSurface(@NonNull SurfaceTexture surfaceTexture) {
Log.d(TAG, "linkSurface() called");
enforcer.run(Stage.INITIALIZED, () -> {
try {
Log.d(TAG, "linkSurface() executing");
previewSurface = surfaceTexture;
camera.setPreviewTexture(surfaceTexture);
camera.startPreview();
enforcer.markCompleted(Stage.PREVIEW_STARTED);
} catch (Exception e) {
Log.w(TAG, "Failed to start preview.", e);
eventListener.onCameraUnavailable();
}
});
}
void capture(@NonNull CaptureCallback callback) {
enforcer.run(Stage.PREVIEW_STARTED, () -> {
camera.takePicture(null, null, null, (data, camera) -> {
callback.onCaptureAvailable(data, cameraId == Camera.CameraInfo.CAMERA_FACING_FRONT);
});
});
}
boolean isCameraFacingFront() {
return cameraId == Camera.CameraInfo.CAMERA_FACING_FRONT;
}
int flip() {
Log.d(TAG, "flip()");
SurfaceTexture surfaceTexture = previewSurface;
cameraId = (cameraId == Camera.CameraInfo.CAMERA_FACING_BACK) ? Camera.CameraInfo.CAMERA_FACING_FRONT : Camera.CameraInfo.CAMERA_FACING_BACK;
release();
initialize();
linkSurface(surfaceTexture);
setScreenRotation(screenRotation);
return cameraId;
}
void setScreenRotation(int screenRotation) {
Log.d(TAG, "setScreenRotation(" + screenRotation + ") called");
enforcer.run(Stage.PREVIEW_STARTED, () -> {
Log.d(TAG, "setScreenRotation(" + screenRotation + ") executing");
this.screenRotation = screenRotation;
int previewRotation = getPreviewRotation(screenRotation);
int outputRotation = getOutputRotation(screenRotation);
Log.d(TAG, "Preview rotation: " + previewRotation + " Output rotation: " + outputRotation);
camera.setDisplayOrientation(previewRotation);
Camera.Parameters params = camera.getParameters();
params.setRotation(outputRotation);
camera.setParameters(params);
});
}
private void onCameraUnavailable() {
enforcer.reset();
eventListener.onCameraUnavailable();
}
private Properties getProperties() {
Camera.Size previewSize = camera.getParameters().getPreviewSize();
return new Properties(Camera.getNumberOfCameras(), previewSize.width, previewSize.height);
}
private Camera.Size getClosestSize(List<Camera.Size> sizes, int width, int height) {
Collections.sort(sizes, ASC_SIZE_COMPARATOR);
int i = 0;
while (i < sizes.size() && (sizes.get(i).width * sizes.get(i).height) < (width * height)) {
i++;
}
i++;
return sizes.get(Math.min(i, sizes.size() - 1));
}
private int getOutputRotation(int displayRotationCode) {
int degrees = convertRotationToDegrees(displayRotationCode);
Camera.CameraInfo info = new Camera.CameraInfo();
Camera.getCameraInfo(cameraId, info);
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
return (info.orientation + degrees) % 360;
} else {
return (info.orientation - degrees + 360) % 360;
}
}
private int getPreviewRotation(int displayRotationCode) {
int degrees = convertRotationToDegrees(displayRotationCode);
Camera.CameraInfo info = new Camera.CameraInfo();
Camera.getCameraInfo(cameraId, info);
int result;
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
result = (info.orientation + degrees) % 360;
result = (360 - result) % 360;
} else {
result = (info.orientation - degrees + 360) % 360;
}
return result;
}
private int convertRotationToDegrees(int screenRotation) {
switch (screenRotation) {
case Surface.ROTATION_0: return 0;
case Surface.ROTATION_90: return 90;
case Surface.ROTATION_180: return 180;
case Surface.ROTATION_270: return 270;
}
return 0;
}
private final Comparator<Camera.Size> ASC_SIZE_COMPARATOR = (o1, o2) -> Integer.compare(o1.width * o1.height, o2.width * o2.height);
private enum Stage {
INITIALIZED, PREVIEW_STARTED
}
class Properties {
private final int cameraCount;
private final int previewWidth;
private final int previewHeight;
Properties(int cameraCount, int previewWidth, int previewHeight) {
this.cameraCount = cameraCount;
this.previewWidth = previewWidth;
this.previewHeight = previewHeight;
}
int getCameraCount() {
return cameraCount;
}
int getPreviewWidth() {
return previewWidth;
}
int getPreviewHeight() {
return previewHeight;
}
@Override
public @NonNull String toString() {
return "cameraCount: " + cameraCount + " previewWidth: " + previewWidth + " previewHeight: " + previewHeight;
}
}
interface EventListener {
void onPropertiesAvailable(@NonNull Properties properties);
void onCameraUnavailable();
}
interface CaptureCallback {
void onCaptureAvailable(@NonNull byte[] jpegData, boolean frontFacing);
}
}
@@ -1,505 +0,0 @@
package org.thoughtcrime.securesms.mediasend;
import android.animation.Animator;
import android.annotation.SuppressLint;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.SurfaceTexture;
import android.graphics.drawable.Drawable;
import android.hardware.Camera;
import android.os.Bundle;
import android.view.Display;
import android.view.GestureDetector;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.TextureView;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.animation.Animation;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.RotateAnimation;
import android.widget.ImageButton;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.MultiTransformation;
import com.bumptech.glide.load.Transformation;
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import com.bumptech.glide.request.target.SimpleTarget;
import com.bumptech.glide.request.transition.Transition;
import com.google.android.material.card.MaterialCardView;
import org.signal.core.util.Stopwatch;
import org.signal.core.util.logging.Log;
import org.signal.core.models.media.Media;
import org.signal.core.ui.logging.LoggingFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
import org.thoughtcrime.securesms.mediasend.camerax.CameraXRemoteConfig;
import org.thoughtcrime.securesms.mediasend.v2.MediaAnimations;
import org.thoughtcrime.securesms.mediasend.v2.MediaCountIndicatorButton;
import org.signal.glide.decryptableuri.DecryptableUri;
import org.signal.core.util.ServiceUtil;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.io.ByteArrayOutputStream;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.Disposable;
/**
* Camera capture implemented with the legacy camera API's. Should only be used if a device is on the {@link CameraXRemoteConfig}.
*/
public class Camera1Fragment extends LoggingFragment implements CameraFragment,
TextureView.SurfaceTextureListener,
Camera1Controller.EventListener
{
private static final String TAG = Log.tag(Camera1Fragment.class);
private TextureView cameraPreview;
private ViewGroup controlsContainer;
private ImageButton flipButton;
private View captureButton;
private Camera1Controller camera;
private Controller controller;
private OrderEnforcer<Stage> orderEnforcer;
private Camera1Controller.Properties properties;
private RotationListener rotationListener;
private Disposable rotationListenerDisposable;
private Disposable mostRecentItemDisposable = Disposable.disposed();
private CameraScreenBrightnessController cameraScreenBrightnessController;
private boolean isMediaSelected;
public static Camera1Fragment newInstance() {
return new Camera1Fragment();
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getActivity() instanceof Controller) {
this.controller = (Controller) getActivity();
} else if (getParentFragment() instanceof Controller) {
this.controller = (Controller) getParentFragment();
}
if (controller == null) {
throw new IllegalStateException("Parent must implement controller interface.");
}
WindowManager windowManager = ServiceUtil.getWindowManager(getActivity());
Display display = windowManager.getDefaultDisplay();
Point displaySize = new Point();
display.getSize(displaySize);
camera = new Camera1Controller(SignalStore.misc().isCameraFacingFront() ? Camera.CameraInfo.CAMERA_FACING_FRONT : Camera.CameraInfo.CAMERA_FACING_BACK, displaySize.x, displaySize.y, this);
orderEnforcer = new OrderEnforcer<>(Stage.SURFACE_AVAILABLE, Stage.CAMERA_PROPERTIES_AVAILABLE);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.camera_fragment, container, false);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
cameraScreenBrightnessController = new CameraScreenBrightnessController(requireActivity().getWindow(), new CameraStateProvider(camera));
getViewLifecycleOwner().getLifecycle().addObserver(cameraScreenBrightnessController);
rotationListener = new RotationListener(requireContext());
cameraPreview = view.findViewById(R.id.camera_preview);
controlsContainer = view.findViewById(R.id.camera_controls_container);
View cameraParent = view.findViewById(R.id.camera_preview_parent);
onOrientationChanged();
cameraPreview.setSurfaceTextureListener(this);
GestureDetector gestureDetector = new GestureDetector(flipGestureListener);
cameraPreview.setOnTouchListener((v, event) -> gestureDetector.onTouchEvent(event));
view.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
// Let's assume portrait for now, so 9:16
float aspectRatio = CameraFragment.getAspectRatioForOrientation(Configuration.ORIENTATION_PORTRAIT);
float width = right - left;
float height = Math.min((1f / aspectRatio) * width, bottom - top);
ViewGroup.LayoutParams params = cameraParent.getLayoutParams();
// If there's a mismatch...
if (params.height != (int) height) {
params.width = (int) width;
params.height = (int) height;
cameraParent.setLayoutParams(params);
}
});
}
@Override
public void onResume() {
super.onResume();
camera.initialize();
if (cameraPreview.isAvailable()) {
orderEnforcer.markCompleted(Stage.SURFACE_AVAILABLE);
}
if (properties != null) {
orderEnforcer.markCompleted(Stage.CAMERA_PROPERTIES_AVAILABLE);
}
orderEnforcer.run(Stage.SURFACE_AVAILABLE, () -> {
camera.linkSurface(cameraPreview.getSurfaceTexture());
});
rotationListenerDisposable = rotationListener.getObservable()
.distinctUntilChanged()
.filter(rotation -> rotation != RotationListener.Rotation.ROTATION_180)
.subscribe(rotation -> {
orderEnforcer.run(Stage.SURFACE_AVAILABLE, () -> {
camera.setScreenRotation(rotation.getSurfaceRotation());
});
});
orderEnforcer.run(Stage.CAMERA_PROPERTIES_AVAILABLE, this::updatePreviewScale);
requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
}
@Override
public void onPause() {
super.onPause();
rotationListenerDisposable.dispose();
camera.release();
orderEnforcer.reset();
}
@Override
public void onDestroyView() {
super.onDestroyView();
mostRecentItemDisposable.dispose();
}
@Override
public void onDestroy() {
super.onDestroy();
requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
}
@Override
public void fadeOutControls(@NonNull Runnable onEndAction) {
controlsContainer.setEnabled(false);
controlsContainer.animate()
.setInterpolator(MediaAnimations.getInterpolator())
.setDuration(250)
.alpha(0f)
.setListener(new AnimationCompleteListener() {
@Override
public void onAnimationEnd(Animator animation) {
controlsContainer.setEnabled(true);
onEndAction.run();
}
});
}
@Override
public void fadeInControls() {
controlsContainer.setEnabled(false);
controlsContainer.animate()
.setInterpolator(MediaAnimations.getInterpolator())
.setDuration(250)
.alpha(1f)
.setListener(new AnimationCompleteListener() {
@Override
public void onAnimationEnd(Animator animation) {
controlsContainer.setEnabled(true);
}
});
}
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
Log.d(TAG, "onSurfaceTextureAvailable");
orderEnforcer.markCompleted(Stage.SURFACE_AVAILABLE);
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
orderEnforcer.run(Stage.CAMERA_PROPERTIES_AVAILABLE, this::updatePreviewScale);
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
return false;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
}
@Override
public void onPropertiesAvailable(@NonNull Camera1Controller.Properties properties) {
Log.d(TAG, "Got camera properties: " + properties);
this.properties = properties;
orderEnforcer.run(Stage.CAMERA_PROPERTIES_AVAILABLE, this::updatePreviewScale);
orderEnforcer.markCompleted(Stage.CAMERA_PROPERTIES_AVAILABLE);
}
@Override
public void onCameraUnavailable() {
controller.onCameraError();
}
private void presentRecentItemThumbnail(@Nullable Media media) {
View thumbBackground = controlsContainer.findViewById(R.id.camera_gallery_button_background);
ImageView thumbnail = controlsContainer.findViewById(R.id.camera_gallery_button);
if (media != null) {
thumbBackground.setBackgroundResource(R.drawable.circle_tintable);
thumbnail.clearColorFilter();
thumbnail.setScaleType(ImageView.ScaleType.FIT_CENTER);
Glide.with(this)
.load(new DecryptableUri(media.getUri()))
.centerCrop()
.into(thumbnail);
} else {
thumbBackground.setBackgroundResource(R.drawable.media_selection_camera_switch_background);
thumbnail.setImageResource(R.drawable.symbol_album_tilt_24);
thumbnail.setColorFilter(Color.WHITE);
thumbnail.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
}
}
@Override
public void presentHud(int selectedMediaCount) {
MediaCountIndicatorButton countButton = controlsContainer.findViewById(R.id.camera_review_button);
View cameraGalleryContainer = controlsContainer.findViewById(R.id.camera_gallery_button_background);
if (selectedMediaCount > 0) {
countButton.setVisibility(View.VISIBLE);
countButton.setCount(selectedMediaCount);
cameraGalleryContainer.setVisibility(View.GONE);
} else {
countButton.setVisibility(View.GONE);
cameraGalleryContainer.setVisibility(View.VISIBLE);
}
isMediaSelected = selectedMediaCount > 0;
updateGalleryVisibility();
}
private void updateGalleryVisibility() {
View cameraGalleryContainer = controlsContainer.findViewById(R.id.camera_gallery_button_background);
if (isMediaSelected) {
cameraGalleryContainer.setVisibility(View.GONE);
} else {
cameraGalleryContainer.setVisibility(View.VISIBLE);
}
}
private void initControls() {
flipButton = requireView().findViewById(R.id.camera_flip_button);
captureButton = requireView().findViewById(R.id.camera_capture_button);
View galleryButton = requireView().findViewById(R.id.camera_gallery_button);
View countButton = requireView().findViewById(R.id.camera_review_button);
mostRecentItemDisposable.dispose();
mostRecentItemDisposable = controller.getMostRecentMediaItem()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(item -> presentRecentItemThumbnail(item.orElse(null)));
initializeViewFinderAndControlsPositioning();
captureButton.setOnClickListener(v -> {
captureButton.setEnabled(false);
onCaptureClicked();
});
captureButton.setOnLongClickListener(unused -> {
CameraFragment.toastVideoRecordingNotAvailable(requireContext());
return true;
});
orderEnforcer.run(Stage.CAMERA_PROPERTIES_AVAILABLE, () -> {
if (properties.getCameraCount() > 1) {
flipButton.setVisibility(properties.getCameraCount() > 1 ? View.VISIBLE : View.GONE);
flipButton.setOnClickListener(v -> {
int newCameraId = camera.flip();
SignalStore.misc().setCameraFacingFront(newCameraId == Camera.CameraInfo.CAMERA_FACING_FRONT);
Animation animation = new RotateAnimation(0, -180, RotateAnimation.RELATIVE_TO_SELF, 0.5f, RotateAnimation.RELATIVE_TO_SELF, 0.5f);
animation.setDuration(200);
animation.setInterpolator(new DecelerateInterpolator());
flipButton.startAnimation(animation);
cameraScreenBrightnessController.onCameraDirectionChanged(newCameraId == Camera.CameraInfo.CAMERA_FACING_FRONT);
});
} else {
flipButton.setVisibility(View.GONE);
}
});
galleryButton.setOnClickListener(v -> controller.onGalleryClicked());
countButton.setOnClickListener(v -> controller.onCameraCountButtonClicked());
}
private void initializeViewFinderAndControlsPositioning() {
MaterialCardView cameraCard = requireView().findViewById(R.id.camera_preview_parent);
View controls = requireView().findViewById(R.id.camera_controls_container);
CameraDisplay cameraDisplay = CameraDisplay.getDisplay(requireActivity());
if (!cameraDisplay.getRoundViewFinderCorners()) {
cameraCard.setRadius(0f);
}
ViewUtil.setBottomMargin(controls, cameraDisplay.getCameraCaptureMarginBottom(getResources()));
if (cameraDisplay.getCameraViewportGravity() == CameraDisplay.CameraViewportGravity.CENTER) {
ConstraintSet constraintSet = new ConstraintSet();
constraintSet.clone((ConstraintLayout) requireView());
constraintSet.connect(R.id.camera_preview_parent, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP);
constraintSet.applyTo((ConstraintLayout) requireView());
} else {
ViewUtil.setBottomMargin(cameraCard, cameraDisplay.getCameraViewportMarginBottom());
}
}
private void onCaptureClicked() {
orderEnforcer.reset();
Stopwatch fastCaptureTimer = new Stopwatch("Capture");
camera.capture((jpegData, frontFacing) -> {
fastCaptureTimer.split("captured");
Transformation<Bitmap> transformation = frontFacing ? new MultiTransformation<>(new CenterCrop(), new FlipTransformation())
: new CenterCrop();
Glide.with(this)
.asBitmap()
.load(jpegData)
.transform(transformation)
.override(cameraPreview.getWidth(), cameraPreview.getHeight())
.into(new SimpleTarget<Bitmap>() {
@Override
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
fastCaptureTimer.split("transform");
ByteArrayOutputStream stream = new ByteArrayOutputStream();
resource.compress(Bitmap.CompressFormat.JPEG, 80, stream);
fastCaptureTimer.split("compressed");
byte[] data = stream.toByteArray();
fastCaptureTimer.split("bytes");
fastCaptureTimer.stop(TAG);
controller.onImageCaptured(data, resource.getWidth(), resource.getHeight());
}
@Override
public void onLoadFailed(@Nullable Drawable errorDrawable) {
controller.onCameraError();
}
});
});
}
private PointF getScaleTransform(float viewWidth, float viewHeight, int cameraWidth, int cameraHeight) {
float camWidth = isPortrait() ? Math.min(cameraWidth, cameraHeight) : Math.max(cameraWidth, cameraHeight);
float camHeight = isPortrait() ? Math.max(cameraWidth, cameraHeight) : Math.min(cameraWidth, cameraHeight);
float scaleX = 1;
float scaleY = 1;
if ((camWidth / viewWidth) > (camHeight / viewHeight)) {
float targetWidth = viewHeight * (camWidth / camHeight);
scaleX = targetWidth / viewWidth;
} else {
float targetHeight = viewWidth * (camHeight / camWidth);
scaleY = targetHeight / viewHeight;
}
return new PointF(scaleX, scaleY);
}
private void onOrientationChanged() {
int layout = R.layout.camera_controls_portrait;
controlsContainer.removeAllViews();
controlsContainer.addView(LayoutInflater.from(getContext()).inflate(layout, controlsContainer, false));
initControls();
}
private void updatePreviewScale() {
PointF scale = getScaleTransform(cameraPreview.getWidth(), cameraPreview.getHeight(), properties.getPreviewWidth(), properties.getPreviewHeight());
Matrix matrix = new Matrix();
float camWidth = isPortrait() ? Math.min(cameraPreview.getWidth(), cameraPreview.getHeight()) : Math.max(cameraPreview.getWidth(), cameraPreview.getHeight());
float camHeight = isPortrait() ? Math.max(cameraPreview.getWidth(), cameraPreview.getHeight()) : Math.min(cameraPreview.getWidth(), cameraPreview.getHeight());
matrix.setScale(scale.x, scale.y);
matrix.postTranslate((camWidth - (camWidth * scale.x)) / 2, (camHeight - (camHeight * scale.y)) / 2);
cameraPreview.setTransform(matrix);
}
private boolean isPortrait() {
return getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT;
}
private final GestureDetector.OnGestureListener flipGestureListener = new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
return true;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
flipButton.performClick();
return true;
}
};
private enum Stage {
SURFACE_AVAILABLE, CAMERA_PROPERTIES_AVAILABLE
}
private static class CameraStateProvider implements CameraScreenBrightnessController.CameraStateProvider {
private final Camera1Controller camera1Controller;
private CameraStateProvider(Camera1Controller camera1Controller) {
this.camera1Controller = camera1Controller;
}
@Override
public boolean isFrontFacingCameraSelected() {
return camera1Controller.isCameraFacingFront();
}
@Override
public boolean isFlashEnabled() {
return false;
}
}
}
@@ -1,80 +0,0 @@
package org.thoughtcrime.securesms.mediasend;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Configuration;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import org.signal.core.models.media.Media;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.mediasend.camerax.CameraXRemoteConfig;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import java.io.FileDescriptor;
import java.util.Optional;
import io.reactivex.rxjava3.core.Flowable;
public interface CameraFragment {
float PORTRAIT_ASPECT_RATIO = 9 / 16f;
@SuppressLint({ "RestrictedApi", "UnsafeOptInUsageError" })
static Fragment newInstance(boolean qrScanEnabled) {
if (CameraXRemoteConfig.isSupported()) {
return CameraXFragment.newInstance(qrScanEnabled);
} else {
return Camera1Fragment.newInstance();
}
}
static Class<? extends Fragment> getFragmentClass() {
if (CameraXRemoteConfig.isSupported()) {
return CameraXFragment.class;
} else {
return Camera1Fragment.class;
}
}
@SuppressLint({ "RestrictedApi", "UnsafeOptInUsageError" })
static Fragment newInstanceForAvatarCapture() {
if (CameraXRemoteConfig.isSupported()) {
return CameraXFragment.newInstanceForAvatarCapture();
} else {
return Camera1Fragment.newInstance();
}
}
static float getAspectRatioForOrientation(int orientation) {
if (orientation == Configuration.ORIENTATION_PORTRAIT) {
return PORTRAIT_ASPECT_RATIO;
} else {
return 1f / PORTRAIT_ASPECT_RATIO;
}
}
static void toastVideoRecordingNotAvailable(@NonNull Context context) {
Toast.makeText(context, R.string.CameraFragment__video_recording_is_not_supported_on_your_device, Toast.LENGTH_SHORT)
.show();
}
void presentHud(int selectedMediaCount);
void fadeOutControls(@NonNull Runnable onEndAction);
void fadeInControls();
interface Controller {
void onCameraError();
void onImageCaptured(@NonNull byte[] data, int width, int height);
void onVideoCaptured(@NonNull FileDescriptor fd);
void onVideoCaptureError();
void onGalleryClicked();
void onCameraCountButtonClicked();
void onQrCodeFound(@NonNull String data);
@NonNull Flowable<Optional<Media>> getMostRecentMediaItem();
@NonNull MediaConstraints getMediaConstraints();
int getMaxVideoDuration();
}
}
@@ -9,6 +9,8 @@ import android.os.Build
import android.os.Bundle
import android.os.ParcelFileDescriptor
import android.widget.Toast
import android.widget.Toast.LENGTH_SHORT
import android.widget.Toast.makeText
import androidx.camera.core.CameraSelector
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
@@ -60,9 +62,11 @@ import org.signal.core.ui.permissions.Permissions
import org.signal.core.util.MemoryFileDescriptor
import org.signal.core.util.asListContains
import org.signal.core.util.logging.Log
import org.signal.mediasend.MediaConstraints
import org.signal.mediasend.capture.CameraFragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.R.string.CameraFragment__video_recording_is_not_supported_on_your_device
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.MediaConstraints
import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.video.VideoUtil
@@ -543,10 +547,12 @@ private fun handleHudEvent(
}
)
} else {
CameraFragment.toastVideoRecordingNotAvailable(context)
makeText(context, CameraFragment__video_recording_is_not_supported_on_your_device, LENGTH_SHORT)
.show()
}
} else {
CameraFragment.toastVideoRecordingNotAvailable(context)
makeText(context, CameraFragment__video_recording_is_not_supported_on_your_device, LENGTH_SHORT)
.show()
}
}
@@ -7,7 +7,7 @@ import androidx.annotation.WorkerThread;
import org.signal.core.models.media.Media;
import org.signal.core.models.media.TransformProperties;
import org.thoughtcrime.securesms.mms.SentMediaQuality;
import org.signal.mediasend.SentMediaQuality;
import java.util.Optional;
@@ -11,10 +11,10 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import org.signal.core.util.Throttler
import org.signal.core.util.logging.Log
import org.signal.mediasend.MediaConstraints
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionViewModel
import org.thoughtcrime.securesms.mediasend.v2.videos.VideoTrimData
import org.thoughtcrime.securesms.mms.MediaConstraints
import org.thoughtcrime.securesms.mms.VideoSlide
import org.thoughtcrime.securesms.scribbles.VideoEditorPlayButtonLayout
import org.thoughtcrime.securesms.util.visible
@@ -4,8 +4,8 @@ import android.content.Context
import androidx.annotation.WorkerThread
import org.signal.core.models.media.Media
import org.signal.core.models.media.TransformProperties
import org.signal.mediasend.SentMediaQuality
import org.thoughtcrime.securesms.mediasend.v2.videos.VideoTrimData
import org.thoughtcrime.securesms.mms.SentMediaQuality
class VideoTrimTransform(private val data: VideoTrimData) : MediaTransform {
@WorkerThread
@@ -1,22 +0,0 @@
package org.thoughtcrime.securesms.mediasend.camerax
import android.os.Build
import org.signal.core.util.asListContains
import org.thoughtcrime.securesms.util.RemoteConfig
/**
* Some phones don't work well with CameraX. This class uses a remote config to decide
* which phones should fall back to the legacy camera.
*/
object CameraXRemoteConfig {
@JvmStatic
fun isSupported(): Boolean {
return !isBlocklisted()
}
@JvmStatic
fun isBlocklisted(): Boolean {
return RemoteConfig.cameraXModelBlocklist.asListContains(Build.MODEL)
}
}
@@ -1,12 +1,7 @@
package org.thoughtcrime.securesms.mediasend.v2
import android.Manifest
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.navigation.NavController
import org.signal.core.ui.permissions.Permissions
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.mediasend.camerax.CameraXRemoteConfig
import org.thoughtcrime.securesms.util.navigation.safeNavigate
class MediaSelectionNavigator(
@@ -32,23 +27,4 @@ class MediaSelectionNavigator(
fun isPreviousScreenMediaReview(navController: NavController): Boolean {
return navController.previousBackStackEntry?.destination?.id == R.id.mediaReviewFragment
}
companion object {
fun Fragment.requestPermissionsForCamera(
onGranted: () -> Unit
) {
if (CameraXRemoteConfig.isSupported()) {
onGranted()
} else {
Permissions.with(this)
.request(Manifest.permission.CAMERA)
.ifNecessary()
.withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_capture_photos_and_video_allow_camera), R.drawable.ic_camera_24)
.withPermanentDenialDialog(getString(R.string.CameraXFragment_signal_needs_camera_access_capture_photos), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_capture_photos_videos, getParentFragmentManager())
.onAllGranted(onGranted)
.onAnyDenied { Toast.makeText(requireContext(), R.string.CameraXFragment_signal_needs_camera_access_capture_photos, Toast.LENGTH_LONG).show() }
.execute()
}
}
}
}
@@ -13,6 +13,8 @@ import org.signal.core.util.BreakIteratorCompat
import org.signal.core.util.ThreadUtil
import org.signal.core.util.logging.Log
import org.signal.imageeditor.core.model.EditorModel
import org.signal.mediasend.MediaConstraints
import org.signal.mediasend.SentMediaQuality
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
import org.thoughtcrime.securesms.conversation.MessageSendType
import org.thoughtcrime.securesms.database.SignalDatabase
@@ -33,9 +35,7 @@ import org.thoughtcrime.securesms.mediasend.VideoTrimTransform
import org.thoughtcrime.securesms.mediasend.v2.videos.VideoTrimData
import org.thoughtcrime.securesms.mms.GifSlide
import org.thoughtcrime.securesms.mms.ImageSlide
import org.thoughtcrime.securesms.mms.MediaConstraints
import org.thoughtcrime.securesms.mms.OutgoingMessage
import org.thoughtcrime.securesms.mms.SentMediaQuality
import org.thoughtcrime.securesms.mms.Slide
import org.thoughtcrime.securesms.mms.SlideDeck
import org.thoughtcrime.securesms.mms.VideoSlide
@@ -2,11 +2,12 @@ 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.thoughtcrime.securesms.conversation.MessageSendType
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mediasend.v2.videos.VideoTrimData
import org.thoughtcrime.securesms.mms.MediaConstraints
import org.thoughtcrime.securesms.mms.SentMediaQuality
import org.thoughtcrime.securesms.mms.PushMediaConstraints
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.util.MediaUtil
@@ -35,7 +36,7 @@ data class MediaSelectionState(
val isVideoTrimmingVisible: Boolean = focusedMedia != null && MediaUtil.isVideoType(focusedMedia.contentType) && MediaConstraints.isVideoTranscodeAvailable() && !focusedMedia.isVideoGif
val transcodingPreset: TranscodingPreset = MediaConstraints.getPushMediaConstraints(SentMediaQuality.fromCode(quality.code)).videoTranscodingSettings
val transcodingPreset: TranscodingPreset = PushMediaConstraints(SentMediaQuality.fromCode(quality.code)).videoTranscodingSettings
val maxSelection = RemoteConfig.maxAttachmentCount
@@ -24,14 +24,15 @@ import org.signal.core.util.Util
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.SentMediaQuality
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.mediasend.MediaSendActivityResult
import org.thoughtcrime.securesms.mediasend.v2.videos.VideoTrimData
import org.thoughtcrime.securesms.mms.MediaConstraints
import org.thoughtcrime.securesms.mms.SentMediaQuality
import org.thoughtcrime.securesms.mms.PushMediaConstraints
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment
@@ -320,7 +321,7 @@ class MediaSelectionViewModel(
}
fun getMediaConstraints(): MediaConstraints {
return MediaConstraints.getPushMediaConstraints()
return PushMediaConstraints(null)
}
fun setSentMediaQuality(sentMediaQuality: SentMediaQuality) {
@@ -4,7 +4,7 @@ import android.content.Context
import androidx.annotation.WorkerThread
import org.signal.core.models.media.Media
import org.signal.core.util.Util
import org.thoughtcrime.securesms.mms.MediaConstraints
import org.signal.mediasend.MediaConstraints
import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.util.MediaUtil
@@ -8,24 +8,22 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.reactivex.rxjava3.core.Flowable
import org.signal.core.models.media.Media
import org.signal.core.ui.permissions.Permissions
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.logging.Log
import org.signal.mediasend.MediaConstraints
import org.signal.mediasend.capture.CameraFragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.mediasend.CameraFragment
import org.thoughtcrime.securesms.mediasend.CameraXFragment
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.mms.MediaConstraints
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.io.FileDescriptor
import java.util.Optional
import java.util.concurrent.TimeUnit
private val TAG = Log.tag(MediaCaptureFragment::class.java)
@@ -49,7 +47,7 @@ class MediaCaptureFragment : Fragment(R.layout.fragment_container), CameraFragme
private val lifecycleDisposable = LifecycleDisposable()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
captureChildFragment = CameraFragment.newInstance(sharedViewModel.isContactSelectionRequired) as CameraFragment
captureChildFragment = CameraXFragment.newInstance(sharedViewModel.isContactSelectionRequired) as CameraFragment
navigator = MediaSelectionNavigator(
toGallery = R.id.action_mediaCaptureFragment_to_mediaGalleryFragment
@@ -140,17 +138,6 @@ class MediaCaptureFragment : Fragment(R.layout.fragment_container), CameraFragme
captureChildFragment.fadeInControls()
}
override fun onCameraError() {
Log.w(TAG, "Camera Error.")
val context = this.context
if (context != null) {
Toast.makeText(context, R.string.MediaSendActivity_camera_unavailable, Toast.LENGTH_SHORT).show()
} else {
Log.w(TAG, "Could not post toast, fragment not attached to a context.")
}
}
override fun onImageCaptured(data: ByteArray, width: Int, height: Int) {
viewModel.onImageCaptured(data, width, height)
}
@@ -184,10 +171,6 @@ class MediaCaptureFragment : Fragment(R.layout.fragment_container), CameraFragme
viewModel.onQrCodeFound(data)
}
override fun getMostRecentMediaItem(): Flowable<Optional<Media>> {
return viewModel.getMostRecentMedia()
}
override fun getMediaConstraints(): MediaConstraints {
return sharedViewModel.getMediaConstraints()
}
@@ -8,10 +8,8 @@ import android.os.Build
import android.provider.MediaStore
import androidx.annotation.WorkerThread
import org.signal.core.models.media.Media
import org.signal.core.ui.util.StorageUtil
import org.signal.core.util.CursorUtil
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.mediasend.MediaRepository
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.util.MediaUtil
@@ -21,25 +19,10 @@ import java.io.FileInputStream
import java.io.IOException
import java.util.LinkedList
private val TAG = Log.tag(MediaCaptureRepository::class.java)
class MediaCaptureRepository(context: Context) {
private val context: Context = context.applicationContext
fun getMostRecentItem(callback: (Media?) -> Unit) {
if (!StorageUtil.canReadAnyFromMediaStore()) {
Log.w(TAG, "Cannot read from storage.")
callback(null)
return
}
SignalExecutors.BOUNDED.execute {
val media: List<Media> = getMediaInBucket(context, Media.ALL_MEDIA_BUCKET_ID, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true)
callback(media.firstOrNull())
}
}
fun renderImageToMedia(data: ByteArray, width: Int, height: Int, onMediaRendered: (Media) -> Unit, onFailedToRender: () -> Unit) {
SignalExecutors.BOUNDED.execute {
val media: Media? = renderCaptureToMedia(
@@ -1,7 +0,0 @@
package org.thoughtcrime.securesms.mediasend.v2.capture
import org.signal.core.models.media.Media
data class MediaCaptureState(
val mostRecentMedia: Media? = null
)
@@ -3,7 +3,6 @@ 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.Flowable
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
@@ -16,9 +15,7 @@ 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 org.thoughtcrime.securesms.util.rx.RxStore
import java.io.FileDescriptor
import java.util.Optional
import java.util.concurrent.TimeUnit
class MediaCaptureViewModel(private val repository: MediaCaptureRepository) : ViewModel() {
@@ -27,8 +24,6 @@ class MediaCaptureViewModel(private val repository: MediaCaptureRepository) : Vi
private val TAG = Log.tag(MediaCaptureViewModel::class.java)
}
private val store: RxStore<MediaCaptureState> = RxStore(MediaCaptureState())
private val internalEvents: Subject<MediaCaptureEvent> = PublishSubject.create()
private val qrData: Subject<String> = PublishSubject.create()
@@ -36,12 +31,6 @@ class MediaCaptureViewModel(private val repository: MediaCaptureRepository) : Vi
val disposables = CompositeDisposable()
init {
repository.getMostRecentItem { media ->
store.update { state ->
state.copy(mostRecentMedia = media)
}
}
disposables += qrData
.throttleFirst(5, TimeUnit.SECONDS)
.filter { UsernameRepository.isValidLink(it) }
@@ -84,7 +73,6 @@ class MediaCaptureViewModel(private val repository: MediaCaptureRepository) : Vi
}
override fun onCleared() {
store.dispose()
disposables.dispose()
}
@@ -96,10 +84,6 @@ class MediaCaptureViewModel(private val repository: MediaCaptureRepository) : Vi
repository.renderVideoToMedia(fd, this::onMediaRendered, this::onMediaRenderFailed)
}
fun getMostRecentMedia(): Flowable<Optional<Media>> {
return store.stateFlowable.map { Optional.ofNullable(it.mostRecentMedia) }
}
fun onQrCodeFound(data: String) {
qrData.onNext(data)
}
@@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.mediasend.v2.gallery
import android.Manifest
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
@@ -26,7 +25,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.camerax.CameraXRemoteConfig
import org.thoughtcrime.securesms.mediasend.v2.review.MediaGalleryGridItemTouchListener
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
import org.thoughtcrime.securesms.util.SystemWindowInsetsSetter
@@ -104,24 +102,7 @@ class MediaGalleryFragment : Fragment(R.layout.v2_media_gallery_fragment) {
if (callbacks.isCameraEnabled()) {
binding.mediaGalleryToolbar.setOnMenuItemClickListener { item ->
if (item.itemId == R.id.action_camera) {
if (CameraXRemoteConfig.isSupported()) {
callbacks.onNavigateToCamera()
} else {
Permissions.with(this)
.request(Manifest.permission.CAMERA)
.ifNecessary()
.onAllGranted { callbacks.onNavigateToCamera() }
.withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_capture_photos_and_video_allow_camera), R.drawable.ic_camera_24)
.withPermanentDenialDialog(
getString(R.string.CameraXFragment_signal_needs_camera_access_capture_photos),
null,
R.string.CameraXFragment_allow_access_camera,
R.string.CameraXFragment_to_capture_photos_videos,
getParentFragmentManager()
)
.onAnyDenied { Toast.makeText(requireContext(), R.string.CameraXFragment_signal_needs_camera_access_capture_photos, Toast.LENGTH_LONG).show() }
.execute()
}
callbacks.onNavigateToCamera()
true
} else {
false
@@ -14,7 +14,6 @@ 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.MediaSelectionNavigator.Companion.requestPermissionsForCamera
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionViewModel
import org.thoughtcrime.securesms.mediasend.v2.review.MediaSelectionItemTouchHelper
import org.signal.core.ui.R as CoreUiR
@@ -126,9 +125,7 @@ class MediaSelectionGalleryFragment : Fragment(R.layout.fragment_container), Med
override fun onNavigateToCamera() {
val controller = findNavController()
requestPermissionsForCamera {
navigator.goToCamera(controller)
}
navigator.goToCamera(controller)
}
override fun onSubmit() {
@@ -39,6 +39,8 @@ 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.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
import org.thoughtcrime.securesms.conversation.MessageSendType
@@ -57,8 +59,6 @@ import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionState
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionViewModel
import org.thoughtcrime.securesms.mediasend.v2.stories.StoriesMultiselectForwardActivity
import org.thoughtcrime.securesms.mediasend.v2.videos.VideoTrimData
import org.thoughtcrime.securesms.mms.MediaConstraints
import org.thoughtcrime.securesms.mms.SentMediaQuality
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment
import org.thoughtcrime.securesms.util.MediaUtil
@@ -30,9 +30,9 @@ import androidx.fragment.app.viewModels
import org.signal.core.ui.compose.BottomSheets
import org.signal.core.ui.compose.ComposeBottomSheetDialogFragment
import org.signal.core.ui.compose.Previews
import org.signal.mediasend.SentMediaQuality
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionViewModel
import org.thoughtcrime.securesms.mms.SentMediaQuality
/**
* Bottom sheet dialog to select the media quality (Standard vs. High) when sending media.
@@ -1,28 +0,0 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.mediasend.v3
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.fragment.compose.AndroidFragment
import org.thoughtcrime.securesms.mediasend.CameraFragment
/**
* Displays the proper camera capture ui
*/
@Composable
fun MediaSendV3CameraSlot() {
val fragmentClass = remember {
CameraFragment.getFragmentClass()
}
AndroidFragment(
clazz = fragmentClass,
modifier = Modifier.fillMaxSize()
)
}
@@ -5,6 +5,7 @@
package org.thoughtcrime.securesms.mediasend.v3
import android.content.Context
import android.net.Uri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
@@ -17,12 +18,14 @@ import org.signal.core.models.media.Media
import org.signal.core.models.media.MediaFolder
import org.signal.core.util.logging.Log
import org.signal.mediasend.EditorState
import org.signal.mediasend.MediaConstraints
import org.signal.mediasend.MediaFilterError
import org.signal.mediasend.MediaFilterResult
import org.signal.mediasend.MediaRecipientId
import org.signal.mediasend.MediaSendRepository
import org.signal.mediasend.SendRequest
import org.signal.mediasend.SendResult
import org.signal.mediasend.SentMediaQuality
import org.signal.mediasend.StorySendRequirements
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
import org.thoughtcrime.securesms.conversation.MessageSendType
@@ -33,14 +36,15 @@ import org.thoughtcrime.securesms.mediasend.MediaRepository
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionRepository
import org.thoughtcrime.securesms.mediasend.v2.MediaValidator
import org.thoughtcrime.securesms.mediasend.v2.videos.VideoTrimData
import org.thoughtcrime.securesms.mms.MediaConstraints
import org.thoughtcrime.securesms.mms.SentMediaQuality
import org.thoughtcrime.securesms.mms.PartAuthority
import org.thoughtcrime.securesms.mms.PushMediaConstraints
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment
import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.util.MediaUtil
import java.io.InputStream
import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume
import kotlin.time.Duration.Companion.seconds
@@ -73,7 +77,7 @@ object MediaSendV3Repository : MediaSendRepository {
isStory: Boolean
): MediaFilterResult = withContext(Dispatchers.IO) {
val populated = MediaRepository().getPopulatedMedia(appContext, media)
val constraints = MediaConstraints.getPushMediaConstraints()
val constraints = PushMediaConstraints(null)
val result = MediaValidator.filterMedia(appContext, populated, constraints, maxSelection, isStory)
val error = mapFilterError(result.filterError, populated, constraints, maxSelection, isStory)
@@ -94,13 +98,12 @@ object MediaSendV3Repository : MediaSendRepository {
}
val legacyEditorStateMap = mapLegacyEditorState(request.editorStateMap)
val quality = SentMediaQuality.fromCode(request.quality)
return@withContext try {
legacyRepository.send(
selectedMedia = request.selectedMedia,
stateMap = legacyEditorStateMap,
quality = quality,
quality = request.quality,
message = request.message,
isViewOnce = request.isViewOnce,
singleContact = null,
@@ -116,13 +119,13 @@ object MediaSendV3Repository : MediaSendRepository {
}
}
override fun getMaxVideoDurationUs(quality: Int, maxFileSizeBytes: Long): Long {
val preset = MediaConstraints.getPushMediaConstraints(SentMediaQuality.fromCode(quality)).videoTranscodingSettings
override fun getMaxVideoDurationUs(quality: SentMediaQuality, maxFileSizeBytes: Long): Long {
val preset = PushMediaConstraints(quality).videoTranscodingSettings
return preset.calculateMaxVideoUploadDurationInSeconds(maxFileSizeBytes).seconds.inWholeMicroseconds
}
override fun getVideoMaxSizeBytes(): Long {
return MediaConstraints.getPushMediaConstraints().videoMaxSize
return PushMediaConstraints(null).videoMaxSize
}
override fun isVideoTranscodeAvailable(): Boolean {
@@ -176,6 +179,10 @@ object MediaSendV3Repository : MediaSendRepository {
.distinctUntilChanged()
}
override fun getAttachmentStream(context: Context, uri: Uri): InputStream {
return PartAuthority.getAttachmentStream(context, uri)
}
private fun resolveSendType(sendType: Int): MessageSendType {
return when (sendType) {
else -> MessageSendType.SignalMessageSendType
@@ -44,6 +44,7 @@ import org.signal.core.util.concurrent.SettableFuture;
import org.signal.core.util.concurrent.SimpleTask;
import org.signal.core.util.logging.Log;
import org.signal.core.ui.view.Stub;
import org.signal.mediasend.MediaConstraints;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.AudioView;
import org.thoughtcrime.securesms.components.DocumentView;
@@ -72,7 +73,7 @@ import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.signal.core.util.bitmaps.BitmapUtil;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.ProfileUtil;
@@ -1,130 +0,0 @@
package org.thoughtcrime.securesms.mms;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import kotlin.Pair;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.jobs.AttachmentUploadJob;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.signal.core.util.MemoryFileDescriptor;
import org.thoughtcrime.securesms.video.TranscodingPreset;
import java.io.IOException;
import java.io.InputStream;
public abstract class MediaConstraints {
private static final String TAG = Log.tag(MediaConstraints.class);
public static MediaConstraints getPushMediaConstraints() {
return getPushMediaConstraints(null);
}
public static MediaConstraints getPushMediaConstraints(@Nullable SentMediaQuality sentMediaQuality) {
return new PushMediaConstraints(sentMediaQuality);
}
public abstract int getImageMaxWidth(Context context);
public abstract int getImageMaxHeight(Context context);
public abstract int getImageMaxSize(Context context);
public TranscodingPreset getVideoTranscodingSettings() {
return TranscodingPreset.LEVEL_1;
}
/**
* Provide a list of dimensions that should be attempted during compression. We will keep moving
* down the list until the image can be scaled to fit under {@link #getImageMaxSize(Context)}.
* The first entry in the list should match your max width/height.
*/
public abstract int[] getImageDimensionTargets(Context context);
public abstract long getGifMaxSize(Context context);
public abstract long getVideoMaxSize();
public @IntRange(from = 0, to = 100) int getImageCompressionQualitySetting(@NonNull Context context) {
return 70;
}
public long getUncompressedVideoMaxSize(Context context) {
return getVideoMaxSize();
}
public long getCompressedVideoMaxSize(Context context) {
return getVideoMaxSize();
}
public abstract long getAudioMaxSize(Context context);
public abstract long getDocumentMaxSize(Context context);
public long getMaxAttachmentSize() {
return AttachmentUploadJob.getMaxPlaintextSize();
}
public boolean isSatisfied(@NonNull Context context, @NonNull Attachment attachment) {
try {
long size = attachment.size;
if (size > getMaxAttachmentSize()) {
return false;
}
return (MediaUtil.isGif(attachment) && size <= getGifMaxSize(context) && isWithinBounds(context, attachment.getUri())) ||
(MediaUtil.isImage(attachment) && size <= getImageMaxSize(context) && isWithinBounds(context, attachment.getUri())) ||
(MediaUtil.isAudio(attachment) && size <= getAudioMaxSize(context)) ||
(MediaUtil.isVideo(attachment) && size <= getVideoMaxSize()) ||
(MediaUtil.isFile(attachment) && size <= getDocumentMaxSize(context));
} catch (IOException ioe) {
Log.w(TAG, "Failed to determine if media's constraints are satisfied.", ioe);
return false;
}
}
public boolean isSatisfied(@NonNull Context context, @NonNull Uri uri, @NonNull String contentType, long size) {
try {
if (size > getMaxAttachmentSize()) {
return false;
}
return (MediaUtil.isGif(contentType) && size <= getGifMaxSize(context) && isWithinBounds(context, uri)) ||
(MediaUtil.isImageType(contentType) && size <= getImageMaxSize(context) && isWithinBounds(context, uri)) ||
(MediaUtil.isAudioType(contentType) && size <= getAudioMaxSize(context)) ||
(MediaUtil.isVideoType(contentType) && size <= getVideoMaxSize()) ||
size <= getDocumentMaxSize(context);
} catch (IOException ioe) {
Log.w(TAG, "Failed to determine if media's constraints are satisfied.", ioe);
return false;
}
}
private boolean isWithinBounds(Context context, Uri uri) throws IOException {
try {
InputStream is = PartAuthority.getAttachmentStream(context, uri);
Pair<Integer, Integer> dimensions = BitmapUtil.getDimensions(is);
return dimensions.getFirst() > 0 && dimensions.getFirst() <= getImageMaxWidth(context) &&
dimensions.getSecond() > 0 && dimensions.getSecond() <= getImageMaxHeight(context);
} catch (BitmapDecodingException e) {
throw new IOException(e);
}
}
public boolean canResize(@NonNull Attachment attachment) {
return MediaUtil.isImage(attachment) && !MediaUtil.isGif(attachment) ||
MediaUtil.isVideo(attachment) && isVideoTranscodeAvailable();
}
public boolean canResize(@NonNull String mediaType) {
return MediaUtil.isImageType(mediaType) && !MediaUtil.isGif(mediaType) ||
MediaUtil.isVideoType(mediaType) && isVideoTranscodeAvailable();
}
public static boolean isVideoTranscodeAvailable() {
return Build.VERSION.SDK_INT >= 26;
}
}
@@ -7,7 +7,10 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.Util;
import org.signal.mediasend.MediaConstraints;
import org.signal.mediasend.SentMediaQuality;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobs.AttachmentUploadJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.LocaleRemoteConfig;
import org.thoughtcrime.securesms.util.RemoteConfig;
@@ -83,6 +86,11 @@ public class PushMediaConstraints extends MediaConstraints {
return getMaxAttachmentSize();
}
@Override
public long getMaxAttachmentSize() {
return AttachmentUploadJob.getMaxPlaintextSize();
}
@Override
public int getImageCompressionQualitySetting(@NonNull Context context) {
return currentConfig.qualitySetting;
@@ -1,28 +0,0 @@
package org.thoughtcrime.securesms.mms
import android.content.Context
import androidx.annotation.StringRes
import org.thoughtcrime.securesms.R
/**
* Quality levels to send media at.
*/
enum class SentMediaQuality(@JvmField val code: Int, @param:StringRes private val label: Int) {
STANDARD(0, R.string.DataAndStorageSettingsFragment__standard),
HIGH(1, R.string.DataAndStorageSettingsFragment__high);
companion object {
@JvmStatic
fun fromCode(code: Int): SentMediaQuality {
return if (HIGH.code == code) {
HIGH
} else {
STANDARD
}
}
fun getLabels(context: Context): Array<String> {
return entries.map { context.getString(it.label) }.toTypedArray()
}
}
}
@@ -13,6 +13,7 @@ import androidx.annotation.Nullable;
import org.signal.blurhash.BlurHash;
import org.signal.core.models.media.TransformProperties;
import org.signal.core.util.logging.Log;
import org.signal.mediasend.SentMediaQuality;
import org.thoughtcrime.securesms.util.MediaUtil;
import java.io.IOException;
@@ -11,6 +11,7 @@ import com.bumptech.glide.Glide
import com.bumptech.glide.load.MultiTransformation
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.resource.bitmap.CircleCrop
import org.signal.core.util.bitmaps.BitmapUtil
import org.signal.glide.decryptableuri.DecryptableUri
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.avatar.fallback.FallbackAvatar
@@ -20,7 +21,6 @@ import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto
import org.thoughtcrime.securesms.conversation.colors.AvatarGradientColors
import org.thoughtcrime.securesms.notifications.NotificationIds
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.BitmapUtil
import java.util.concurrent.ExecutionException
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
@@ -4,6 +4,7 @@ import android.content.Context
import android.net.Uri
import android.os.Build
import org.signal.core.util.asListContains
import org.signal.core.util.bitmaps.BitmapDecodingException
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
import org.signal.glide.decryptableuri.DecryptableUri
@@ -11,7 +12,6 @@ import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.mms.Slide
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.util.BitmapDecodingException
import org.thoughtcrime.securesms.util.ImageCompressionUtil
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.kb
@@ -11,13 +11,12 @@ import androidx.appcompat.widget.Toolbar;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.Navigation;
import org.signal.core.ui.logging.LoggingFragment;
import org.signal.core.util.concurrent.LifecycleDisposable;
import org.signal.core.util.logging.Log;
import org.signal.qr.QrScannerView;
import org.signal.core.ui.logging.LoggingFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.mediasend.camerax.CameraXRemoteConfig;
import org.thoughtcrime.securesms.payments.MobileCoinPublicAddress;
import org.signal.core.util.concurrent.LifecycleDisposable;
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
@@ -53,7 +52,7 @@ public final class PaymentsTransferQrScanFragment extends LoggingFragment {
Toolbar toolbar = view.findViewById(R.id.payments_transfer_scan_qr);
toolbar.setNavigationOnClickListener(v -> Navigation.findNavController(v).popBackStack());
scannerView.start(getViewLifecycleOwner(), CameraXRemoteConfig.isBlocklisted());
scannerView.start(getViewLifecycleOwner());
lifecycleDisposable.bindTo(getViewLifecycleOwner());
@@ -3,7 +3,8 @@ package org.thoughtcrime.securesms.profiles;
import android.content.Context;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.jobs.AttachmentUploadJob;
import org.signal.mediasend.MediaConstraints;
public class ProfileMediaConstraints extends MediaConstraints {
@Override
@@ -45,4 +46,9 @@ public class ProfileMediaConstraints extends MediaConstraints {
public long getDocumentMaxSize(Context context) {
return 0;
}
@Override
public long getMaxAttachmentSize() {
return AttachmentUploadJob.getMaxPlaintextSize();
}
}
@@ -14,12 +14,13 @@ import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.ContentTypeUtil;
import org.signal.core.util.concurrent.ListenableFuture;
import org.signal.core.util.concurrent.SettableFuture;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.signal.mediasend.MediaConstraints;
import org.signal.core.util.bitmaps.BitmapDecodingException;
import org.thoughtcrime.securesms.util.ImageCompressionUtil;
public class SystemProfileUtil {
@@ -38,8 +39,23 @@ public class SystemProfileUtil {
if (!TextUtils.isEmpty(photoUri)) {
try {
BitmapUtil.ScaleResult result = BitmapUtil.createScaledBytes(context, Uri.parse(photoUri), mediaConstraints);
return result.getBitmap();
ImageCompressionUtil.Result result = null;
for (int size : mediaConstraints.getImageDimensionTargets(context)) {
result = ImageCompressionUtil.compressWithinConstraints(context,
ContentTypeUtil.IMAGE_JPEG,
Uri.parse(photoUri),
size,
mediaConstraints.getImageMaxSize(context),
mediaConstraints.getImageCompressionQualitySetting(context));
if (result != null) {
break;
}
}
if (result != null) {
return result.getData();
}
} catch (BitmapDecodingException e) {
Log.w(TAG, e);
}
@@ -54,9 +54,9 @@ 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.MediaConstraints;
import org.signal.mediasend.MediaConstraints;
import org.thoughtcrime.securesms.mms.PushMediaConstraints;
import org.thoughtcrime.securesms.mms.SentMediaQuality;
import org.signal.mediasend.SentMediaQuality;
import org.signal.core.ui.permissions.Permissions;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.scribbles.stickers.AnalogClockStickerRenderer;
@@ -33,7 +33,7 @@ import org.signal.core.models.media.Media;
import org.thoughtcrime.securesms.mediasend.v2.text.TextStoryBackgroundColors;
import org.thoughtcrime.securesms.mms.ImageSlide;
import org.thoughtcrime.securesms.mms.OutgoingMessage;
import org.thoughtcrime.securesms.mms.SentMediaQuality;
import org.signal.mediasend.SentMediaQuality;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.mms.SlideFactory;
@@ -16,6 +16,8 @@ import org.signal.core.ui.BottomSheetUtil
import org.signal.core.util.ThreadUtil
import org.signal.core.util.logging.Log
import org.signal.core.util.orNull
import org.signal.mediasend.MediaConstraints
import org.signal.mediasend.SentMediaQuality
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.contacts.HeaderAction
import org.thoughtcrime.securesms.database.SignalDatabase
@@ -25,9 +27,7 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mediasend.v2.stories.ChooseStoryTypeBottomSheet
import org.thoughtcrime.securesms.mms.MediaConstraints
import org.thoughtcrime.securesms.mms.OutgoingMessage
import org.thoughtcrime.securesms.mms.SentMediaQuality
import org.thoughtcrime.securesms.mms.VideoSlide
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
@@ -17,6 +17,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.signal.core.util.Base64
import org.signal.core.util.DimensionUnit
import org.signal.core.util.bitmaps.BitmapUtil
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.attachments.Attachment
@@ -33,7 +34,6 @@ import org.thoughtcrime.securesms.stories.StoryTextPostModel
import org.thoughtcrime.securesms.stories.landing.StoriesLandingItem
import org.thoughtcrime.securesms.stories.viewer.page.StoryPost
import org.thoughtcrime.securesms.stories.viewer.page.StoryViewerPageState
import org.thoughtcrime.securesms.util.BitmapUtil
import org.thoughtcrime.securesms.util.DeleteDialog
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.SaveAttachmentUtil
@@ -1,12 +0,0 @@
package org.thoughtcrime.securesms.util;
public class BitmapDecodingException extends Exception {
public BitmapDecodingException(String s) {
super(s);
}
public BitmapDecodingException(Exception nested) {
super(nested);
}
}
@@ -1,441 +0,0 @@
package org.thoughtcrime.securesms.util;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.ImageFormat;
import android.graphics.Rect;
import android.graphics.YuvImage;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import kotlin.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.exifinterface.media.ExifInterface;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.Util;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLContext;
import javax.microedition.khronos.egl.EGLDisplay;
public class BitmapUtil {
private static final String TAG = Log.tag(BitmapUtil.class);
private static final int MAX_COMPRESSION_QUALITY = 90;
private static final int MIN_COMPRESSION_QUALITY = 45;
private static final int MAX_COMPRESSION_ATTEMPTS = 5;
private static final int MIN_COMPRESSION_QUALITY_DECREASE = 5;
private static final int MAX_IMAGE_HALF_SCALES = 3;
/**
* @deprecated You probably want to use {@link ImageCompressionUtil} instead, which has a clearer
* contract and handles mimetypes properly.
*/
@Deprecated
@WorkerThread
public static <T> ScaleResult createScaledBytes(@NonNull Context context, @NonNull T model, @NonNull MediaConstraints constraints)
throws BitmapDecodingException
{
return createScaledBytes(context, model,
constraints.getImageMaxWidth(context),
constraints.getImageMaxHeight(context),
constraints.getImageMaxSize(context));
}
/**
* @deprecated You probably want to use {@link ImageCompressionUtil} instead, which has a clearer
* contract and handles mimetypes properly.
*/
@Deprecated
@WorkerThread
public static <T> ScaleResult createScaledBytes(@NonNull Context context,
@NonNull T model,
final int maxImageWidth,
final int maxImageHeight,
final int maxImageSize)
throws BitmapDecodingException
{
return createScaledBytes(context, model, maxImageWidth, maxImageHeight, maxImageSize, CompressFormat.JPEG);
}
/**
* @deprecated You probably want to use {@link ImageCompressionUtil} instead, which has a clearer
* contract and handles mimetypes properly.
*/
@Deprecated
@WorkerThread
public static <T> ScaleResult createScaledBytes(Context context,
T model,
int maxImageWidth,
int maxImageHeight,
int maxImageSize,
@NonNull CompressFormat format)
throws BitmapDecodingException
{
return createScaledBytes(context, model, maxImageWidth, maxImageHeight, maxImageSize, format, 1, 0);
}
@WorkerThread
private static <T> ScaleResult createScaledBytes(@NonNull Context context,
@NonNull T model,
final int maxImageWidth,
final int maxImageHeight,
final int maxImageSize,
@NonNull CompressFormat format,
final int sizeAttempt,
int totalAttempts)
throws BitmapDecodingException
{
try {
int quality = MAX_COMPRESSION_QUALITY;
int attempts = 0;
byte[] bytes;
Bitmap scaledBitmap = Glide.with(context.getApplicationContext())
.asBitmap()
.load(model)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.centerInside()
.submit(maxImageWidth, maxImageHeight)
.get();
if (scaledBitmap == null) {
throw new BitmapDecodingException("Unable to decode image");
}
Log.i(TAG, String.format(Locale.US,"Initial scaled bitmap has size of %d bytes.", scaledBitmap.getByteCount()));
Log.i(TAG, String.format(Locale.US, "Max dimensions %d x %d, %d bytes", maxImageWidth, maxImageHeight, maxImageSize));
try {
do {
totalAttempts++;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
scaledBitmap.compress(format, quality, baos);
bytes = baos.toByteArray();
Log.d(TAG, "iteration with quality " + quality + " size " + bytes.length + " bytes.");
if (quality == MIN_COMPRESSION_QUALITY) break;
int nextQuality = (int)Math.floor(quality * Math.sqrt((double)maxImageSize / bytes.length));
if (quality - nextQuality < MIN_COMPRESSION_QUALITY_DECREASE) {
nextQuality = quality - MIN_COMPRESSION_QUALITY_DECREASE;
}
quality = Math.max(nextQuality, MIN_COMPRESSION_QUALITY);
}
while (bytes.length > maxImageSize && attempts++ < MAX_COMPRESSION_ATTEMPTS);
if (bytes.length > maxImageSize) {
if (sizeAttempt <= MAX_IMAGE_HALF_SCALES) {
scaledBitmap.recycle();
scaledBitmap = null;
Log.i(TAG, "Halving dimensions and retrying.");
return createScaledBytes(context, model, maxImageWidth / 2, maxImageHeight / 2, maxImageSize, format, sizeAttempt + 1, totalAttempts);
} else {
throw new BitmapDecodingException("Unable to scale image below " + bytes.length + " bytes.");
}
}
if (bytes.length <= 0) {
throw new BitmapDecodingException("Decoding failed. Bitmap has a length of " + bytes.length + " bytes.");
}
Log.i(TAG, String.format(Locale.US, "createScaledBytes(%s) -> quality %d, %d attempt(s) over %d sizes.", model.getClass().getName(), quality, totalAttempts, sizeAttempt));
return new ScaleResult(bytes, scaledBitmap.getWidth(), scaledBitmap.getHeight());
} finally {
if (scaledBitmap != null) scaledBitmap.recycle();
}
} catch (InterruptedException | ExecutionException e) {
throw new BitmapDecodingException(e);
}
}
public static @NonNull CompressFormat getCompressFormatForContentType(@Nullable String contentType) {
if (contentType == null) return CompressFormat.JPEG;
switch (contentType) {
case MediaUtil.IMAGE_JPEG: return CompressFormat.JPEG;
case MediaUtil.IMAGE_PNG: return CompressFormat.PNG;
case MediaUtil.IMAGE_WEBP: return CompressFormat.WEBP;
default: return CompressFormat.JPEG;
}
}
private static BitmapFactory.Options getImageDimensions(InputStream inputStream)
throws BitmapDecodingException
{
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BufferedInputStream fis = new BufferedInputStream(inputStream);
BitmapFactory.decodeStream(fis, null, options);
try {
fis.close();
} catch (IOException ioe) {
Log.w(TAG, "failed to close the InputStream after reading image dimensions");
}
if (options.outWidth == -1 || options.outHeight == -1) {
throw new BitmapDecodingException("Failed to decode image dimensions: " + options.outWidth + ", " + options.outHeight);
}
return options;
}
public static int getExifOrientation(ExifInterface exif) {
return exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
}
@Nullable
public static Pair<Integer, Integer> getExifDimensions(ExifInterface exif) {
int width = exif.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, 0);
int height = exif.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, 0);
if (width == 0 || height == 0) {
return null;
}
int orientation = getExifOrientation(exif);
if (orientation == ExifInterface.ORIENTATION_ROTATE_90 ||
orientation == ExifInterface.ORIENTATION_ROTATE_270 ||
orientation == ExifInterface.ORIENTATION_TRANSVERSE ||
orientation == ExifInterface.ORIENTATION_TRANSPOSE)
{
return new Pair<>(height, width);
}
return new Pair<>(width, height);
}
public static Pair<Integer, Integer> getDimensions(InputStream inputStream) throws BitmapDecodingException {
BitmapFactory.Options options = getImageDimensions(inputStream);
return new Pair(options.outWidth, options.outHeight);
}
public static ByteArrayInputStream toCompressedJpeg(Bitmap bitmap) {
ByteArrayOutputStream thumbnailBytes = new ByteArrayOutputStream();
bitmap.compress(CompressFormat.JPEG, 85, thumbnailBytes);
return new ByteArrayInputStream(thumbnailBytes.toByteArray());
}
public static @Nullable byte[] toByteArray(@Nullable Bitmap bitmap) {
if (bitmap == null) return null;
ByteArrayOutputStream stream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
return stream.toByteArray();
}
public static @Nullable byte[] toWebPByteArray(@Nullable Bitmap bitmap) {
if (bitmap == null) return null;
ByteArrayOutputStream stream = new ByteArrayOutputStream();
if (Build.VERSION.SDK_INT >= 30) {
bitmap.compress(CompressFormat.WEBP_LOSSLESS, 100, stream);
} else {
bitmap.compress(CompressFormat.WEBP, 100, stream);
}
return stream.toByteArray();
}
public static @Nullable Bitmap fromByteArray(@Nullable byte[] bytes) {
if (bytes == null) return null;
return BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
}
public static byte[] createFromNV21(@NonNull final byte[] data,
final int width,
final int height,
int rotation,
final Rect croppingRect,
final boolean flipHorizontal)
throws IOException
{
byte[] rotated = rotateNV21(data, width, height, rotation, flipHorizontal);
final int rotatedWidth = rotation % 180 > 0 ? height : width;
final int rotatedHeight = rotation % 180 > 0 ? width : height;
YuvImage previewImage = new YuvImage(rotated, ImageFormat.NV21,
rotatedWidth, rotatedHeight, null);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
previewImage.compressToJpeg(croppingRect, 80, outputStream);
byte[] bytes = outputStream.toByteArray();
outputStream.close();
return bytes;
}
/*
* NV21 a.k.a. YUV420sp
* YUV 4:2:0 planar image, with 8 bit Y samples, followed by interleaved V/U plane with 8bit 2x2
* subsampled chroma samples.
*
* http://www.fourcc.org/yuv.php#NV21
*/
public static byte[] rotateNV21(@NonNull final byte[] yuv,
final int width,
final int height,
final int rotation,
final boolean flipHorizontal)
throws IOException
{
if (rotation == 0) return yuv;
if (rotation % 90 != 0 || rotation < 0 || rotation > 270) {
throw new IllegalArgumentException("0 <= rotation < 360, rotation % 90 == 0");
} else if ((width * height * 3) / 2 != yuv.length) {
throw new IOException("provided width and height don't jive with the data length (" +
yuv.length + "). Width: " + width + " height: " + height +
" = data length: " + (width * height * 3) / 2);
}
final byte[] output = new byte[yuv.length];
final int frameSize = width * height;
final boolean swap = rotation % 180 != 0;
final boolean xflip = flipHorizontal ? rotation % 270 == 0 : rotation % 270 != 0;
final boolean yflip = rotation >= 180;
for (int j = 0; j < height; j++) {
for (int i = 0; i < width; i++) {
final int yIn = j * width + i;
final int uIn = frameSize + (j >> 1) * width + (i & ~1);
final int vIn = uIn + 1;
final int wOut = swap ? height : width;
final int hOut = swap ? width : height;
final int iSwapped = swap ? j : i;
final int jSwapped = swap ? i : j;
final int iOut = xflip ? wOut - iSwapped - 1 : iSwapped;
final int jOut = yflip ? hOut - jSwapped - 1 : jSwapped;
final int yOut = jOut * wOut + iOut;
final int uOut = frameSize + (jOut >> 1) * wOut + (iOut & ~1);
final int vOut = uOut + 1;
output[yOut] = (byte)(0xff & yuv[yIn]);
output[uOut] = (byte)(0xff & yuv[uIn]);
output[vOut] = (byte)(0xff & yuv[vIn]);
}
}
return output;
}
public static Bitmap createFromDrawable(final Drawable drawable, final int width, final int height) {
final AtomicBoolean created = new AtomicBoolean(false);
final Bitmap[] result = new Bitmap[1];
Runnable runnable = new Runnable() {
@Override
public void run() {
if (drawable instanceof BitmapDrawable) {
result[0] = ((BitmapDrawable) drawable).getBitmap();
} else {
int canvasWidth = drawable.getIntrinsicWidth();
if (canvasWidth <= 0) canvasWidth = width;
int canvasHeight = drawable.getIntrinsicHeight();
if (canvasHeight <= 0) canvasHeight = height;
Bitmap bitmap;
try {
bitmap = Bitmap.createBitmap(canvasWidth, canvasHeight, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
} catch (Exception e) {
Log.w(TAG, e);
bitmap = null;
}
result[0] = bitmap;
}
synchronized (result) {
created.set(true);
result.notifyAll();
}
}
};
ThreadUtil.runOnMain(runnable);
synchronized (result) {
while (!created.get()) Util.wait(result, 0);
return result[0];
}
}
public static int getMaxTextureSize() {
final int MAX_ALLOWED_TEXTURE_SIZE = 2048;
EGL10 egl = (EGL10) EGLContext.getEGL();
EGLDisplay display = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
int[] version = new int[2];
egl.eglInitialize(display, version);
int[] totalConfigurations = new int[1];
egl.eglGetConfigs(display, null, 0, totalConfigurations);
EGLConfig[] configurationsList = new EGLConfig[totalConfigurations[0]];
egl.eglGetConfigs(display, configurationsList, totalConfigurations[0], totalConfigurations);
int[] textureSize = new int[1];
int maximumTextureSize = 0;
for (int i = 0; i < totalConfigurations[0]; i++) {
egl.eglGetConfigAttrib(display, configurationsList[i], EGL10.EGL_MAX_PBUFFER_WIDTH, textureSize);
if (maximumTextureSize < textureSize[0])
maximumTextureSize = textureSize[0];
}
egl.eglTerminate(display);
return Math.min(maximumTextureSize, MAX_ALLOWED_TEXTURE_SIZE);
}
public static class ScaleResult {
private final byte[] bitmap;
private final int width;
private final int height;
public ScaleResult(byte[] bitmap, int width, int height) {
this.bitmap = bitmap;
this.width = width;
this.height = height;
}
public byte[] getBitmap() {
return bitmap;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
}
}
@@ -16,6 +16,7 @@ import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import org.signal.core.util.ByteSize;
import org.signal.core.util.bitmaps.BitmapDecodingException;
import org.signal.core.util.logging.Log;
import java.io.ByteArrayOutputStream;
@@ -27,6 +27,8 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.gif.GifDrawable;
import org.signal.core.util.ContentTypeUtil;
import org.signal.core.util.bitmaps.BitmapDecodingException;
import org.signal.core.util.bitmaps.BitmapUtil;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.signal.core.models.database.AttachmentId;
@@ -841,13 +841,6 @@ object RemoteConfig {
hotSwappable = true
)
/** A comma-separated list of manufacturers that should *not* use CameraX. */
val cameraXModelBlocklist: String by remoteString(
key = "android.cameraXModelBlockList.3",
defaultValue = "",
hotSwappable = true
)
/** A comma-separated list of manufacturers that should *not* use CameraX mixed mode. */
val cameraXMixedModelBlocklist: String by remoteString(
key = "android.cameraXMixedModelBlockList.3",
@@ -13,7 +13,6 @@ import org.signal.qr.QrScannerView
import org.signal.qr.kitkat.ScanListener
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ShapeScrim
import org.thoughtcrime.securesms.mediasend.camerax.CameraXRemoteConfig
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.fragments.findListener
@@ -41,7 +40,7 @@ class VerifyScanFragment : Fragment() {
ViewUtil.updateLayoutParams(cameraMarks, width, height)
}
cameraView.start(viewLifecycleOwner, CameraXRemoteConfig.isBlocklisted())
cameraView.start(viewLifecycleOwner)
lifecycleDisposable.bindTo(viewLifecycleOwner)
@@ -1,13 +1,10 @@
package org.thoughtcrime.securesms.video;
import android.content.Context;
import android.content.res.Resources;
import android.util.DisplayMetrics;
import android.util.Size;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.signal.mediasend.MediaConstraints;
import org.thoughtcrime.securesms.video.videoconverter.utils.VideoConstants;
public final class VideoUtil {
@@ -20,7 +20,7 @@ import org.thoughtcrime.securesms.fonts.FontTypefaceProvider;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.AsynchronousCallback;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.signal.core.util.bitmaps.BitmapUtil;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;