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);
}