mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-08-06 05:14:50 +01:00
Move CameraXFragment to feature:media-send.
This commit is contained in:
+3
-2
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+15
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
package org.thoughtcrime.securesms.mediasend
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.res.Resources
|
||||
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.
|
||||
*/
|
||||
enum class CameraDisplay(
|
||||
private val aspectRatio: Float,
|
||||
val roundViewFinderCorners: Boolean,
|
||||
private val withTogglePositionInfo: PositionInfo,
|
||||
private val withoutTogglePositionInfo: PositionInfo,
|
||||
@Dimension(unit = Dimension.DP) private val toggleBottomMargin: Int
|
||||
) {
|
||||
DISPLAY_20_9(
|
||||
aspectRatio = 9f / 20f,
|
||||
roundViewFinderCorners = true,
|
||||
withTogglePositionInfo = PositionInfo(
|
||||
cameraCaptureMarginBottomDp = 130,
|
||||
cameraViewportMarginBottomDp = 106,
|
||||
cameraViewportGravity = CameraViewportGravity.BOTTOM
|
||||
),
|
||||
withoutTogglePositionInfo = PositionInfo(
|
||||
cameraCaptureMarginBottomDp = 130,
|
||||
cameraViewportGravity = CameraViewportGravity.CENTER
|
||||
),
|
||||
toggleBottomMargin = 52
|
||||
),
|
||||
DISPLAY_19_9(
|
||||
aspectRatio = 9f / 19f,
|
||||
roundViewFinderCorners = true,
|
||||
withTogglePositionInfo = PositionInfo(
|
||||
cameraCaptureMarginBottomDp = 128,
|
||||
cameraViewportMarginBottomDp = 104,
|
||||
cameraViewportGravity = CameraViewportGravity.BOTTOM
|
||||
),
|
||||
withoutTogglePositionInfo = PositionInfo(
|
||||
cameraCaptureMarginBottomDp = 128,
|
||||
cameraViewportGravity = CameraViewportGravity.CENTER
|
||||
),
|
||||
toggleBottomMargin = 52
|
||||
),
|
||||
DISPLAY_18_9(
|
||||
aspectRatio = 9f / 18f,
|
||||
roundViewFinderCorners = true,
|
||||
withTogglePositionInfo = PositionInfo(
|
||||
cameraCaptureMarginBottomDp = 120,
|
||||
cameraViewportGravity = CameraViewportGravity.CENTER
|
||||
),
|
||||
withoutTogglePositionInfo = PositionInfo(
|
||||
cameraCaptureMarginBottomDp = 84,
|
||||
cameraViewportGravity = CameraViewportGravity.CENTER
|
||||
),
|
||||
toggleBottomMargin = 54
|
||||
),
|
||||
DISPLAY_16_9(
|
||||
aspectRatio = 9f / 16f,
|
||||
roundViewFinderCorners = false,
|
||||
withTogglePositionInfo = PositionInfo(
|
||||
cameraCaptureMarginBottomDp = 120,
|
||||
cameraViewportGravity = CameraViewportGravity.BOTTOM
|
||||
),
|
||||
withoutTogglePositionInfo = PositionInfo(
|
||||
cameraCaptureMarginBottomDp = 84,
|
||||
cameraViewportGravity = CameraViewportGravity.BOTTOM
|
||||
),
|
||||
toggleBottomMargin = 54
|
||||
),
|
||||
DISPLAY_6_5(
|
||||
aspectRatio = 5f / 6f,
|
||||
roundViewFinderCorners = false,
|
||||
withTogglePositionInfo = PositionInfo(
|
||||
cameraCaptureMarginBottomDp = 120,
|
||||
cameraViewportGravity = CameraViewportGravity.CENTER
|
||||
),
|
||||
withoutTogglePositionInfo = PositionInfo(
|
||||
cameraCaptureMarginBottomDp = 84,
|
||||
cameraViewportGravity = CameraViewportGravity.CENTER
|
||||
),
|
||||
toggleBottomMargin = 54
|
||||
);
|
||||
|
||||
@JvmOverloads
|
||||
@Px
|
||||
fun getCameraCaptureMarginBottom(resources: Resources, storiesEnabled: Boolean = Stories.isFeatureEnabled()): Int {
|
||||
val positionInfo = if (storiesEnabled) withTogglePositionInfo else withoutTogglePositionInfo
|
||||
|
||||
return positionInfo.cameraCaptureMarginBottomDp.dp - getCameraButtonSizeOffset(resources)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
@Px
|
||||
fun getCameraViewportMarginBottom(storiesEnabled: Boolean = Stories.isFeatureEnabled()): Int {
|
||||
val positionInfo = if (storiesEnabled) withTogglePositionInfo else withoutTogglePositionInfo
|
||||
|
||||
return positionInfo.cameraViewportMarginBottomDp.dp
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun getCameraViewportGravity(storiesEnabled: Boolean = Stories.isFeatureEnabled()): CameraViewportGravity {
|
||||
val positionInfo = if (storiesEnabled) withTogglePositionInfo else withoutTogglePositionInfo
|
||||
|
||||
return positionInfo.cameraViewportGravity
|
||||
}
|
||||
|
||||
@Px
|
||||
fun getToggleBottomMargin(): Int {
|
||||
return toggleBottomMargin.dp
|
||||
}
|
||||
|
||||
companion object {
|
||||
@Px
|
||||
@JvmStatic
|
||||
private fun getCameraButtonSizeOffset(resources: Resources): Int {
|
||||
val cameraCaptureButtonSize = resources.getDimensionPixelSize(R.dimen.camera_capture_button_size)
|
||||
val cameraCaptureImageButtonSize = resources.getDimensionPixelSize(R.dimen.camera_capture_image_button_size)
|
||||
|
||||
return (cameraCaptureButtonSize - cameraCaptureImageButtonSize) / 2
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the camera display type given the current window metrics. Note that this
|
||||
* will automatically invert the aspect ratio in the case of a non-portrait orientation,
|
||||
* since we fix camera to portrait.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getDisplay(activity: Activity): CameraDisplay {
|
||||
val windowMetricsCalculator = WindowMetricsCalculator.getOrCreate()
|
||||
val windowMetrics = windowMetricsCalculator.computeCurrentWindowMetrics(activity)
|
||||
val width = windowMetrics.bounds.width()
|
||||
val height = windowMetrics.bounds.height()
|
||||
val winAr = width.toFloat() / height
|
||||
val aspectRatio = if (winAr > 1f) 1 / winAr else winAr
|
||||
|
||||
return when {
|
||||
aspectRatio <= DISPLAY_20_9.aspectRatio -> DISPLAY_20_9
|
||||
aspectRatio <= DISPLAY_19_9.aspectRatio -> DISPLAY_19_9
|
||||
aspectRatio <= DISPLAY_18_9.aspectRatio -> DISPLAY_18_9
|
||||
aspectRatio >= DISPLAY_6_5.aspectRatio -> DISPLAY_6_5
|
||||
else -> DISPLAY_16_9
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class CameraViewportGravity {
|
||||
CENTER,
|
||||
BOTTOM
|
||||
}
|
||||
|
||||
data class PositionInfo(
|
||||
@Dimension(unit = Dimension.DP) val cameraCaptureMarginBottomDp: Int,
|
||||
@Dimension(unit = Dimension.DP) val cameraViewportMarginBottomDp: Int = 0,
|
||||
val cameraViewportGravity: CameraViewportGravity
|
||||
)
|
||||
}
|
||||
-71
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,762 +0,0 @@
|
||||
package org.thoughtcrime.securesms.mediasend
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
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
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import org.signal.camera.CameraCaptureMode
|
||||
import org.signal.camera.CameraScreen
|
||||
import org.signal.camera.CameraScreenEvents
|
||||
import org.signal.camera.CameraScreenViewModel
|
||||
import org.signal.camera.CameraXUtil
|
||||
import org.signal.camera.VideoCaptureResult
|
||||
import org.signal.camera.VideoOutput
|
||||
import org.signal.camera.hud.GalleryThumbnailButton
|
||||
import org.signal.camera.hud.StandardCameraHud
|
||||
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.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.stories.Stories
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.thoughtcrime.securesms.video.VideoUtil
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
|
||||
private val TAG = Log.tag(CameraXFragment::class.java)
|
||||
|
||||
/**
|
||||
* Camera capture implemented using a Compose-based CameraScreen with CameraX SDK under the hood.
|
||||
* This is the preferred camera implementation when supported.
|
||||
*/
|
||||
class CameraXFragment : ComposeFragment(), CameraFragment {
|
||||
companion object {
|
||||
private const val IS_VIDEO_ENABLED = "is_video_enabled"
|
||||
private const val IS_QR_SCAN_ENABLED = "is_qr_scan_enabled"
|
||||
private const val CONTROLS_ANIMATION_DURATION = 250L
|
||||
|
||||
@JvmStatic
|
||||
fun newInstanceForAvatarCapture(): CameraXFragment {
|
||||
return CameraXFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putBoolean(IS_VIDEO_ENABLED, false)
|
||||
putBoolean(IS_QR_SCAN_ENABLED, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun newInstance(qrScanEnabled: Boolean): CameraXFragment {
|
||||
return CameraXFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putBoolean(IS_QR_SCAN_ENABLED, qrScanEnabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var controller: CameraFragment.Controller? = null
|
||||
private var videoFileDescriptor: MemoryFileDescriptor? = null
|
||||
private var captureMode: CameraCaptureMode = CameraCaptureMode.ImageOnly
|
||||
|
||||
private val isVideoEnabled: Boolean
|
||||
get() = requireArguments().getBoolean(IS_VIDEO_ENABLED, true)
|
||||
|
||||
private val isQrScanEnabled: Boolean
|
||||
get() = requireArguments().getBoolean(IS_QR_SCAN_ENABLED, false)
|
||||
|
||||
private var controlsVisible = mutableStateOf(true)
|
||||
private var selectedMediaCount = mutableIntStateOf(0)
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
controller = when {
|
||||
activity is CameraFragment.Controller -> activity as CameraFragment.Controller
|
||||
parentFragment is CameraFragment.Controller -> parentFragment as CameraFragment.Controller
|
||||
else -> throw IllegalStateException("Parent must implement Controller interface.")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
captureMode = resolveCaptureMode()
|
||||
Log.d(TAG, "Starting CameraX with capture mode $captureMode")
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
CameraXScreen(
|
||||
controller = controller,
|
||||
isVideoEnabled = captureMode != CameraCaptureMode.ImageOnly,
|
||||
isQrScanEnabled = isQrScanEnabled,
|
||||
captureMode = captureMode,
|
||||
controlsVisible = controlsVisible.value,
|
||||
selectedMediaCount = selectedMediaCount.intValue,
|
||||
onCheckPermissions = { checkPermissions(isVideoEnabled) },
|
||||
hasCameraPermission = { hasCameraPermission() },
|
||||
onRequestMicPermission = { requestMicPermission() },
|
||||
onGalleryClicked = { controller?.onGalleryClicked() },
|
||||
createVideoFileDescriptor = { createVideoFileDescriptor() },
|
||||
getMaxVideoDurationInSeconds = { getMaxVideoDurationInSeconds() },
|
||||
cameraDisplay = CameraDisplay.getDisplay(requireActivity())
|
||||
)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
closeVideoFileDescriptor()
|
||||
requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
|
||||
}
|
||||
|
||||
override fun presentHud(selectedMediaCount: Int) {
|
||||
this.selectedMediaCount.intValue = selectedMediaCount
|
||||
}
|
||||
|
||||
override fun fadeOutControls(onEndAction: Runnable) {
|
||||
controlsVisible.value = false
|
||||
// Post the end action after a short delay to allow animation to complete
|
||||
view?.postDelayed({ onEndAction.run() }, CONTROLS_ANIMATION_DURATION)
|
||||
}
|
||||
|
||||
override fun fadeInControls() {
|
||||
controlsVisible.value = true
|
||||
}
|
||||
|
||||
private fun checkPermissions(includeAudio: Boolean) {
|
||||
if (hasCameraPermission()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (includeAudio) {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)
|
||||
.ifNecessary()
|
||||
.onSomeGranted { permissions ->
|
||||
// Will trigger recomposition via hasCameraPermission check
|
||||
}
|
||||
.onSomePermanentlyDenied { deniedPermissions ->
|
||||
if (deniedPermissions.containsAll(listOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO))) {
|
||||
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(
|
||||
R.string.CameraXFragment_allow_access_camera,
|
||||
R.string.CameraXFragment_to_capture_photos_videos,
|
||||
false
|
||||
).show(parentFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||
}
|
||||
}
|
||||
.onSomeDenied { deniedPermissions ->
|
||||
if (deniedPermissions.contains(Manifest.permission.CAMERA)) {
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
R.string.CameraXFragment_signal_needs_camera_access_capture_photos,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
.execute()
|
||||
} else {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.onAllGranted { /* Will trigger recomposition */ }
|
||||
.onAnyDenied {
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
R.string.CameraXFragment_signal_needs_camera_access_capture_photos,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
.withPermanentDenialDialog(
|
||||
getString(R.string.CameraXFragment_signal_needs_camera_access_capture_photos),
|
||||
null,
|
||||
R.string.CameraXFragment_allow_access_camera,
|
||||
R.string.CameraXFragment_to_capture_photos,
|
||||
parentFragmentManager
|
||||
)
|
||||
.execute()
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasCameraPermission(): Boolean {
|
||||
return Permissions.hasAll(requireContext(), Manifest.permission.CAMERA)
|
||||
}
|
||||
|
||||
private fun requestMicPermission() {
|
||||
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)
|
||||
.onAnyDenied { Toast.makeText(requireContext(), R.string.CameraXFragment_signal_needs_microphone_access_video, Toast.LENGTH_LONG).show() }
|
||||
.execute()
|
||||
}
|
||||
|
||||
private fun createVideoFileDescriptor(): ParcelFileDescriptor? {
|
||||
if (Build.VERSION.SDK_INT < 26) {
|
||||
throw IllegalStateException("Video capture requires API 26 or higher")
|
||||
}
|
||||
|
||||
return try {
|
||||
closeVideoFileDescriptor()
|
||||
videoFileDescriptor = CameraXUtil.createVideoFileDescriptor(requireContext())
|
||||
videoFileDescriptor?.parcelFd
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Failed to create video file descriptor", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun closeVideoFileDescriptor() {
|
||||
videoFileDescriptor?.let {
|
||||
try {
|
||||
it.close()
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Failed to close video file descriptor", e)
|
||||
}
|
||||
videoFileDescriptor = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMaxVideoDurationInSeconds(): Int {
|
||||
var maxDuration = VideoUtil.getMaxVideoRecordDurationInSeconds(requireContext(), controller!!.mediaConstraints)
|
||||
val controllerMaxDuration = controller?.maxVideoDuration ?: 0
|
||||
if (controllerMaxDuration > 0) {
|
||||
maxDuration = controllerMaxDuration
|
||||
}
|
||||
return maxDuration
|
||||
}
|
||||
|
||||
private fun resolveCaptureMode(): CameraCaptureMode {
|
||||
val isVideoSupported = Build.VERSION.SDK_INT >= 26 &&
|
||||
isVideoEnabled &&
|
||||
MediaConstraints.isVideoTranscodeAvailable()
|
||||
|
||||
val isMixedModeSupported = isVideoSupported &&
|
||||
CameraXUtil.isMixedModeSupported(requireContext()) &&
|
||||
!RemoteConfig.cameraXMixedModelBlocklist.asListContains(Build.MODEL)
|
||||
|
||||
return when {
|
||||
isMixedModeSupported -> CameraCaptureMode.ImageAndVideoSimultaneous
|
||||
isVideoSupported -> CameraCaptureMode.ImageAndVideoExclusive
|
||||
else -> CameraCaptureMode.ImageOnly
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CameraXScreen(
|
||||
controller: CameraFragment.Controller?,
|
||||
isVideoEnabled: Boolean,
|
||||
isQrScanEnabled: Boolean,
|
||||
captureMode: CameraCaptureMode,
|
||||
controlsVisible: Boolean,
|
||||
selectedMediaCount: Int,
|
||||
onCheckPermissions: () -> Unit,
|
||||
hasCameraPermission: () -> Boolean,
|
||||
onRequestMicPermission: () -> Unit,
|
||||
onGalleryClicked: () -> Unit,
|
||||
createVideoFileDescriptor: () -> ParcelFileDescriptor?,
|
||||
getMaxVideoDurationInSeconds: () -> Int,
|
||||
cameraDisplay: CameraDisplay,
|
||||
storiesEnabled: Boolean = Stories.isFeatureEnabled()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val cameraViewModel: CameraScreenViewModel = viewModel()
|
||||
val cameraState by cameraViewModel.state
|
||||
var hasPermission by remember { mutableStateOf(hasCameraPermission()) }
|
||||
|
||||
LaunchedEffect(cameraViewModel) {
|
||||
val lensFacing = if (SignalStore.misc.isCameraFacingFront) {
|
||||
CameraSelector.LENS_FACING_FRONT
|
||||
} else {
|
||||
CameraSelector.LENS_FACING_BACK
|
||||
}
|
||||
cameraViewModel.setLensFacing(lensFacing)
|
||||
}
|
||||
|
||||
LaunchedEffect(cameraViewModel) {
|
||||
snapshotFlow { cameraState.lensFacing }
|
||||
.collect { lensFacing ->
|
||||
SignalStore.misc.isCameraFacingFront = lensFacing == CameraSelector.LENS_FACING_FRONT
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (!hasPermission) {
|
||||
onCheckPermissions()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(cameraViewModel, isQrScanEnabled) {
|
||||
if (isQrScanEnabled) {
|
||||
cameraViewModel.qrCodeDetected.collect { qrCode ->
|
||||
controller?.onQrCodeFound(qrCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
while (true) {
|
||||
kotlinx.coroutines.delay(500)
|
||||
val newHasPermission = hasCameraPermission()
|
||||
if (newHasPermission != hasPermission) {
|
||||
hasPermission = newHasPermission
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val resources = LocalContext.current.resources
|
||||
|
||||
val hudBottomMargin = with(LocalDensity.current) {
|
||||
cameraDisplay.getCameraCaptureMarginBottom(resources, storiesEnabled).toDp()
|
||||
}
|
||||
|
||||
val viewportGravity = cameraDisplay.getCameraViewportGravity(storiesEnabled)
|
||||
val cameraAlignment = when (viewportGravity) {
|
||||
CameraDisplay.CameraViewportGravity.CENTER -> Alignment.Center
|
||||
CameraDisplay.CameraViewportGravity.BOTTOM -> Alignment.BottomCenter
|
||||
}
|
||||
|
||||
val viewportBottomMargin = if (viewportGravity == CameraDisplay.CameraViewportGravity.BOTTOM) {
|
||||
with(LocalDensity.current) { cameraDisplay.getCameraViewportMarginBottom(storiesEnabled).toDp() }
|
||||
} else {
|
||||
0.dp
|
||||
}
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
// We have to do a bunch of match to figure out how to place the camera buttons because
|
||||
// the logic relies on positining things from the edge of the screen, which doesn't jive
|
||||
// with how the composables are arranged. When this screen is re-written, we should simplify
|
||||
// this whole setup. For now, I'm just doing my best to match current behavior.
|
||||
val cameraAspectRatio = 9f / 16f
|
||||
val availableHeight = maxHeight - viewportBottomMargin
|
||||
val availableAspectRatio = maxWidth / availableHeight
|
||||
val matchHeightFirst = availableAspectRatio > cameraAspectRatio
|
||||
|
||||
val viewportHeight = if (matchHeightFirst) {
|
||||
availableHeight
|
||||
} else {
|
||||
maxWidth / cameraAspectRatio
|
||||
}
|
||||
|
||||
val bottomGapFromAlignment = when (viewportGravity) {
|
||||
CameraDisplay.CameraViewportGravity.CENTER -> (availableHeight - viewportHeight) / 2
|
||||
CameraDisplay.CameraViewportGravity.BOTTOM -> 0.dp
|
||||
}
|
||||
|
||||
val totalBottomOffset = viewportBottomMargin + bottomGapFromAlignment
|
||||
val hudBottomPaddingInsideViewport = maxOf(0.dp, hudBottomMargin - totalBottomOffset)
|
||||
|
||||
if (hasPermission) {
|
||||
CameraScreen(
|
||||
state = cameraState,
|
||||
emitter = { event -> cameraViewModel.onEvent(event) },
|
||||
roundCorners = cameraDisplay.roundViewFinderCorners,
|
||||
contentAlignment = cameraAlignment,
|
||||
captureMode = captureMode,
|
||||
enableQrScanning = isQrScanEnabled,
|
||||
modifier = Modifier.padding(bottom = viewportBottomMargin)
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = controlsVisible,
|
||||
enter = fadeIn(animationSpec = tween(durationMillis = 150)),
|
||||
exit = fadeOut(animationSpec = tween(durationMillis = 150))
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
StandardCameraHud(
|
||||
state = cameraState,
|
||||
modifier = Modifier.padding(bottom = hudBottomPaddingInsideViewport),
|
||||
maxRecordingDurationMs = getMaxVideoDurationInSeconds() * 1000L,
|
||||
mediaSelectionCount = selectedMediaCount,
|
||||
hasAudioPermission = { context.checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED },
|
||||
emitter = { event ->
|
||||
handleHudEvent(
|
||||
event = event,
|
||||
context = context,
|
||||
cameraViewModel = cameraViewModel,
|
||||
controller = controller,
|
||||
isVideoEnabled = isVideoEnabled,
|
||||
onRequestMicPermission = onRequestMicPermission,
|
||||
createVideoFileDescriptor = createVideoFileDescriptor
|
||||
)
|
||||
},
|
||||
stringResources = StringResources(
|
||||
photoCaptureFailed = R.string.CameraXFragment_photo_capture_failed,
|
||||
photoProcessingFailed = R.string.CameraXFragment_photo_processing_failed
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
PermissionMissingContent(
|
||||
isVideoEnabled = isVideoEnabled,
|
||||
onRequestPermissions = onCheckPermissions,
|
||||
onGalleryClicked = onGalleryClicked,
|
||||
galleryButtonBottomPadding = hudBottomMargin + 16.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PermissionMissingContent(
|
||||
isVideoEnabled: Boolean,
|
||||
onRequestPermissions: () -> Unit,
|
||||
onGalleryClicked: () -> Unit,
|
||||
galleryButtonBottomPadding: Dp = 16.dp
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val hasAudioPermission = remember { Permissions.hasAll(context, Manifest.permission.RECORD_AUDIO) }
|
||||
|
||||
val textResId = if (!isVideoEnabled || hasAudioPermission) {
|
||||
R.string.CameraXFragment_to_capture_photos_and_video_allow_camera
|
||||
} else {
|
||||
R.string.CameraXFragment_to_capture_photos_and_video_allow_camera_microphone
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(textResId),
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Button(onClick = onRequestPermissions) {
|
||||
Text(text = stringResource(R.string.CameraXFragment_allow_access))
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(bottom = galleryButtonBottomPadding, end = 40.dp)
|
||||
) {
|
||||
GalleryThumbnailButton(onClick = onGalleryClicked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleHudEvent(
|
||||
event: StandardCameraHudEvents,
|
||||
context: Context,
|
||||
cameraViewModel: CameraScreenViewModel,
|
||||
controller: CameraFragment.Controller?,
|
||||
isVideoEnabled: Boolean,
|
||||
onRequestMicPermission: () -> Unit,
|
||||
createVideoFileDescriptor: () -> ParcelFileDescriptor?
|
||||
) {
|
||||
when (event) {
|
||||
is StandardCameraHudEvents.PhotoCaptureTriggered -> {
|
||||
cameraViewModel.capturePhoto(
|
||||
context = context,
|
||||
onPhotoCaptured = { bitmap ->
|
||||
handlePhotoCaptured(bitmap, controller)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
is StandardCameraHudEvents.VideoCaptureStarted -> {
|
||||
if (Build.VERSION.SDK_INT >= 26 && isVideoEnabled) {
|
||||
val fileDescriptor = createVideoFileDescriptor()
|
||||
if (fileDescriptor != null) {
|
||||
cameraViewModel.startRecording(
|
||||
context = context,
|
||||
output = VideoOutput.FileDescriptorOutput(fileDescriptor),
|
||||
onVideoCaptured = { result ->
|
||||
handleVideoCaptured(result, controller)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
makeText(context, CameraFragment__video_recording_is_not_supported_on_your_device, LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
} else {
|
||||
makeText(context, CameraFragment__video_recording_is_not_supported_on_your_device, LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
is StandardCameraHudEvents.VideoCaptureStopped -> {
|
||||
cameraViewModel.stopRecording()
|
||||
}
|
||||
|
||||
is StandardCameraHudEvents.GalleryClick -> {
|
||||
controller?.onGalleryClicked()
|
||||
}
|
||||
|
||||
is StandardCameraHudEvents.MediaSelectionClick -> {
|
||||
controller?.onCameraCountButtonClicked()
|
||||
}
|
||||
|
||||
is StandardCameraHudEvents.ToggleFlash -> {
|
||||
cameraViewModel.onEvent(CameraScreenEvents.NextFlashMode)
|
||||
}
|
||||
|
||||
is StandardCameraHudEvents.ClearCaptureError -> {
|
||||
cameraViewModel.onEvent(CameraScreenEvents.ClearCaptureError)
|
||||
}
|
||||
|
||||
is StandardCameraHudEvents.SwitchCamera -> {
|
||||
cameraViewModel.onEvent(CameraScreenEvents.SwitchCamera(context))
|
||||
}
|
||||
|
||||
is StandardCameraHudEvents.SetZoomLevel -> {
|
||||
cameraViewModel.onEvent(CameraScreenEvents.LinearZoom(event.zoomLevel))
|
||||
}
|
||||
|
||||
is StandardCameraHudEvents.AudioPermissionRequired -> {
|
||||
onRequestMicPermission()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePhotoCaptured(bitmap: Bitmap, controller: CameraFragment.Controller?) {
|
||||
// Convert bitmap to JPEG byte array
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream)
|
||||
val data = outputStream.toByteArray()
|
||||
|
||||
controller?.onImageCaptured(data, bitmap.width, bitmap.height)
|
||||
}
|
||||
|
||||
private fun handleVideoCaptured(result: VideoCaptureResult, controller: CameraFragment.Controller?) {
|
||||
when (result) {
|
||||
is VideoCaptureResult.Success -> {
|
||||
result.fileDescriptor?.let { parcelFd ->
|
||||
try {
|
||||
// Seek to beginning before reading
|
||||
android.system.Os.lseek(parcelFd.fileDescriptor, 0, android.system.OsConstants.SEEK_SET)
|
||||
controller?.onVideoCaptured(parcelFd.fileDescriptor)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to seek video file descriptor", e)
|
||||
controller?.onVideoCaptureError()
|
||||
}
|
||||
} ?: controller?.onVideoCaptureError()
|
||||
}
|
||||
|
||||
is VideoCaptureResult.Error -> {
|
||||
Log.w(TAG, "Video capture failed: ${result.message}", result.throwable)
|
||||
controller?.onVideoCaptureError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@androidx.compose.ui.tooling.preview.Preview(
|
||||
name = "20:9 Display",
|
||||
showBackground = true,
|
||||
widthDp = 360,
|
||||
heightDp = 800
|
||||
)
|
||||
@Composable
|
||||
private fun CameraXScreenPreview_20_9() {
|
||||
org.signal.core.ui.compose.Previews.Preview {
|
||||
CameraXScreen(
|
||||
controller = null,
|
||||
isVideoEnabled = true,
|
||||
isQrScanEnabled = false,
|
||||
captureMode = CameraCaptureMode.ImageAndVideoSimultaneous,
|
||||
controlsVisible = true,
|
||||
selectedMediaCount = 0,
|
||||
onCheckPermissions = {},
|
||||
hasCameraPermission = { true },
|
||||
onRequestMicPermission = { },
|
||||
onGalleryClicked = { },
|
||||
createVideoFileDescriptor = { null },
|
||||
getMaxVideoDurationInSeconds = { 60 },
|
||||
cameraDisplay = CameraDisplay.DISPLAY_20_9,
|
||||
storiesEnabled = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@androidx.compose.ui.tooling.preview.Preview(
|
||||
name = "19:9 Display",
|
||||
showBackground = true,
|
||||
widthDp = 360,
|
||||
heightDp = 760
|
||||
)
|
||||
@Composable
|
||||
private fun CameraXScreenPreview_19_9() {
|
||||
org.signal.core.ui.compose.Previews.Preview {
|
||||
CameraXScreen(
|
||||
controller = null,
|
||||
isVideoEnabled = true,
|
||||
isQrScanEnabled = false,
|
||||
captureMode = CameraCaptureMode.ImageAndVideoSimultaneous,
|
||||
controlsVisible = true,
|
||||
selectedMediaCount = 0,
|
||||
onCheckPermissions = {},
|
||||
hasCameraPermission = { true },
|
||||
onRequestMicPermission = { },
|
||||
onGalleryClicked = { },
|
||||
createVideoFileDescriptor = { null },
|
||||
getMaxVideoDurationInSeconds = { 60 },
|
||||
cameraDisplay = CameraDisplay.DISPLAY_19_9,
|
||||
storiesEnabled = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@androidx.compose.ui.tooling.preview.Preview(
|
||||
name = "18:9 Display",
|
||||
showBackground = true,
|
||||
widthDp = 360,
|
||||
heightDp = 720
|
||||
)
|
||||
@Composable
|
||||
private fun CameraXScreenPreview_18_9() {
|
||||
org.signal.core.ui.compose.Previews.Preview {
|
||||
CameraXScreen(
|
||||
controller = null,
|
||||
isVideoEnabled = true,
|
||||
isQrScanEnabled = false,
|
||||
captureMode = CameraCaptureMode.ImageAndVideoSimultaneous,
|
||||
controlsVisible = true,
|
||||
selectedMediaCount = 0,
|
||||
onCheckPermissions = {},
|
||||
hasCameraPermission = { true },
|
||||
onRequestMicPermission = { },
|
||||
onGalleryClicked = { },
|
||||
createVideoFileDescriptor = { null },
|
||||
getMaxVideoDurationInSeconds = { 60 },
|
||||
cameraDisplay = CameraDisplay.DISPLAY_18_9,
|
||||
storiesEnabled = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@androidx.compose.ui.tooling.preview.Preview(
|
||||
name = "16:9 Display",
|
||||
showBackground = true,
|
||||
widthDp = 360,
|
||||
heightDp = 640
|
||||
)
|
||||
@Composable
|
||||
private fun CameraXScreenPreview_16_9() {
|
||||
org.signal.core.ui.compose.Previews.Preview {
|
||||
CameraXScreen(
|
||||
controller = null,
|
||||
isVideoEnabled = true,
|
||||
isQrScanEnabled = false,
|
||||
captureMode = CameraCaptureMode.ImageAndVideoSimultaneous,
|
||||
controlsVisible = true,
|
||||
selectedMediaCount = 0,
|
||||
onCheckPermissions = {},
|
||||
hasCameraPermission = { true },
|
||||
onRequestMicPermission = { },
|
||||
onGalleryClicked = { },
|
||||
createVideoFileDescriptor = { null },
|
||||
getMaxVideoDurationInSeconds = { 60 },
|
||||
cameraDisplay = CameraDisplay.DISPLAY_16_9,
|
||||
storiesEnabled = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@androidx.compose.ui.tooling.preview.Preview(
|
||||
name = "6:5 Display (Tablet)",
|
||||
showBackground = true,
|
||||
widthDp = 480,
|
||||
heightDp = 576
|
||||
)
|
||||
@Composable
|
||||
private fun CameraXScreenPreview_6_5() {
|
||||
org.signal.core.ui.compose.Previews.Preview {
|
||||
CameraXScreen(
|
||||
controller = null,
|
||||
isVideoEnabled = true,
|
||||
isQrScanEnabled = false,
|
||||
captureMode = CameraCaptureMode.ImageAndVideoSimultaneous,
|
||||
controlsVisible = true,
|
||||
selectedMediaCount = 0,
|
||||
onCheckPermissions = {},
|
||||
hasCameraPermission = { true },
|
||||
onRequestMicPermission = { },
|
||||
onGalleryClicked = { },
|
||||
createVideoFileDescriptor = { null },
|
||||
getMaxVideoDurationInSeconds = { 60 },
|
||||
cameraDisplay = CameraDisplay.DISPLAY_6_5,
|
||||
storiesEnabled = true
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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<MediaFolder> = 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
|
||||
|
||||
+2
-2
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package org.thoughtcrime.securesms.video;
|
||||
|
||||
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 {
|
||||
|
||||
private VideoUtil() { }
|
||||
|
||||
public static int getMaxVideoRecordDurationInSeconds(@NonNull Context context, @NonNull MediaConstraints mediaConstraints) {
|
||||
long allowedSize = mediaConstraints.getCompressedVideoMaxSize(context);
|
||||
int duration = (int) Math.floor((float) allowedSize / VideoConstants.MAX_ALLOWED_BYTES_PER_SECOND);
|
||||
|
||||
return Math.min(duration, VideoConstants.VIDEO_MAX_RECORD_LENGTH_S);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user