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"