diff --git a/res/drawable-hdpi/flash_auto_32.webp b/res/drawable-hdpi/flash_auto_32.webp new file mode 100644 index 0000000000..b2f9129a9a Binary files /dev/null and b/res/drawable-hdpi/flash_auto_32.webp differ diff --git a/res/drawable-hdpi/flash_off_32.webp b/res/drawable-hdpi/flash_off_32.webp new file mode 100644 index 0000000000..83133ae838 Binary files /dev/null and b/res/drawable-hdpi/flash_off_32.webp differ diff --git a/res/drawable-hdpi/flash_on_32.webp b/res/drawable-hdpi/flash_on_32.webp new file mode 100644 index 0000000000..65a6ee2092 Binary files /dev/null and b/res/drawable-hdpi/flash_on_32.webp differ diff --git a/res/drawable-mdpi/flash_auto_32.webp b/res/drawable-mdpi/flash_auto_32.webp new file mode 100644 index 0000000000..b92093f095 Binary files /dev/null and b/res/drawable-mdpi/flash_auto_32.webp differ diff --git a/res/drawable-mdpi/flash_off_32.webp b/res/drawable-mdpi/flash_off_32.webp new file mode 100644 index 0000000000..5751024aa7 Binary files /dev/null and b/res/drawable-mdpi/flash_off_32.webp differ diff --git a/res/drawable-mdpi/flash_on_32.webp b/res/drawable-mdpi/flash_on_32.webp new file mode 100644 index 0000000000..386de74f1c Binary files /dev/null and b/res/drawable-mdpi/flash_on_32.webp differ diff --git a/res/drawable-xhdpi/flash_auto_32.webp b/res/drawable-xhdpi/flash_auto_32.webp new file mode 100644 index 0000000000..7b87a058e6 Binary files /dev/null and b/res/drawable-xhdpi/flash_auto_32.webp differ diff --git a/res/drawable-xhdpi/flash_off_32.webp b/res/drawable-xhdpi/flash_off_32.webp new file mode 100644 index 0000000000..649fd43249 Binary files /dev/null and b/res/drawable-xhdpi/flash_off_32.webp differ diff --git a/res/drawable-xhdpi/flash_on_32.webp b/res/drawable-xhdpi/flash_on_32.webp new file mode 100644 index 0000000000..f12dd80959 Binary files /dev/null and b/res/drawable-xhdpi/flash_on_32.webp differ diff --git a/res/drawable-xxhdpi/flash_auto_32.webp b/res/drawable-xxhdpi/flash_auto_32.webp new file mode 100644 index 0000000000..39aeefa3b9 Binary files /dev/null and b/res/drawable-xxhdpi/flash_auto_32.webp differ diff --git a/res/drawable-xxhdpi/flash_off_32.webp b/res/drawable-xxhdpi/flash_off_32.webp new file mode 100644 index 0000000000..9147665a9f Binary files /dev/null and b/res/drawable-xxhdpi/flash_off_32.webp differ diff --git a/res/drawable-xxhdpi/flash_on_32.webp b/res/drawable-xxhdpi/flash_on_32.webp new file mode 100644 index 0000000000..d1d8424b83 Binary files /dev/null and b/res/drawable-xxhdpi/flash_on_32.webp differ diff --git a/res/drawable-xxxhdpi/flash_auto_32.webp b/res/drawable-xxxhdpi/flash_auto_32.webp new file mode 100644 index 0000000000..f59bc30556 Binary files /dev/null and b/res/drawable-xxxhdpi/flash_auto_32.webp differ diff --git a/res/drawable-xxxhdpi/flash_off_32.webp b/res/drawable-xxxhdpi/flash_off_32.webp new file mode 100644 index 0000000000..ab1edbeef0 Binary files /dev/null and b/res/drawable-xxxhdpi/flash_off_32.webp differ diff --git a/res/drawable-xxxhdpi/flash_on_32.webp b/res/drawable-xxxhdpi/flash_on_32.webp new file mode 100644 index 0000000000..a2d36e2258 Binary files /dev/null and b/res/drawable-xxxhdpi/flash_on_32.webp differ diff --git a/res/drawable/camerax_flash_toggle.xml b/res/drawable/camerax_flash_toggle.xml new file mode 100644 index 0000000000..4831029918 --- /dev/null +++ b/res/drawable/camerax_flash_toggle.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/res/layout/camera_controls_landscape.xml b/res/layout/camera_controls_landscape.xml index bb044eb722..8cf4313c6f 100644 --- a/res/layout/camera_controls_landscape.xml +++ b/res/layout/camera_controls_landscape.xml @@ -17,6 +17,16 @@ app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent"/> + + + + diff --git a/res/layout/camera_controls_portrait.xml b/res/layout/camera_controls_portrait.xml index e49f3b1a15..e2af5dada5 100644 --- a/res/layout/camera_controls_portrait.xml +++ b/res/layout/camera_controls_portrait.xml @@ -17,6 +17,16 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> + + + + diff --git a/res/values/attrs.xml b/res/values/attrs.xml index c2ed44653b..1dd75c9abf 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -362,4 +362,10 @@ + + + + + + diff --git a/src/org/thoughtcrime/securesms/mediasend/CameraXFragment.java b/src/org/thoughtcrime/securesms/mediasend/CameraXFragment.java index 129d7951c0..5220df679a 100644 --- a/src/org/thoughtcrime/securesms/mediasend/CameraXFragment.java +++ b/src/org/thoughtcrime/securesms/mediasend/CameraXFragment.java @@ -29,6 +29,7 @@ import com.bumptech.glide.Glide; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.mediasend.camerax.CameraXFlashToggleView; import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil; import org.thoughtcrime.securesms.mediasend.camerax.CameraXView; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; @@ -52,6 +53,7 @@ public class CameraXFragment extends Fragment implements CameraFragment { private ViewGroup controlsContainer; private Controller controller; private MediaSendViewModel viewModel; + private View selfieFlash; public static CameraXFragment newInstance() { return new CameraXFragment(); @@ -160,14 +162,18 @@ public class CameraXFragment extends Fragment implements CameraFragment { @SuppressLint({"ClickableViewAccessibility", "MissingPermission"}) private void initControls() { - View flipButton = requireView().findViewById(R.id.camera_flip_button); - View captureButton = requireView().findViewById(R.id.camera_capture_button); - View galleryButton = requireView().findViewById(R.id.camera_gallery_button); - View countButton = requireView().findViewById(R.id.camera_count_button); + View flipButton = requireView().findViewById(R.id.camera_flip_button); + View captureButton = requireView().findViewById(R.id.camera_capture_button); + View galleryButton = requireView().findViewById(R.id.camera_gallery_button); + View countButton = requireView().findViewById(R.id.camera_count_button); + CameraXFlashToggleView flashButton = requireView().findViewById(R.id.camera_flash_button); + + selfieFlash = requireView().findViewById(R.id.camera_selfie_flash); captureButton.setOnClickListener(v -> { captureButton.setEnabled(false); flipButton.setEnabled(false); + flashButton.setEnabled(false); onCaptureClicked(); }); @@ -181,6 +187,8 @@ public class CameraXFragment extends Fragment implements CameraFragment { animation.setDuration(200); animation.setInterpolator(new DecelerateInterpolator()); flipButton.startAnimation(animation); + flashButton.setAutoFlashEnabled(camera.hasFlash()); + flashButton.setFlash(camera.getFlash()); }); GestureDetector gestureDetector = new GestureDetector(requireContext(), new GestureDetector.SimpleOnGestureListener() { @@ -199,6 +207,10 @@ public class CameraXFragment extends Fragment implements CameraFragment { flipButton.setVisibility(View.GONE); } + flashButton.setAutoFlashEnabled(camera.hasFlash()); + flashButton.setFlash(camera.getFlash()); + flashButton.setOnFlashModeChangedListener(camera::setFlash); + galleryButton.setOnClickListener(v -> controller.onGalleryClicked()); countButton.setOnClickListener(v -> controller.onCameraCountButtonClicked()); @@ -208,9 +220,17 @@ public class CameraXFragment extends Fragment implements CameraFragment { private void onCaptureClicked() { Stopwatch stopwatch = new Stopwatch("Capture"); + CameraXSelfieFlashHelper flashHelper = new CameraXSelfieFlashHelper( + requireActivity().getWindow(), + camera, + selfieFlash + ); + camera.takePicture(new ImageCapture.OnImageCapturedListener() { @Override public void onCaptureSuccess(ImageProxy image, int rotationDegrees) { + flashHelper.endFlash(); + SimpleTask.run(CameraXFragment.this.getLifecycle(), () -> { stopwatch.split("captured"); try { @@ -234,8 +254,11 @@ public class CameraXFragment extends Fragment implements CameraFragment { @Override public void onError(ImageCapture.UseCaseError useCaseError, String message, @Nullable Throwable cause) { + flashHelper.endFlash(); controller.onCameraError(); } }); + + flashHelper.startFlash(); } } diff --git a/src/org/thoughtcrime/securesms/mediasend/CameraXSelfieFlashHelper.java b/src/org/thoughtcrime/securesms/mediasend/CameraXSelfieFlashHelper.java new file mode 100644 index 0000000000..aa88dfb3bc --- /dev/null +++ b/src/org/thoughtcrime/securesms/mediasend/CameraXSelfieFlashHelper.java @@ -0,0 +1,72 @@ +package org.thoughtcrime.securesms.mediasend; + +import android.view.View; +import android.view.Window; +import android.view.WindowManager; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.camera.core.CameraX; +import androidx.camera.core.FlashMode; + +import org.thoughtcrime.securesms.mediasend.camerax.CameraXView; + +@RequiresApi(21) +final class CameraXSelfieFlashHelper { + + private static final float MAX_SCREEN_BRIGHTNESS = 1f; + private static final float MAX_SELFIE_FLASH_ALPHA = 0.75f; + private static final long SELFIE_FLASH_DURATION_MS = 250; + + private final Window window; + private final CameraXView camera; + private final View selfieFlash; + + private float brightnessBeforeFlash; + private boolean inFlash; + + CameraXSelfieFlashHelper(@NonNull Window window, + @NonNull CameraXView camera, + @NonNull View selfieFlash) + { + this.window = window; + this.camera = camera; + this.selfieFlash = selfieFlash; + } + + void startFlash() { + if (inFlash || !shouldUseViewBasedFlash()) return; + inFlash = true; + + WindowManager.LayoutParams params = window.getAttributes(); + + brightnessBeforeFlash = params.screenBrightness; + params.screenBrightness = MAX_SCREEN_BRIGHTNESS; + window.setAttributes(params); + + selfieFlash.animate() + .alpha(MAX_SELFIE_FLASH_ALPHA) + .setDuration(SELFIE_FLASH_DURATION_MS); + } + + void endFlash() { + if (!inFlash) return; + + WindowManager.LayoutParams params = window.getAttributes(); + + params.screenBrightness = brightnessBeforeFlash; + window.setAttributes(params); + + selfieFlash.animate() + .alpha(0f) + .setDuration(SELFIE_FLASH_DURATION_MS); + + inFlash = false; + } + + private boolean shouldUseViewBasedFlash() { + return camera.getFlash() == FlashMode.ON && + !camera.hasFlash() && + camera.getCameraLensFacing() == CameraX.LensFacing.FRONT; + } +} diff --git a/src/org/thoughtcrime/securesms/mediasend/camerax/CameraXFlashToggleView.java b/src/org/thoughtcrime/securesms/mediasend/camerax/CameraXFlashToggleView.java new file mode 100644 index 0000000000..588b44e45a --- /dev/null +++ b/src/org/thoughtcrime/securesms/mediasend/camerax/CameraXFlashToggleView.java @@ -0,0 +1,131 @@ +package org.thoughtcrime.securesms.mediasend.camerax; + +import android.content.Context; +import android.os.Bundle; +import android.os.Parcelable; +import android.util.AttributeSet; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatImageView; +import androidx.camera.core.FlashMode; + +import org.thoughtcrime.securesms.R; + +import java.util.Arrays; +import java.util.List; + +public final class CameraXFlashToggleView extends AppCompatImageView { + + private static final String STATE_FLASH_INDEX = "flash.toggle.state.flash.index"; + private static final String STATE_SUPPORT_AUTO = "flash.toggle.state.support.auto"; + private static final String STATE_PARENT = "flash.toggle.state.parent"; + + private static final int[] FLASH_AUTO = { R.attr.state_flash_auto }; + private static final int[] FLASH_OFF = { R.attr.state_flash_off }; + private static final int[] FLASH_ON = { R.attr.state_flash_on }; + private static final int[][] FLASH_ENUM = { FLASH_AUTO, FLASH_OFF, FLASH_ON }; + private static final List FLASH_MODES = Arrays.asList(FlashMode.AUTO, FlashMode.OFF, FlashMode.ON); + private static final FlashMode FLASH_FALLBACK = FlashMode.OFF; + + private boolean supportsFlashModeAuto = true; + private int flashIndex; + private OnFlashModeChangedListener flashModeChangedListener; + + public CameraXFlashToggleView(Context context) { + this(context, null); + } + + public CameraXFlashToggleView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public CameraXFlashToggleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + super.setOnClickListener((v) -> setFlash(FLASH_MODES.get((flashIndex + 1) % FLASH_ENUM.length))); + } + + @Override + public int[] onCreateDrawableState(int extraSpace) { + final int[] extra = FLASH_ENUM[flashIndex]; + final int[] drawableState = super.onCreateDrawableState(extraSpace + extra.length); + mergeDrawableStates(drawableState, extra); + return drawableState; + } + + @Override + public void setOnClickListener(@Nullable OnClickListener l) { + throw new IllegalStateException("This View does not support custom click listeners."); + } + + public void setAutoFlashEnabled(boolean isAutoEnabled) { + supportsFlashModeAuto = isAutoEnabled; + setFlash(FLASH_MODES.get(flashIndex)); + } + + public void setFlash(@NonNull FlashMode flashMode) { + flashIndex = resolveFlashIndex(FLASH_MODES.indexOf(flashMode), supportsFlashModeAuto); + refreshDrawableState(); + notifyListener(); + } + + public void setOnFlashModeChangedListener(@Nullable OnFlashModeChangedListener listener) { + this.flashModeChangedListener = listener; + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable parentState = super.onSaveInstanceState(); + Bundle bundle = new Bundle(); + + bundle.putParcelable(STATE_PARENT, parentState); + bundle.putInt(STATE_FLASH_INDEX, flashIndex); + bundle.putBoolean(STATE_SUPPORT_AUTO, supportsFlashModeAuto); + return bundle; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (state instanceof Bundle) { + Bundle savedState = (Bundle) state; + + supportsFlashModeAuto = savedState.getBoolean(STATE_SUPPORT_AUTO); + setFlash(FLASH_MODES.get( + resolveFlashIndex(savedState.getInt(STATE_FLASH_INDEX), supportsFlashModeAuto)) + ); + + super.onRestoreInstanceState(savedState.getParcelable(STATE_PARENT)); + } else { + super.onRestoreInstanceState(state); + } + } + + private void notifyListener() { + if (flashModeChangedListener == null) return; + + flashModeChangedListener.flashModeChanged(FLASH_MODES.get(flashIndex)); + } + + private static int resolveFlashIndex(int desiredFlashIndex, boolean supportsFlashModeAuto) { + if (isIllegalFlashIndex(desiredFlashIndex)) { + throw new IllegalArgumentException("Unsupported index: " + desiredFlashIndex); + } + if (isUnsupportedFlashMode(desiredFlashIndex, supportsFlashModeAuto)) { + return FLASH_MODES.indexOf(FLASH_FALLBACK); + } + return desiredFlashIndex; + } + + private static boolean isIllegalFlashIndex(int desiredFlashIndex) { + return desiredFlashIndex < 0 || desiredFlashIndex > FLASH_ENUM.length; + } + + private static boolean isUnsupportedFlashMode(int desiredFlashIndex, boolean supportsFlashModeAuto) { + return FLASH_MODES.get(desiredFlashIndex) == FlashMode.AUTO && !supportsFlashModeAuto; + } + + public interface OnFlashModeChangedListener { + void flashModeChanged(FlashMode flashMode); + } +} diff --git a/src/org/thoughtcrime/securesms/mediasend/camerax/CameraXModule.java b/src/org/thoughtcrime/securesms/mediasend/camerax/CameraXModule.java index e486a506ae..4655350358 100644 --- a/src/org/thoughtcrime/securesms/mediasend/camerax/CameraXModule.java +++ b/src/org/thoughtcrime/securesms/mediasend/camerax/CameraXModule.java @@ -686,6 +686,16 @@ final class CameraXModule { mImageCapture.setFlashMode(flash); } + public boolean hasFlash() { + try { + Boolean flashInfoAvailable = mCameraManager.getCameraCharacteristics(getActiveCamera()) + .get(CameraCharacteristics.FLASH_INFO_AVAILABLE); + return flashInfoAvailable == Boolean.TRUE; + } catch (CameraInfoUnavailableException | CameraAccessException e) { + return false; + } + } + public void enableTorch(boolean torch) { if (mPreview == null) { return; diff --git a/src/org/thoughtcrime/securesms/mediasend/camerax/CameraXView.java b/src/org/thoughtcrime/securesms/mediasend/camerax/CameraXView.java index 9a6d74c534..1b69c70464 100644 --- a/src/org/thoughtcrime/securesms/mediasend/camerax/CameraXView.java +++ b/src/org/thoughtcrime/securesms/mediasend/camerax/CameraXView.java @@ -683,6 +683,10 @@ public final class CameraXView extends ViewGroup { mCameraModule.setFlash(flashMode); } + public boolean hasFlash() { + return mCameraModule.hasFlash(); + } + private int getRelativeCameraOrientation(boolean compensateForMirroring) { return mCameraModule.getRelativeCameraOrientation(compensateForMirroring); }