mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-08-06 13:31:18 +01:00
Rip out Camera1.
This commit is contained in:
committed by
Greyson Parrelli
parent
c8d2a06676
commit
a7b4a5d93d
@@ -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")
|
||||
|
||||
+9
-8
@@ -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;
|
||||
|
||||
+1
-1
@@ -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
-1
@@ -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(
|
||||
|
||||
+1
-1
@@ -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
-18
@@ -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 = {
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+2
-19
@@ -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?) {
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+5
-18
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
|
||||
+4
-3
@@ -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
|
||||
|
||||
|
||||
+4
-21
@@ -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()
|
||||
}
|
||||
|
||||
-17
@@ -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
|
||||
)
|
||||
-16
@@ -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
-20
@@ -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
|
||||
|
||||
+1
-4
@@ -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() {
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+3
-4
@@ -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 {
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user