Aperture: Implement screen flash

Co-authored-by: Asher Simonds <dayanhammer@gmail.com>

Change-Id: Icc82f2e503f4e1619909f360297fbe4264f6fa7d
diff --git a/app/src/main/java/org/lineageos/aperture/CameraActivity.kt b/app/src/main/java/org/lineageos/aperture/CameraActivity.kt
index 253cf21..dca5d2d 100644
--- a/app/src/main/java/org/lineageos/aperture/CameraActivity.kt
+++ b/app/src/main/java/org/lineageos/aperture/CameraActivity.kt
@@ -63,6 +63,7 @@
 import androidx.camera.view.CameraController
 import androidx.camera.view.LifecycleCameraController
 import androidx.camera.view.PreviewView
+import androidx.camera.view.ScreenFlashView
 import androidx.camera.view.onPinchToZoom
 import androidx.camera.view.video.AudioConfig
 import androidx.cardview.widget.CardView
@@ -168,6 +169,7 @@
     private val previewBlurView by lazy { findViewById<PreviewBlurView>(R.id.previewBlurView) }
     private val primaryBarLayout by lazy { findViewById<ConstraintLayout>(R.id.primaryBarLayout) }
     private val proButton by lazy { findViewById<ImageButton>(R.id.proButton) }
+    private val screenFlashView by lazy { findViewById<ScreenFlashView>(R.id.screenFlashView) }
     private val secondaryBottomBarLayout by lazy { findViewById<ConstraintLayout>(R.id.secondaryBottomBarLayout) }
     private val secondaryTopBarLayout by lazy { findViewById<HorizontalScrollView>(R.id.secondaryTopBarLayout) }
     private val settingsButton by lazy { findViewById<Button>(R.id.settingsButton) }
@@ -230,6 +232,9 @@
 
     private var zoomGestureMutex = Mutex()
 
+    private val supportedFlashModes: Set<FlashMode>
+        get() = cameraMode.supportedFlashModes.intersect(camera.supportedFlashModes)
+
     // Video
     private val supportedVideoQualities: Set<Quality>
         get() = camera.supportedVideoQualities.keys
@@ -656,19 +661,16 @@
         proButton.setOnClickListener {
             secondaryTopBarLayout.slide()
         }
-        flashButton.setOnClickListener { cycleFlashMode() }
-        flashButton.setOnLongClickListener {
-            if (cameraMode == CameraMode.PHOTO) {
-                toggleForceTorch()
-                true
-            } else {
-                false
-            }
-        }
+        flashButton.setOnClickListener { cycleFlashMode(false) }
+        flashButton.setOnLongClickListener { cycleFlashMode(true) }
 
         // Attach CameraController to PreviewView
         viewFinder.controller = cameraController
 
+        // Attach CameraController to ScreenFlashView
+        screenFlashView.setController(cameraController)
+        screenFlashView.setScreenFlashWindow(window)
+
         // Observe torch state
         cameraController.torchState.observe(this) {
             flashMode = cameraController.flashMode
@@ -871,11 +873,6 @@
 
         // Observe camera
         model.camera.observe(this) {
-            val camera = it ?: return@observe
-
-            // Update secondary bar buttons
-            flashButton.isVisible = camera.hasFlashUnit
-
             updateSecondaryTopBarButtons()
         }
 
@@ -949,6 +946,7 @@
                         FlashMode.AUTO -> R.drawable.ic_flash_auto
                         FlashMode.ON -> R.drawable.ic_flash_on
                         FlashMode.TORCH -> R.drawable.ic_flash_torch
+                        FlashMode.SCREEN -> R.drawable.ic_flash_screen
                     }
                 )
             )
@@ -1816,7 +1814,7 @@
                 CameraMode.PHOTO -> sharedPreferences.photoFlashMode
                 CameraMode.VIDEO -> sharedPreferences.videoFlashMode
                 CameraMode.QR -> FlashMode.OFF
-            }
+            }.takeIf { supportedFlashModes.contains(it) } ?: FlashMode.OFF
         )
         setMicrophoneMode(videoMicMode)
 
@@ -1925,8 +1923,16 @@
             val supportedVideoFrameRates = videoQualityInfo?.supportedFrameRates ?: setOf()
             val supportedVideoDynamicRanges = videoQualityInfo?.supportedDynamicRanges ?: setOf()
 
