diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameLinkSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameLinkSettingsFragment.kt index faf735c9a2..2864a5c3d9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameLinkSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameLinkSettingsFragment.kt @@ -79,6 +79,7 @@ import org.thoughtcrime.securesms.providers.BlobProvider import org.thoughtcrime.securesms.util.CommunicationActions import java.io.ByteArrayOutputStream import java.util.UUID +import org.signal.mediasend.R as MediaSendR @OptIn(ExperimentalPermissionsApi::class) class UsernameLinkSettingsFragment : ComposeFragment() { @@ -158,8 +159,8 @@ class UsernameLinkSettingsFragment : ComposeFragment() { Permissions.with(this) .request(Manifest.permission.CAMERA) .ifNecessary() - .withPermanentDenialDialog(getString(R.string.CameraXFragment_signal_needs_camera_access_scan_qr_code), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_scan_qr_codes, parentFragmentManager) - .onAnyDenied { Toast.makeText(requireContext(), R.string.CameraXFragment_signal_needs_camera_access_scan_qr_code, Toast.LENGTH_LONG).show() } + .withPermanentDenialDialog(getString(MediaSendR.string.CameraXFragment_signal_needs_camera_access_scan_qr_code), null, MediaSendR.string.CameraXFragment_allow_access_camera, MediaSendR.string.CameraXFragment_to_scan_qr_codes, parentFragmentManager) + .onAnyDenied { Toast.makeText(requireContext(), MediaSendR.string.CameraXFragment_signal_needs_camera_access_scan_qr_code, Toast.LENGTH_LONG).show() } .execute() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameQrScanScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameQrScanScreen.kt index 3d39a44e85..28babee4b4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameQrScanScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameQrScanScreen.kt @@ -36,6 +36,7 @@ import org.signal.core.ui.compose.theme.SignalTheme import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.qr.QrCrosshair import org.thoughtcrime.securesms.recipients.Recipient +import org.signal.mediasend.R as MediaSendR /** * A screen that allows you to scan a QR code to start a chat. @@ -116,7 +117,7 @@ fun UsernameQrScanScreen( .padding(48.dp) ) { Text( - text = stringResource(R.string.CameraXFragment_to_scan_qr_code_allow_camera), + text = stringResource(MediaSendR.string.CameraXFragment_to_scan_qr_code_allow_camera), textAlign = TextAlign.Center, style = MaterialTheme.typography.bodyLarge, color = Color.White @@ -125,7 +126,7 @@ fun UsernameQrScanScreen( colors = ButtonDefaults.filledTonalButtonColors(), onClick = onOpenCameraClicked ) { - Text(stringResource(R.string.CameraXFragment_allow_access)) + Text(stringResource(MediaSendR.string.CameraXFragment_allow_access)) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameQrScannerActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameQrScannerActivity.kt index d6a060bcfc..ee21747222 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameQrScannerActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameQrScannerActivity.kt @@ -47,6 +47,7 @@ import org.signal.core.util.permissions.PermissionCompat import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId +import org.signal.mediasend.R as MediaSendR /** * Prompts the user to scan a username QR code. Uses the activity result to communicate the recipient that was found, or null if no valid usernames were scanned. @@ -132,8 +133,8 @@ class UsernameQrScannerActivity : AppCompatActivity() { Permissions.with(this) .request(Manifest.permission.CAMERA) .ifNecessary() - .withPermanentDenialDialog(getString(R.string.CameraXFragment_signal_needs_camera_access_scan_qr_code), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_scan_qr_codes, supportFragmentManager) - .onAnyDenied { Toast.makeText(this, R.string.CameraXFragment_signal_needs_camera_access_scan_qr_code, Toast.LENGTH_LONG).show() } + .withPermanentDenialDialog(getString(MediaSendR.string.CameraXFragment_signal_needs_camera_access_scan_qr_code), null, MediaSendR.string.CameraXFragment_allow_access_camera, MediaSendR.string.CameraXFragment_to_scan_qr_codes, supportFragmentManager) + .onAnyDenied { Toast.makeText(this, MediaSendR.string.CameraXFragment_signal_needs_camera_access_scan_qr_code, Toast.LENGTH_LONG).show() } .execute() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppDependencies.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppDependencies.kt index 9da386887a..d741e583e8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppDependencies.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppDependencies.kt @@ -5,6 +5,7 @@ import android.app.Application import androidx.media3.exoplayer.ExoPlayer import io.reactivex.rxjava3.subjects.BehaviorSubject import okhttp3.OkHttpClient +import org.signal.camera.CameraDependencies import org.signal.core.ui.CoreUiDependencies import org.signal.core.util.CoreUtilDependencies import org.signal.core.util.billing.BillingApi @@ -118,6 +119,7 @@ object AppDependencies { ) CoreUiDependencies.init(application, CoreUiDependenciesProvider) SignalGlideDependencies.init(application, SignalGlideDependenciesProvider) + CameraDependencies.init(application, CameraDependenciesProvider) MediaSendDependencies.init(application, MediaSendDependenciesProvider) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/CameraDependenciesProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/CameraDependenciesProvider.kt new file mode 100644 index 0000000000..a72c705722 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/CameraDependenciesProvider.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.dependencies + +import org.signal.camera.CameraDependencies +import org.thoughtcrime.securesms.stories.Stories + +object CameraDependenciesProvider : CameraDependencies.Provider { + override fun isStoriesFeatureEnabled(): Boolean { + return Stories.isFeatureEnabled() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/AddLinkDeviceFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/AddLinkDeviceFragment.kt index 03780c1fcc..cabdbda95c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/AddLinkDeviceFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/AddLinkDeviceFragment.kt @@ -38,6 +38,7 @@ import org.signal.core.ui.permissions.Permissions import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.util.VibrateUtil import org.thoughtcrime.securesms.util.navigation.safeNavigate +import org.signal.mediasend.R as MediaSendR /** * Fragment that allows users to scan a QR code from their camera to link a device @@ -102,8 +103,8 @@ class AddLinkDeviceFragment : ComposeFragment() { Permissions.with(this) .request(Manifest.permission.CAMERA) .ifNecessary() - .withPermanentDenialDialog(getString(R.string.CameraXFragment_signal_needs_camera_access_scan_qr_code), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_scan_qr_codes, parentFragmentManager) - .onAnyDenied { Toast.makeText(requireContext(), R.string.CameraXFragment_signal_needs_camera_access_scan_qr_code, Toast.LENGTH_LONG).show() } + .withPermanentDenialDialog(getString(MediaSendR.string.CameraXFragment_signal_needs_camera_access_scan_qr_code), null, MediaSendR.string.CameraXFragment_allow_access_camera, MediaSendR.string.CameraXFragment_to_scan_qr_codes, parentFragmentManager) + .onAnyDenied { Toast.makeText(requireContext(), MediaSendR.string.CameraXFragment_signal_needs_camera_access_scan_qr_code, Toast.LENGTH_LONG).show() } .execute() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceQrScanScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceQrScanScreen.kt index c09a549375..50165564dc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceQrScanScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceQrScanScreen.kt @@ -36,6 +36,7 @@ import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.linkdevice.LinkDeviceRepository.LinkDeviceResult import org.thoughtcrime.securesms.qr.QrCrosshair import org.thoughtcrime.securesms.util.navigation.safeNavigate +import org.signal.mediasend.R as MediaSendR /** * A screen that allows you to scan a QR code to link a device @@ -141,7 +142,7 @@ fun LinkDeviceQrScanScreen( .padding(48.dp) ) { Text( - text = stringResource(R.string.CameraXFragment_to_scan_qr_code_allow_camera), + text = stringResource(MediaSendR.string.CameraXFragment_to_scan_qr_code_allow_camera), textAlign = TextAlign.Center, style = MaterialTheme.typography.bodyLarge, color = Color.White @@ -150,7 +151,7 @@ fun LinkDeviceQrScanScreen( colors = ButtonDefaults.filledTonalButtonColors(), onClick = onRequestPermissions ) { - Text(stringResource(R.string.CameraXFragment_allow_access)) + Text(stringResource(MediaSendR.string.CameraXFragment_allow_access)) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionBattery.kt b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionBattery.kt index 00cbe65ae2..c7687fe433 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionBattery.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionBattery.kt @@ -12,7 +12,6 @@ import org.thoughtcrime.securesms.util.BucketInfo import org.thoughtcrime.securesms.util.DeviceProperties import org.thoughtcrime.securesms.util.WakeLockUtil import java.util.Locale -import java.util.concurrent.TimeUnit import kotlin.time.Duration.Companion.milliseconds /** diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionActivity.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionActivity.java index cce1086261..45fb5d8ce1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionActivity.java @@ -17,7 +17,8 @@ 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.signal.mediasend.CameraFragment; +import org.signal.mediasend.capture.CameraXFragment; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.mediasend.v2.gallery.MediaGalleryFragment; import org.thoughtcrime.securesms.mms.PushMediaConstraints; diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraButtonView.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraButtonView.java deleted file mode 100644 index 3777d3bcb1..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraButtonView.java +++ /dev/null @@ -1,246 +0,0 @@ -package org.thoughtcrime.securesms.mediasend; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Rect; -import android.graphics.RectF; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; -import android.view.animation.Animation; -import android.view.animation.AnimationUtils; -import android.view.animation.DecelerateInterpolator; -import android.view.animation.Interpolator; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.signal.core.util.DimensionUnit; -import org.thoughtcrime.securesms.R; -import org.signal.core.util.Util; -import org.thoughtcrime.securesms.util.ViewUtil; - -public class CameraButtonView extends View { - - private enum CameraButtonMode { IMAGE, MIXED } - - private static final float CAPTURE_ARC_STROKE_WIDTH = 3.5f; - private static final int CAPTURE_FILL_PROTECTION = 10; - private static final int PROGRESS_ARC_STROKE_WIDTH = 4; - private static final int HALF_PROGRESS_ARC_STROKE_WIDTH = PROGRESS_ARC_STROKE_WIDTH / 2; - private static final float DEADZONE_REDUCTION_PERCENT = 0.35f; - - private final @NonNull Paint outlinePaint = outlinePaint(); - private final @NonNull Paint backgroundPaint = backgroundPaint(); - private final @NonNull Paint arcPaint = arcPaint(); - private final @NonNull Paint recordPaint = recordPaint(); - private final @NonNull Paint progressPaint = progressPaint(); - private final @NonNull Paint captureFillPaint = captureFillPaint(); - - private Animation growAnimation; - private Animation shrinkAnimation; - - private boolean isRecordingVideo; - private float progressPercent = 0f; - - private @NonNull CameraButtonMode cameraButtonMode = CameraButtonMode.IMAGE; - - private final float imageCaptureSize; - private final float recordSize; - private final RectF progressRect = new RectF(); - private final Rect deadzoneRect = new Rect(); - - - public CameraButtonView(@NonNull Context context) { - this(context, null); - } - - public CameraButtonView(@NonNull Context context, @Nullable AttributeSet attrs) { - this(context, attrs, 0); - } - - public CameraButtonView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - - TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CameraButtonView, defStyleAttr, 0); - - imageCaptureSize = a.getDimensionPixelSize(R.styleable.CameraButtonView_imageCaptureSize, -1); - recordSize = a.getDimensionPixelSize(R.styleable.CameraButtonView_recordSize, -1); - a.recycle(); - - initializeImageAnimations(); - } - - private static Paint recordPaint() { - Paint recordPaint = new Paint(); - recordPaint.setColor(0xFFF44336); - recordPaint.setAntiAlias(true); - recordPaint.setStyle(Paint.Style.FILL); - return recordPaint; - } - - private static Paint outlinePaint() { - Paint outlinePaint = new Paint(); - outlinePaint.setColor(0x26000000); - outlinePaint.setAntiAlias(true); - outlinePaint.setStyle(Paint.Style.STROKE); - outlinePaint.setStrokeWidth(ViewUtil.dpToPx(4)); - return outlinePaint; - } - - private static Paint backgroundPaint() { - Paint backgroundPaint = new Paint(); - backgroundPaint.setColor(0x4CFFFFFF); - backgroundPaint.setAntiAlias(true); - backgroundPaint.setStyle(Paint.Style.FILL); - return backgroundPaint; - } - - private static Paint arcPaint() { - Paint arcPaint = new Paint(); - arcPaint.setColor(0xFFFFFFFF); - arcPaint.setAntiAlias(true); - arcPaint.setStyle(Paint.Style.STROKE); - arcPaint.setStrokeWidth(DimensionUnit.DP.toPixels(CAPTURE_ARC_STROKE_WIDTH)); - return arcPaint; - } - - private static Paint captureFillPaint() { - Paint arcPaint = new Paint(); - arcPaint.setColor(0xFFFFFFFF); - arcPaint.setAntiAlias(true); - arcPaint.setStyle(Paint.Style.FILL); - return arcPaint; - } - - - private static Paint progressPaint() { - Paint progressPaint = new Paint(); - progressPaint.setColor(0xFFFFFFFF); - progressPaint.setAntiAlias(true); - progressPaint.setStyle(Paint.Style.STROKE); - progressPaint.setStrokeWidth(ViewUtil.dpToPx(PROGRESS_ARC_STROKE_WIDTH)); - progressPaint.setShadowLayer(4, 0, 2, 0x40000000); - return progressPaint; - } - - private void initializeImageAnimations() { - shrinkAnimation = AnimationUtils.loadAnimation(getContext(), R.anim.camera_capture_button_shrink); - growAnimation = AnimationUtils.loadAnimation(getContext(), R.anim.camera_capture_button_grow); - - shrinkAnimation.setFillAfter(true); - shrinkAnimation.setFillEnabled(true); - growAnimation.setFillAfter(true); - growAnimation.setFillEnabled(true); - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - - if (isRecordingVideo) { - drawForVideoCapture(canvas); - } else { - drawForImageCapture(canvas); - } - } - - private void drawForImageCapture(Canvas canvas) { - float centerX = getWidth() / 2f; - float centerY = getHeight() / 2f; - - float radius = imageCaptureSize / 2f; - canvas.drawCircle(centerX, centerY, radius, backgroundPaint); - canvas.drawCircle(centerX, centerY, radius, arcPaint); - canvas.drawCircle(centerX, centerY, radius - DimensionUnit.DP.toPixels(CAPTURE_FILL_PROTECTION), captureFillPaint); - } - - private void drawForVideoCapture(Canvas canvas) { - float centerX = getWidth() / 2f; - float centerY = getHeight() / 2f; - - canvas.drawCircle(centerX, centerY, centerY, backgroundPaint); - canvas.drawCircle(centerX, centerY, centerY, outlinePaint); - - canvas.drawCircle(centerX, centerY, recordSize / 2f, recordPaint); - - progressRect.top = ViewUtil.dpToPx(HALF_PROGRESS_ARC_STROKE_WIDTH); - progressRect.left = ViewUtil.dpToPx(HALF_PROGRESS_ARC_STROKE_WIDTH); - progressRect.right = getWidth() - ViewUtil.dpToPx(HALF_PROGRESS_ARC_STROKE_WIDTH); - progressRect.bottom = getHeight() - ViewUtil.dpToPx(HALF_PROGRESS_ARC_STROKE_WIDTH); - - canvas.drawArc(progressRect, 270f, 360f * progressPercent, false, progressPaint); - } - - public void setProgress(float percentage) { - progressPercent = Util.clamp(percentage, 0f, 1f); - invalidate(); - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - if (cameraButtonMode == CameraButtonMode.IMAGE) { - return handleImageModeTouchEvent(event); - } - - boolean eventWasHandled = handleVideoModeTouchEvent(event); - int action = event.getAction(); - - if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { - isRecordingVideo = false; - } - - return eventWasHandled; - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - - getLocalVisibleRect(deadzoneRect); - deadzoneRect.left += (int) (getWidth() * DEADZONE_REDUCTION_PERCENT / 2f); - deadzoneRect.top += (int) (getHeight() * DEADZONE_REDUCTION_PERCENT / 2f); - deadzoneRect.right -= (int) (getWidth() * DEADZONE_REDUCTION_PERCENT / 2f); - deadzoneRect.bottom -= (int) (getHeight() * DEADZONE_REDUCTION_PERCENT / 2f); - } - - private boolean handleImageModeTouchEvent(MotionEvent event) { - int action = event.getAction(); - switch (action) { - case MotionEvent.ACTION_DOWN: - if (isEnabled()) { - startAnimation(shrinkAnimation); - performClick(); - } - return true; - case MotionEvent.ACTION_UP: - startAnimation(growAnimation); - return true; - default: - return super.onTouchEvent(event); - } - } - - private boolean handleVideoModeTouchEvent(MotionEvent event) { - int action = event.getAction(); - switch (action) { - case MotionEvent.ACTION_DOWN: - if (isEnabled()) { - startAnimation(shrinkAnimation); - } - case MotionEvent.ACTION_MOVE: - break; - case MotionEvent.ACTION_CANCEL: - case MotionEvent.ACTION_UP: - if (!isRecordingVideo) { - startAnimation(growAnimation); - } - break; - } - - return super.onTouchEvent(event); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraScreenBrightnessController.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraScreenBrightnessController.kt deleted file mode 100644 index b109edeae6..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraScreenBrightnessController.kt +++ /dev/null @@ -1,71 +0,0 @@ -package org.thoughtcrime.securesms.mediasend - -import android.view.Window -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner - -/** - * Modifies screen brightness to increase to a max of 66% if lower than that for optimal picture - * taking conditions. This brightness is only applied when the front-facing camera is selected. - */ -class CameraScreenBrightnessController( - private val window: Window, - private val cameraStateProvider: CameraStateProvider -) : DefaultLifecycleObserver { - - companion object { - private const val FRONT_CAMERA_BRIGHTNESS = 0.66f - } - - private val originalBrightness: Float by lazy { window.attributes.screenBrightness } - - override fun onResume(owner: LifecycleOwner) { - onCameraDirectionChanged(cameraStateProvider.isFrontFacingCameraSelected()) - onCameraFlashChanged(cameraStateProvider.isFlashEnabled()) - } - - override fun onPause(owner: LifecycleOwner) { - disableBrightness() - } - - /** - * Because setting camera direction is an asynchronous action, we cannot rely on - * the `CameraDirectionProvider` at this point. - */ - fun onCameraDirectionChanged(isFrontFacing: Boolean) { - if (isFrontFacing && cameraStateProvider.isFlashEnabled()) { - enableBrightness() - } else { - disableBrightness() - } - } - - fun onCameraFlashChanged(isFlashEnabled: Boolean) { - if (isFlashEnabled && cameraStateProvider.isFrontFacingCameraSelected()) { - enableBrightness() - } else { - disableBrightness() - } - } - - private fun enableBrightness() { - if (originalBrightness < FRONT_CAMERA_BRIGHTNESS) { - window.attributes = window.attributes.apply { - screenBrightness = FRONT_CAMERA_BRIGHTNESS - } - } - } - - private fun disableBrightness() { - if (window.attributes.screenBrightness == FRONT_CAMERA_BRIGHTNESS) { - window.attributes = window.attributes.apply { - screenBrightness = originalBrightness - } - } - } - - interface CameraStateProvider { - fun isFrontFacingCameraSelected(): Boolean - fun isFlashEnabled(): Boolean - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionActivity.kt index 5e60bc6202..54cd77fb52 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionActivity.kt @@ -23,6 +23,7 @@ import androidx.transition.AutoTransition import androidx.transition.TransitionManager import com.google.android.material.animation.ArgbEvaluatorCompat import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import org.signal.camera.CameraDisplay import org.signal.core.models.media.Media import org.signal.core.util.BreakIteratorCompat import org.signal.core.util.Debouncer @@ -42,7 +43,6 @@ import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardEventViewModel import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageFragment import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchFragment import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil -import org.thoughtcrime.securesms.mediasend.CameraDisplay import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult import org.thoughtcrime.securesms.mediasend.v2.review.MediaReviewFragment import org.thoughtcrime.securesms.mediasend.v2.text.TextStoryPostCreationViewModel diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/capture/MediaCaptureFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/capture/MediaCaptureFragment.kt index 22cc528e01..ea56a61ea0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/capture/MediaCaptureFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/capture/MediaCaptureFragment.kt @@ -11,11 +11,11 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder 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.CameraFragment import org.signal.mediasend.MediaConstraints -import org.signal.mediasend.capture.CameraFragment +import org.signal.mediasend.capture.CameraXFragment import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationFragment.kt index abf61f4932..32d481ff06 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationFragment.kt @@ -17,6 +17,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.kotlin.subscribeBy +import org.signal.camera.CameraDisplay import org.signal.core.util.concurrent.LifecycleDisposable import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey @@ -25,7 +26,6 @@ import org.thoughtcrime.securesms.databinding.StoriesTextPostCreationFragmentBin import org.thoughtcrime.securesms.linkpreview.LinkPreview import org.thoughtcrime.securesms.linkpreview.LinkPreviewState import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModelV2 -import org.thoughtcrime.securesms.mediasend.CameraDisplay import org.thoughtcrime.securesms.mediasend.v2.HudCommand import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionViewModel import org.thoughtcrime.securesms.mediasend.v2.stories.StoriesMultiselectForwardActivity diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v3/MediaSendV3Repository.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v3/MediaSendV3Repository.kt index dd81f257f6..d31d5113dc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v3/MediaSendV3Repository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v3/MediaSendV3Repository.kt @@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.mediasend.v3 import android.content.Context import android.net.Uri +import android.os.Build import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged @@ -16,6 +17,7 @@ import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext import org.signal.core.models.media.Media import org.signal.core.models.media.MediaFolder +import org.signal.core.util.asListContains import org.signal.core.util.logging.Log import org.signal.mediasend.EditorState import org.signal.mediasend.MediaConstraints @@ -32,6 +34,7 @@ import org.thoughtcrime.securesms.conversation.MessageSendType import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.IdentityRecord import org.thoughtcrime.securesms.dependencies.AppDependencies +import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.mediasend.MediaRepository import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionRepository import org.thoughtcrime.securesms.mediasend.v2.MediaValidator @@ -43,6 +46,7 @@ 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 org.thoughtcrime.securesms.util.RemoteConfig import java.io.InputStream import java.util.concurrent.TimeUnit import kotlin.coroutines.resume @@ -58,6 +62,12 @@ object MediaSendV3Repository : MediaSendRepository { private val legacyRepository = MediaSelectionRepository(appContext) private val mediaRepository = MediaRepository() + override var isCameraFacingFront: Boolean + get() = SignalStore.misc.isCameraFacingFront + set(value) { + SignalStore.misc.isCameraFacingFront = value + } + override suspend fun getFolders(): List = suspendCancellableCoroutine { continuation -> mediaRepository.getFolders(appContext) { folders -> continuation.resume(folders) @@ -182,6 +192,10 @@ object MediaSendV3Repository : MediaSendRepository { return PartAuthority.getAttachmentStream(context, uri) } + override fun isMixedModeAvailable(): Boolean { + return !RemoteConfig.cameraXMixedModelBlocklist.asListContains(Build.MODEL) + } + private fun resolveSendType(sendType: Int): MessageSendType { return when (sendType) { else -> MessageSendType.SignalMessageSendType diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/transfer/PaymentsTransferFragment.java b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/transfer/PaymentsTransferFragment.java index 5367160f43..889c69e87f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/transfer/PaymentsTransferFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/transfer/PaymentsTransferFragment.java @@ -96,8 +96,8 @@ public final class PaymentsTransferFragment extends LoggingFragment { Permissions.with(this) .request(Manifest.permission.CAMERA) .ifNecessary() - .withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.PaymentsTransferFragment__to_scan_a_qr_code_signal_needs), R.drawable.ic_camera_24) - .withPermanentDenialDialog(getString(R.string.PaymentsTransferFragment__to_scan_a_qr_code_signal_needs_access_to_the_camera), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_scan_qr_codes, getParentFragmentManager()) + .withRationaleDialog(getString(org.signal.mediasend.R.string.CameraXFragment_allow_access_camera), getString(R.string.PaymentsTransferFragment__to_scan_a_qr_code_signal_needs), R.drawable.ic_camera_24) + .withPermanentDenialDialog(getString(R.string.PaymentsTransferFragment__to_scan_a_qr_code_signal_needs_access_to_the_camera), null, org.signal.mediasend.R.string.CameraXFragment_allow_access_camera, org.signal.mediasend.R.string.CameraXFragment_to_scan_qr_codes, getParentFragmentManager()) .onAllGranted(() -> SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), R.id.action_paymentsTransfer_to_paymentsScanQr)) .onAnyDenied(() -> Toast.makeText(requireContext(), R.string.PaymentsTransferFragment__to_scan_a_qr_code_signal_needs_access_to_the_camera, Toast.LENGTH_LONG).show()) .execute(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyIdentityFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyIdentityFragment.kt index 2cbe0e47b1..bd661cb277 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyIdentityFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyIdentityFragment.kt @@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId +import org.signal.mediasend.R as MediaSendR /** * Fragment to assist user in verifying recipient identity utilizing keys. @@ -106,8 +107,8 @@ class VerifyIdentityFragment : Fragment(R.layout.fragment_container), ScanListen Permissions.with(this) .request(Manifest.permission.CAMERA) .ifNecessary() - .withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_scan_qr_code_allow_camera), R.drawable.ic_camera_24) - .withPermanentDenialDialog(getString(R.string.VerifyIdentityActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code_but_it_has_been_permanently_denied), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_scan_qr_codes, getParentFragmentManager()) + .withRationaleDialog(getString(MediaSendR.string.CameraXFragment_allow_access_camera), getString(MediaSendR.string.CameraXFragment_to_scan_qr_code_allow_camera), R.drawable.ic_camera_24) + .withPermanentDenialDialog(getString(R.string.VerifyIdentityActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code_but_it_has_been_permanently_denied), null, MediaSendR.string.CameraXFragment_allow_access_camera, MediaSendR.string.CameraXFragment_to_scan_qr_codes, getParentFragmentManager()) .onAllGranted { childFragmentManager.beginTransaction() .setCustomAnimations(R.anim.slide_from_top, R.anim.slide_to_bottom, R.anim.slide_from_bottom, R.anim.slide_to_top) @@ -115,7 +116,7 @@ class VerifyIdentityFragment : Fragment(R.layout.fragment_container), ScanListen .addToBackStack(null) .commitAllowingStateLoss() } - .onAnyDenied { Toast.makeText(requireContext(), R.string.CameraXFragment_signal_needs_camera_access_scan_qr_code, Toast.LENGTH_LONG).show() } + .onAnyDenied { Toast.makeText(requireContext(), MediaSendR.string.CameraXFragment_signal_needs_camera_access_scan_qr_code, Toast.LENGTH_LONG).show() } .execute() } diff --git a/app/src/main/res/layout/camera_controls_portrait.xml b/app/src/main/res/layout/camera_controls_portrait.xml deleted file mode 100644 index b01046240b..0000000000 --- a/app/src/main/res/layout/camera_controls_portrait.xml +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/camera_fragment.xml b/app/src/main/res/layout/camera_fragment.xml deleted file mode 100644 index ec0787af70..0000000000 --- a/app/src/main/res/layout/camera_fragment.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/layout/camerax_fragment.xml b/app/src/main/res/layout/camerax_fragment.xml deleted file mode 100644 index a0b42eb5d5..0000000000 --- a/app/src/main/res/layout/camerax_fragment.xml +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 6fca784db4..1239466a2a 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -30,9 +30,6 @@ 104dp 69dp - 124dp - 76dp - 18dp 4dp 2dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fb23def49d..c45b0b977f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -275,60 +275,6 @@ Got it - - - Video recording is not supported on your device - - - Tap for photo, hold for video - - Capture - Change camera - Open gallery - - Allow access - - Allow access to your camera and microphone - - Allow access to your camera - - Allow access to your microphone - - To capture photos and video, allow Signal access to the camera. - - To capture photos and video, allow Signal access to the camera and microphone. - - To capture videos with sound, allow Signal access to your microphone. - - To scan a QR code, allow Signal access to the camera. - - Signal needs camera access to capture photos - - Signal needs camera access to scan QR codes - - Signal needs microphone access to capture video - - To capture photos in Signal: - - To capture photos and videos in Signal: - - To capture videos with sound: - - To scan QR codes: - - Failed to capture photo. Please try again. - - Failed to process photo. Please try again. - - Switch camera - - Flash off - - Flash on - - Flash auto - - Send Search @@ -530,7 +476,6 @@ Signal needs the Camera permission to take photos or video, but it has been permanently denied. Please continue to app settings, select \"Permissions\", and enable \"Camera\". Signal needs Camera permissions to take photos or video Enable the microphone permission to capture videos with sound. - Signal needs microphone permissions to record videos, but they have been denied. Please continue to app settings, select \"Permissions\", and enable \"Microphone\" and \"Camera\". Signal needs microphone permissions to record videos. %1$s %2$s diff --git a/core/ui/src/main/java/org/signal/core/ui/compose/SignalIcons.kt b/core/ui/src/main/java/org/signal/core/ui/compose/SignalIcons.kt index b64ba6404b..007949505e 100644 --- a/core/ui/src/main/java/org/signal/core/ui/compose/SignalIcons.kt +++ b/core/ui/src/main/java/org/signal/core/ui/compose/SignalIcons.kt @@ -64,6 +64,7 @@ enum class SignalIcons(private val icon: SignalIcon) : SignalIcon by icon { Link(icon(R.drawable.symbol_link_24)), Lock(icon(R.drawable.symbol_lock_24)), Maximize(icon(R.drawable.symbol_maximize_24)), + Mic(icon(R.drawable.symbol_mic_24)), MoreVertical(icon(R.drawable.symbol_more_vertical_24)), PersonCircle(icon(R.drawable.symbol_person_circle_24)), Phone(icon(R.drawable.symbol_phone_24)), diff --git a/core/ui/src/main/res/drawable/symbol_mic_24.xml b/core/ui/src/main/res/drawable/symbol_mic_24.xml new file mode 100644 index 0000000000..07b882debe --- /dev/null +++ b/core/ui/src/main/res/drawable/symbol_mic_24.xml @@ -0,0 +1,12 @@ + + + + diff --git a/demo/video/src/androidTest/java/org/thoughtcrime/video/app/transcode/VideoTranscodeInstrumentationTest.kt b/demo/video/src/androidTest/java/org/thoughtcrime/video/app/transcode/VideoTranscodeInstrumentationTest.kt index b104b695ee..6bef8819b0 100644 --- a/demo/video/src/androidTest/java/org/thoughtcrime/video/app/transcode/VideoTranscodeInstrumentationTest.kt +++ b/demo/video/src/androidTest/java/org/thoughtcrime/video/app/transcode/VideoTranscodeInstrumentationTest.kt @@ -16,8 +16,8 @@ import org.junit.Assume import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.signal.core.util.video.TranscodingPreset import org.thoughtcrime.securesms.video.StreamingTranscoder -import org.thoughtcrime.securesms.video.TranscodingPreset import org.thoughtcrime.securesms.video.exceptions.VideoSourceException import org.thoughtcrime.securesms.video.videoconverter.exceptions.CodecUnavailableException import org.thoughtcrime.securesms.video.videoconverter.exceptions.EncodingException diff --git a/feature/camera/src/main/java/org/signal/camera/CameraDependencies.kt b/feature/camera/src/main/java/org/signal/camera/CameraDependencies.kt new file mode 100644 index 0000000000..f849ab5b69 --- /dev/null +++ b/feature/camera/src/main/java/org/signal/camera/CameraDependencies.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.camera + +import android.app.Application + +/** + * Camera Feature Module dependencies + */ +object CameraDependencies { + private lateinit var _application: Application + private lateinit var _provider: Provider + + @Synchronized + fun init(application: Application, provider: Provider) { + if (this::_application.isInitialized || this::_provider.isInitialized) { + return + } + + _application = application + _provider = provider + } + + val application + get() = _application + + fun isStoriesFeatureEnabled(): Boolean { + return _provider.isStoriesFeatureEnabled() + } + + interface Provider { + fun isStoriesFeatureEnabled(): Boolean + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraDisplay.kt b/feature/camera/src/main/java/org/signal/camera/CameraDisplay.kt similarity index 90% rename from app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraDisplay.kt rename to feature/camera/src/main/java/org/signal/camera/CameraDisplay.kt index 42b2a59e96..1397c7686f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraDisplay.kt +++ b/feature/camera/src/main/java/org/signal/camera/CameraDisplay.kt @@ -1,4 +1,9 @@ -package org.thoughtcrime.securesms.mediasend +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.camera import android.app.Activity import android.content.res.Resources @@ -6,8 +11,6 @@ import androidx.annotation.Dimension import androidx.annotation.Px import androidx.window.layout.WindowMetricsCalculator import org.signal.core.util.dp -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.stories.Stories /** * Description of the Camera Viewport, Controls, and Toggle position information. @@ -17,7 +20,7 @@ enum class CameraDisplay( val roundViewFinderCorners: Boolean, private val withTogglePositionInfo: PositionInfo, private val withoutTogglePositionInfo: PositionInfo, - @Dimension(unit = Dimension.DP) private val toggleBottomMargin: Int + @get:Dimension(unit = Dimension.DP) private val toggleBottomMargin: Int ) { DISPLAY_20_9( aspectRatio = 9f / 20f, @@ -89,7 +92,7 @@ enum class CameraDisplay( @JvmOverloads @Px - fun getCameraCaptureMarginBottom(resources: Resources, storiesEnabled: Boolean = Stories.isFeatureEnabled()): Int { + fun getCameraCaptureMarginBottom(resources: Resources, storiesEnabled: Boolean = CameraDependencies.isStoriesFeatureEnabled()): Int { val positionInfo = if (storiesEnabled) withTogglePositionInfo else withoutTogglePositionInfo return positionInfo.cameraCaptureMarginBottomDp.dp - getCameraButtonSizeOffset(resources) @@ -97,14 +100,14 @@ enum class CameraDisplay( @JvmOverloads @Px - fun getCameraViewportMarginBottom(storiesEnabled: Boolean = Stories.isFeatureEnabled()): Int { + fun getCameraViewportMarginBottom(storiesEnabled: Boolean = CameraDependencies.isStoriesFeatureEnabled()): Int { val positionInfo = if (storiesEnabled) withTogglePositionInfo else withoutTogglePositionInfo return positionInfo.cameraViewportMarginBottomDp.dp } @JvmOverloads - fun getCameraViewportGravity(storiesEnabled: Boolean = Stories.isFeatureEnabled()): CameraViewportGravity { + fun getCameraViewportGravity(storiesEnabled: Boolean = CameraDependencies.isStoriesFeatureEnabled()): CameraViewportGravity { val positionInfo = if (storiesEnabled) withTogglePositionInfo else withoutTogglePositionInfo return positionInfo.cameraViewportGravity diff --git a/feature/camera/src/main/res/values/dimen.xml b/feature/camera/src/main/res/values/dimen.xml new file mode 100644 index 0000000000..0ae9438e5e --- /dev/null +++ b/feature/camera/src/main/res/values/dimen.xml @@ -0,0 +1,9 @@ + + + + 124dp + 76dp + \ No newline at end of file diff --git a/feature/media-send/build.gradle.kts b/feature/media-send/build.gradle.kts index edc2704dc4..f33c8615e4 100644 --- a/feature/media-send/build.gradle.kts +++ b/feature/media-send/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { implementation(project(":lib:image-editor")) implementation(project(":lib:glide")) implementation(project(":lib:video")) + implementation(project(":feature:camera")) // Compose BOM platform(libs.androidx.compose.bom).let { composeBom -> @@ -67,6 +68,9 @@ dependencies { // Media implementation(libs.androidx.media3.exoplayer) + // CameraX + implementation(libs.androidx.camera.core) + // Testing testImplementation(testLibs.junit.junit) testImplementation(testLibs.mockk) diff --git a/feature/media-send/src/main/java/org/signal/mediasend/capture/CameraFragment.java b/feature/media-send/src/main/java/org/signal/mediasend/CameraFragment.java similarity index 89% rename from feature/media-send/src/main/java/org/signal/mediasend/capture/CameraFragment.java rename to feature/media-send/src/main/java/org/signal/mediasend/CameraFragment.java index 237fd14c8d..ab47eefe57 100644 --- a/feature/media-send/src/main/java/org/signal/mediasend/capture/CameraFragment.java +++ b/feature/media-send/src/main/java/org/signal/mediasend/CameraFragment.java @@ -3,12 +3,10 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.signal.mediasend.capture; +package org.signal.mediasend; import androidx.annotation.NonNull; -import org.signal.mediasend.MediaConstraints; - import java.io.FileDescriptor; public interface CameraFragment { diff --git a/feature/media-send/src/main/java/org/signal/mediasend/MediaSendRepository.kt b/feature/media-send/src/main/java/org/signal/mediasend/MediaSendRepository.kt index ea596c1087..f76ff9263e 100644 --- a/feature/media-send/src/main/java/org/signal/mediasend/MediaSendRepository.kt +++ b/feature/media-send/src/main/java/org/signal/mediasend/MediaSendRepository.kt @@ -102,6 +102,10 @@ interface MediaSendRepository { fun observeRecipientValid(recipientId: MediaRecipientId): Flow fun getAttachmentStream(context: Context, uri: Uri): InputStream + + fun isMixedModeAvailable(): Boolean + + var isCameraFacingFront: Boolean } /** diff --git a/app/src/main/java/org/thoughtcrime/securesms/video/VideoUtil.java b/feature/media-send/src/main/java/org/signal/mediasend/VideoUtil.java similarity index 83% rename from app/src/main/java/org/thoughtcrime/securesms/video/VideoUtil.java rename to feature/media-send/src/main/java/org/signal/mediasend/VideoUtil.java index 10a91fbb78..414f468990 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/video/VideoUtil.java +++ b/feature/media-send/src/main/java/org/signal/mediasend/VideoUtil.java @@ -1,10 +1,14 @@ -package org.thoughtcrime.securesms.video; +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.mediasend; import android.content.Context; import androidx.annotation.NonNull; -import org.signal.mediasend.MediaConstraints; import org.thoughtcrime.securesms.video.videoconverter.utils.VideoConstants; public final class VideoUtil { diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.kt b/feature/media-send/src/main/java/org/signal/mediasend/capture/CameraXFragment.kt similarity index 92% rename from app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.kt rename to feature/media-send/src/main/java/org/signal/mediasend/capture/CameraXFragment.kt index 2699630810..74841a5728 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.kt +++ b/feature/media-send/src/main/java/org/signal/mediasend/capture/CameraXFragment.kt @@ -1,4 +1,9 @@ -package org.thoughtcrime.securesms.mediasend +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.mediasend.capture import android.Manifest import android.content.Context @@ -8,9 +13,9 @@ import android.graphics.Bitmap import android.os.Build import android.os.Bundle import android.os.ParcelFileDescriptor +import android.system.Os +import android.system.OsConstants 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 @@ -41,10 +46,14 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import kotlinx.coroutines.delay import org.signal.camera.CameraCaptureMode +import org.signal.camera.CameraDependencies +import org.signal.camera.CameraDisplay import org.signal.camera.CameraScreen import org.signal.camera.CameraScreenEvents import org.signal.camera.CameraScreenViewModel @@ -57,19 +66,16 @@ import org.signal.camera.hud.StandardCameraHudEvents import org.signal.camera.hud.StringResources import org.signal.core.ui.BottomSheetUtil import org.signal.core.ui.compose.ComposeFragment -import org.signal.core.ui.permissions.PermissionDeniedBottomSheet.Companion.showPermissionFragment +import org.signal.core.ui.compose.Previews +import org.signal.core.ui.permissions.PermissionDeniedBottomSheet 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.CameraFragment 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.stories.Stories -import org.thoughtcrime.securesms.util.RemoteConfig -import org.thoughtcrime.securesms.video.VideoUtil +import org.signal.mediasend.MediaSendDependencies +import org.signal.mediasend.R +import org.signal.mediasend.VideoUtil import java.io.ByteArrayOutputStream import java.io.IOException @@ -199,13 +205,13 @@ class CameraXFragment : ComposeFragment(), CameraFragment { } .onSomePermanentlyDenied { deniedPermissions -> if (deniedPermissions.containsAll(listOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO))) { - showPermissionFragment( + PermissionDeniedBottomSheet.showPermissionFragment( R.string.CameraXFragment_allow_access_camera_microphone, R.string.CameraXFragment_to_capture_photos_videos, false ).show(parentFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) } else if (deniedPermissions.contains(Manifest.permission.CAMERA)) { - showPermissionFragment( + PermissionDeniedBottomSheet.showPermissionFragment( R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_capture_photos_videos, false @@ -253,8 +259,14 @@ class CameraXFragment : ComposeFragment(), CameraFragment { Permissions.with(this) .request(Manifest.permission.RECORD_AUDIO) .ifNecessary() - .withRationaleDialog(getString(R.string.CameraXFragment_allow_access_microphone), getString(R.string.CameraXFragment_to_capture_videos_with_sound), R.drawable.ic_mic_24) - .withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_recording_permissions_to_capture_video), null, R.string.CameraXFragment_allow_access_microphone, R.string.CameraXFragment_to_capture_videos, parentFragmentManager) + .withRationaleDialog(getString(R.string.CameraXFragment_allow_access_microphone), getString(R.string.CameraXFragment_to_capture_videos_with_sound), org.signal.core.ui.R.drawable.symbol_mic_24) + .withPermanentDenialDialog( + getString(R.string.CameraXFragment_signal_needs_the_recording_permissions_to_capture_video), + null, + R.string.CameraXFragment_allow_access_microphone, + R.string.CameraXFragment_to_capture_videos, + parentFragmentManager + ) .onAnyDenied { Toast.makeText(requireContext(), R.string.CameraXFragment_signal_needs_microphone_access_video, Toast.LENGTH_LONG).show() } .execute() } @@ -301,7 +313,7 @@ class CameraXFragment : ComposeFragment(), CameraFragment { val isMixedModeSupported = isVideoSupported && CameraXUtil.isMixedModeSupported(requireContext()) && - !RemoteConfig.cameraXMixedModelBlocklist.asListContains(Build.MODEL) + MediaSendDependencies.mediaSendRepository.isMixedModeAvailable() return when { isMixedModeSupported -> CameraCaptureMode.ImageAndVideoSimultaneous @@ -326,7 +338,7 @@ private fun CameraXScreen( createVideoFileDescriptor: () -> ParcelFileDescriptor?, getMaxVideoDurationInSeconds: () -> Int, cameraDisplay: CameraDisplay, - storiesEnabled: Boolean = Stories.isFeatureEnabled() + storiesEnabled: Boolean = CameraDependencies.isStoriesFeatureEnabled() ) { val context = LocalContext.current val cameraViewModel: CameraScreenViewModel = viewModel() @@ -334,7 +346,7 @@ private fun CameraXScreen( var hasPermission by remember { mutableStateOf(hasCameraPermission()) } LaunchedEffect(cameraViewModel) { - val lensFacing = if (SignalStore.misc.isCameraFacingFront) { + val lensFacing = if (MediaSendDependencies.mediaSendRepository.isCameraFacingFront) { CameraSelector.LENS_FACING_FRONT } else { CameraSelector.LENS_FACING_BACK @@ -345,7 +357,7 @@ private fun CameraXScreen( LaunchedEffect(cameraViewModel) { snapshotFlow { cameraState.lensFacing } .collect { lensFacing -> - SignalStore.misc.isCameraFacingFront = lensFacing == CameraSelector.LENS_FACING_FRONT + MediaSendDependencies.mediaSendRepository.isCameraFacingFront = lensFacing == CameraSelector.LENS_FACING_FRONT } } @@ -365,7 +377,7 @@ private fun CameraXScreen( LaunchedEffect(Unit) { while (true) { - kotlinx.coroutines.delay(500) + delay(500) val newHasPermission = hasCameraPermission() if (newHasPermission != hasPermission) { hasPermission = newHasPermission @@ -547,11 +559,11 @@ private fun handleHudEvent( } ) } else { - makeText(context, CameraFragment__video_recording_is_not_supported_on_your_device, LENGTH_SHORT) + Toast.makeText(context, R.string.CameraFragment__video_recording_is_not_supported_on_your_device, Toast.LENGTH_SHORT) .show() } } else { - makeText(context, CameraFragment__video_recording_is_not_supported_on_your_device, LENGTH_SHORT) + Toast.makeText(context, R.string.CameraFragment__video_recording_is_not_supported_on_your_device, Toast.LENGTH_SHORT) .show() } } @@ -605,7 +617,7 @@ private fun handleVideoCaptured(result: VideoCaptureResult, controller: CameraFr result.fileDescriptor?.let { parcelFd -> try { // Seek to beginning before reading - android.system.Os.lseek(parcelFd.fileDescriptor, 0, android.system.OsConstants.SEEK_SET) + Os.lseek(parcelFd.fileDescriptor, 0, OsConstants.SEEK_SET) controller?.onVideoCaptured(parcelFd.fileDescriptor) } catch (e: Exception) { Log.w(TAG, "Failed to seek video file descriptor", e) @@ -621,7 +633,7 @@ private fun handleVideoCaptured(result: VideoCaptureResult, controller: CameraFr } } -@androidx.compose.ui.tooling.preview.Preview( +@Preview( name = "20:9 Display", showBackground = true, widthDp = 360, @@ -629,7 +641,7 @@ private fun handleVideoCaptured(result: VideoCaptureResult, controller: CameraFr ) @Composable private fun CameraXScreenPreview_20_9() { - org.signal.core.ui.compose.Previews.Preview { + Previews.Preview { CameraXScreen( controller = null, isVideoEnabled = true, @@ -649,7 +661,7 @@ private fun CameraXScreenPreview_20_9() { } } -@androidx.compose.ui.tooling.preview.Preview( +@Preview( name = "19:9 Display", showBackground = true, widthDp = 360, @@ -657,7 +669,7 @@ private fun CameraXScreenPreview_20_9() { ) @Composable private fun CameraXScreenPreview_19_9() { - org.signal.core.ui.compose.Previews.Preview { + Previews.Preview { CameraXScreen( controller = null, isVideoEnabled = true, @@ -677,7 +689,7 @@ private fun CameraXScreenPreview_19_9() { } } -@androidx.compose.ui.tooling.preview.Preview( +@Preview( name = "18:9 Display", showBackground = true, widthDp = 360, @@ -685,7 +697,7 @@ private fun CameraXScreenPreview_19_9() { ) @Composable private fun CameraXScreenPreview_18_9() { - org.signal.core.ui.compose.Previews.Preview { + Previews.Preview { CameraXScreen( controller = null, isVideoEnabled = true, @@ -705,7 +717,7 @@ private fun CameraXScreenPreview_18_9() { } } -@androidx.compose.ui.tooling.preview.Preview( +@Preview( name = "16:9 Display", showBackground = true, widthDp = 360, @@ -713,7 +725,7 @@ private fun CameraXScreenPreview_18_9() { ) @Composable private fun CameraXScreenPreview_16_9() { - org.signal.core.ui.compose.Previews.Preview { + Previews.Preview { CameraXScreen( controller = null, isVideoEnabled = true, @@ -733,7 +745,7 @@ private fun CameraXScreenPreview_16_9() { } } -@androidx.compose.ui.tooling.preview.Preview( +@Preview( name = "6:5 Display (Tablet)", showBackground = true, widthDp = 480, @@ -741,7 +753,7 @@ private fun CameraXScreenPreview_16_9() { ) @Composable private fun CameraXScreenPreview_6_5() { - org.signal.core.ui.compose.Previews.Preview { + Previews.Preview { CameraXScreen( controller = null, isVideoEnabled = true, diff --git a/feature/media-send/src/main/res/values/strings.xml b/feature/media-send/src/main/res/values/strings.xml index 0d8008cd5c..db4566bdb2 100644 --- a/feature/media-send/src/main/res/values/strings.xml +++ b/feature/media-send/src/main/res/values/strings.xml @@ -28,4 +28,61 @@ Text Story Play video + + + + Video recording is not supported on your device + + + Tap for photo, hold for video + + Capture + Change camera + Open gallery + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: + + Failed to capture photo. Please try again. + + Failed to process photo. Please try again. + + Switch camera + + Flash off + + Flash on + + Flash auto + + Send + + Signal needs microphone permissions to record videos, but they have been denied. Please continue to app settings, select \"Permissions\", and enable \"Microphone\" and \"Camera\".