+            val supportedFlashModes = cameraMode.supportedFlashModes.intersect(
+                camera.supportedFlashModes
+            )
+
+            // Hide the button if the only available mode is off,
+            // we want the user to know if any other mode is being used
+            flashButton.isVisible =
+                supportedFlashModes.size != 1 || supportedFlashModes.first() != FlashMode.OFF
             flashButton.isEnabled =
-                cameraMode != CameraMode.PHOTO || cameraState == CameraState.IDLE
+                cameraState == CameraState.IDLE || cameraMode == CameraMode.VIDEO
             effectButton.isVisible = cameraMode == CameraMode.PHOTO &&
                     photoCaptureMode != ImageCapture.CAPTURE_MODE_ZERO_SHUTTER_LAG &&
                     camera.supportedExtensionModes.size > 1
@@ -2070,49 +2076,51 @@
 
     /**
      * Cycle flash mode
+     * @param forceTorch Whether force torch mode should be toggled
+     * @return false if called with an unsupported configuration, true otherwise
      */
-    private fun cycleFlashMode() {
+    private fun cycleFlashMode(forceTorch: Boolean): Boolean {
+        // Long-press is supported only on photo mode and if torch mode is available
+        val forceTorchAvailable = cameraMode == CameraMode.PHOTO
+                && camera.supportedFlashModes.contains(FlashMode.TORCH)
+        if (forceTorch && !forceTorchAvailable) {
+            return false
+        }
+
         val currentFlashMode = flashMode
 
-        when (cameraMode) {
-            CameraMode.PHOTO -> FlashMode.PHOTO_ALLOWED_MODES.next(currentFlashMode)
-            CameraMode.VIDEO -> FlashMode.VIDEO_ALLOWED_MODES.next(currentFlashMode)
-            else -> FlashMode.OFF
+        when (forceTorch) {
+            true -> when (currentFlashMode) {
+                FlashMode.TORCH -> sharedPreferences.photoFlashMode.takeIf {
+                    supportedFlashModes.contains(it)
+                } ?: FlashMode.OFF
+                else -> FlashMode.TORCH
+            }
+
+            else -> supportedFlashModes.toList().next(currentFlashMode)
         }?.let {
             changeFlashMode(it)
 
-            when (cameraMode) {
-                CameraMode.PHOTO -> sharedPreferences.photoFlashMode = it
-                CameraMode.VIDEO -> sharedPreferences.videoFlashMode = it
-                else -> {}
+            if (!forceTorch) {
+                when (cameraMode) {
+                    CameraMode.PHOTO -> sharedPreferences.photoFlashMode = it
+                    CameraMode.VIDEO -> sharedPreferences.videoFlashMode = it
+                    else -> {}
+                }
             }
         }
 
-        if (cameraMode == CameraMode.PHOTO && !sharedPreferences.forceTorchHelpShown &&
-            !forceTorchSnackbar.isShownOrQueued
-        ) {
-            forceTorchSnackbar.show()
-        }
-    }
-
-    /**
-     * Toggle torch mode on photo mode.
-     */
-    private fun toggleForceTorch() {
-        val currentFlashMode = flashMode
-
-        val newFlashMode = if (currentFlashMode != FlashMode.TORCH) {
-            FlashMode.TORCH
-        } else {
-            sharedPreferences.photoFlashMode
-        }
-
-        changeFlashMode(newFlashMode)
-
+        // Check if we should show the force torch suggestion
         if (!sharedPreferences.forceTorchHelpShown) {
-            // The user figured it out by themself
-            sharedPreferences.forceTorchHelpShown = true
+            if (forceTorch) {
+                // The user figured it out by themself
+                sharedPreferences.forceTorchHelpShown = true
+            } else if (!forceTorchSnackbar.isShownOrQueued) {
+                forceTorchSnackbar.show()
+            }
         }
+
+        return true
     }
 
     /**
diff --git a/app/src/main/java/org/lineageos/aperture/camera/Camera.kt b/app/src/main/java/org/lineageos/aperture/camera/Camera.kt
index f8cad35..2a0e4e5 100644
--- a/app/src/main/java/org/lineageos/aperture/camera/Camera.kt
+++ b/app/src/main/java/org/lineageos/aperture/camera/Camera.kt
@@ -18,6 +18,7 @@
 import org.lineageos.aperture.models.ColorCorrectionAberrationMode
 import org.lineageos.aperture.models.DistortionCorrectionMode
 import org.lineageos.aperture.models.EdgeMode
+import org.lineageos.aperture.models.FlashMode
 import org.lineageos.aperture.models.FrameRate
 import org.lineageos.aperture.models.HotPixelMode
 import org.lineageos.aperture.models.NoiseReductionMode
@@ -50,7 +51,7 @@
     val cameraType = cameraFacing.cameraType
 
     val exposureCompensationRange = cameraInfo.exposureState.exposureCompensationRange
-    val hasFlashUnit = cameraInfo.hasFlashUnit()
+    private val hasFlashUnit = cameraInfo.hasFlashUnit()
 
     val isLogical = camera2CameraInfo.physicalCameraIds.size > 1
 
@@ -234,6 +235,24 @@
         }
     }.toSet()
 
+    /**
+     * The supported flash modes of this camera.
+     * Keep in mind that support also depends on the camera mode used.
+     */
+    val supportedFlashModes = mutableSetOf(
+        FlashMode.OFF,
+    ).apply {
+        if (hasFlashUnit) {
+            add(FlashMode.AUTO)
+            add(FlashMode.ON)
+            add(FlashMode.TORCH)
+        }
+
+        if (cameraFacing == CameraFacing.FRONT) {
+            add(FlashMode.SCREEN)
+        }
+    }
+
     override fun equals(other: Any?): Boolean {
         val camera = this::class.safeCast(other) ?: return false
         return this.cameraId == camera.cameraId
diff --git a/app/src/main/java/org/lineageos/aperture/ext/CameraController.kt b/app/src/main/java/org/lineageos/aperture/ext/CameraController.kt
index fee0ddd..d84c52c 100644
--- a/app/src/main/java/org/lineageos/aperture/ext/CameraController.kt
+++ b/app/src/main/java/org/lineageos/aperture/ext/CameraController.kt
@@ -18,6 +18,7 @@
             ImageCapture.FLASH_MODE_AUTO -> FlashMode.AUTO
             ImageCapture.FLASH_MODE_ON -> FlashMode.ON
             ImageCapture.FLASH_MODE_OFF -> FlashMode.OFF
+            ImageCapture.FLASH_MODE_SCREEN -> FlashMode.SCREEN
             else -> throw Exception("Invalid flash mode")
         }
     }
@@ -29,6 +30,7 @@
             FlashMode.AUTO -> ImageCapture.FLASH_MODE_AUTO
             FlashMode.ON -> ImageCapture.FLASH_MODE_ON
             FlashMode.TORCH -> ImageCapture.FLASH_MODE_OFF
+            FlashMode.SCREEN -> ImageCapture.FLASH_MODE_SCREEN
         }
     }
 
diff --git a/app/src/main/java/org/lineageos/aperture/ext/SharedPreferences.kt b/app/src/main/java/org/lineageos/aperture/ext/SharedPreferences.kt
index 209e811..ea45ef6 100644
--- a/app/src/main/java/org/lineageos/aperture/ext/SharedPreferences.kt
+++ b/app/src/main/java/org/lineageos/aperture/ext/SharedPreferences.kt
@@ -160,6 +160,7 @@
         "auto" -> FlashMode.AUTO
         "on" -> FlashMode.ON
         "torch" -> FlashMode.TORCH
+        "screen" -> FlashMode.SCREEN
         // Default to auto
         else -> FlashMode.AUTO
     }
@@ -170,6 +171,7 @@
                 FlashMode.AUTO -> "auto"
                 FlashMode.ON -> "on"
                 FlashMode.TORCH -> "torch"
+                FlashMode.SCREEN -> "screen"
             }
         )
     }
diff --git a/app/src/main/java/org/lineageos/aperture/models/CameraMode.kt b/app/src/main/java/org/lineageos/aperture/models/CameraMode.kt
index 950e579..a8568ea 100644
--- a/app/src/main/java/org/lineageos/aperture/models/CameraMode.kt
+++ b/app/src/main/java/org/lineageos/aperture/models/CameraMode.kt
@@ -8,8 +8,25 @@
 import androidx.annotation.StringRes
 import org.lineageos.aperture.R
 
-enum class CameraMode(@StringRes val title: Int) {
-    PHOTO(R.string.camera_mode_photo),
-    VIDEO(R.string.camera_mode_video),
+enum class CameraMode(
+    @StringRes val title: Int,
+    val supportedFlashModes: Set<FlashMode> = setOf(FlashMode.OFF),
+) {
+    PHOTO(
+        R.string.camera_mode_photo,
+        setOf(
+            FlashMode.OFF,
+            FlashMode.AUTO,
+            FlashMode.ON,
+            FlashMode.SCREEN,
+        ),
+    ),
+    VIDEO(
+        R.string.camera_mode_video,
+        setOf(
+            FlashMode.OFF,
+            FlashMode.TORCH,
+        ),
+    ),
     QR(R.string.camera_mode_qr),
 }
diff --git a/app/src/main/java/org/lineageos/aperture/models/FlashMode.kt b/app/src/main/java/org/lineageos/aperture/models/FlashMode.kt
index a8c8525..83dd2e5 100644
--- a/app/src/main/java/org/lineageos/aperture/models/FlashMode.kt
+++ b/app/src/main/java/org/lineageos/aperture/models/FlashMode.kt
@@ -24,24 +24,11 @@
     /**
      * Constant emission of light during preview, auto-focus and snapshot.
      */
-    TORCH;
+    TORCH,
 
-    companion object {
-        /**
-         * Allowed flash modes when in photo mode.
-         */
-        val PHOTO_ALLOWED_MODES = listOf(
-            OFF,
-            AUTO,
-            ON,
-        )
-
-        /**
-         * Allowed flash modes when in video mode.
-         */
-        val VIDEO_ALLOWED_MODES = listOf(
-            OFF,
-            TORCH,
-        )
-    }
+    /**
+     * Display screen brightness will be used as alternative to flash when taking a picture with
+     * front camera.
+     */
+    SCREEN,
 }
diff --git a/app/src/main/res/drawable/ic_flash_screen.xml b/app/src/main/res/drawable/ic_flash_screen.xml
new file mode 100644
index 0000000..081de98
--- /dev/null
+++ b/app/src/main/res/drawable/ic_flash_screen.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     SPDX-FileCopyrightText: 2024 The LineageOS Project
+     SPDX-License-Identifier: Apache-2.0
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="#000000"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="@android:color/white"
+        android:fillType="evenOdd"
+        android:pathData="M7,23C6.45,23 5.979,22.804 5.588,22.413C5.196,22.021 5,21.55 5,21V3C5,2.45 5.196,1.979 5.588,1.587C5.979,1.196 6.45,1 7,1H14V3H7V15.2C7.75,14.817 8.546,14.521 9.387,14.313C10.229,14.104 11.1,14 12,14C12.9,14 13.771,14.104 14.613,14.313C15.454,14.521 16.25,14.817 17,15.2V14H19V21C19,21.55 18.804,22.021 18.413,22.413C18.021,22.804 17.55,23 17,23H7ZM12,16C11.1,16 10.225,16.129 9.375,16.388C8.525,16.646 7.733,17.017 7,17.5V21H17V17.5C16.267,17.017 15.475,16.646 14.625,16.388C13.775,16.129 12.9,16 12,16ZM12,13C12.833,13 13.542,12.708 14.125,12.125C14.708,11.542 15,10.833 15,10C15,9.167 14.708,8.458 14.125,7.875C13.542,7.292 12.833,7 12,7C11.167,7 10.458,7.292 9.875,7.875C9.292,8.458 9,9.167 9,10C9,10.833 9.292,11.542 9.875,12.125C10.458,12.708 11.167,13 12,13ZM12,11C11.717,11 11.479,10.904 11.288,10.712C11.096,10.521 11,10.283 11,10C11,9.717 11.096,9.479 11.288,9.288C11.479,9.096 11.717,9 12,9C12.283,9 12.521,9.096 12.712,9.288C12.904,9.479 13,9.717 13,10C13,10.283 12.904,10.521 12.712,10.712C12.521,10.904 12.283,11 12,11ZM16,7.417V1H21.25L19.5,5.667H21.833L17.75,12.667V7.417H16Z" />
+</vector>
diff --git a/app/src/main/res/layout/activity_camera.xml b/app/src/main/res/layout/activity_camera.xml
index 9d9ca5a..18bee78 100644
--- a/app/src/main/res/layout/activity_camera.xml
+++ b/app/src/main/res/layout/activity_camera.xml
@@ -370,6 +370,15 @@
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent" />
 
+    <androidx.camera.view.ScreenFlashView
+        android:id="@+id/screenFlashView"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
     <include
         android:id="@+id/capturePreviewLayout"
         layout="@layout/capture_preview_layout"