Aperture: Move camera manager to the view model
Totally fine to use the application context, that's what CameraX wants
to use anyway
Change-Id: I3ee08ff5a89bd54799f63a7e49bf447a76102693
diff --git a/app/src/main/java/org/lineageos/aperture/CameraActivity.kt b/app/src/main/java/org/lineageos/aperture/CameraActivity.kt
index bc22892..8228054 100644
--- a/app/src/main/java/org/lineageos/aperture/CameraActivity.kt
+++ b/app/src/main/java/org/lineageos/aperture/CameraActivity.kt
@@ -98,7 +98,6 @@
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.withContext
-import org.lineageos.aperture.camera.CameraManager
import org.lineageos.aperture.ext.*
import org.lineageos.aperture.models.AssistantIntent
import org.lineageos.aperture.models.CameraFacing
@@ -147,7 +146,6 @@
import java.io.ByteArrayOutputStream
import java.io.FileNotFoundException
import java.io.InputStream
-import java.util.concurrent.ExecutorService
import kotlin.math.abs
import kotlin.reflect.safeCast
import androidx.camera.core.CameraState as CameraXCameraState
@@ -155,6 +153,9 @@
@androidx.camera.camera2.interop.ExperimentalCamera2Interop
@androidx.camera.core.ExperimentalZeroShutterLag
open class CameraActivity : AppCompatActivity() {
+ // View models
+ private val model: CameraViewModel by viewModels()
+
// Views
private val aspectRatioButton by lazy { findViewById<Button>(R.id.aspectRatioButton) }
private val cameraModeSelectorLayout by lazy { findViewById<CameraModeSelectorLayout>(R.id.cameraModeSelectorLayout) }
@@ -198,20 +199,13 @@
private val powerManager by lazy { getSystemService(PowerManager::class.java) }
// Core camera utils
- private lateinit var cameraManager: CameraManager
- private val cameraController: LifecycleCameraController
- get() = cameraManager.cameraController
- private val cameraExecutor: ExecutorService
- get() = cameraManager.cameraExecutor
+ private lateinit var cameraController: LifecycleCameraController
private lateinit var cameraSoundsUtils: CameraSoundsUtils
private val sharedPreferences by lazy {
PreferenceManager.getDefaultSharedPreferences(this)
}
private val permissionsUtils by lazy { PermissionsUtils(this) }
- // Current camera state
- private val model: CameraViewModel by viewModels()
-
private var camera by nonNullablePropertyDelegate { model.camera }
private var cameraMode by nonNullablePropertyDelegate { model.cameraMode }
private var singleCaptureMode by nonNullablePropertyDelegate { model.inSingleCaptureMode }
@@ -563,8 +557,8 @@
// Register shortcuts
ShortcutsUtils.registerShortcuts(this)
- // Initialize camera manager
- cameraManager = CameraManager(this)
+ // Initialize the camera controller
+ cameraController = LifecycleCameraController(this)
// Initialize sounds utils
cameraSoundsUtils = CameraSoundsUtils(sharedPreferences)
@@ -604,7 +598,7 @@
}
}
- if (cameraMode == CameraMode.VIDEO && !cameraManager.videoRecordingAvailable()) {
+ if (cameraMode == CameraMode.VIDEO && !model.videoRecordingAvailable()) {
// If an app asked for a video we have to bail out
if (singleCaptureMode) {
Toast.makeText(
@@ -618,7 +612,7 @@
}
// Select a camera
- camera = cameraManager.getCameraOfFacingOrFirstAvailable(
+ camera = model.getCameraOfFacingOrFirstAvailable(
initialCameraFacing, cameraMode
) ?: run {
noCamera()
@@ -1174,8 +1168,9 @@
}
override fun onDestroy() {
+ model.shutdown()
+
super.onDestroy()
- cameraManager.shutdown()
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
@@ -1416,7 +1411,7 @@
videoRecording = cameraController.startRecording(
outputOptions,
videoAudioConfig,
- cameraExecutor
+ model.cameraExecutor
) {
when (it) {
is VideoRecordEvent.Start -> runOnUiThread {
@@ -1483,7 +1478,7 @@
// Get the desired camera
camera = when (cameraMode) {
- CameraMode.QR -> cameraManager.getCameraOfFacingOrFirstAvailable(
+ CameraMode.QR -> model.getCameraOfFacingOrFirstAvailable(
CameraFacing.BACK, cameraMode
)
@@ -1496,7 +1491,7 @@
// If the current camera doesn't support the selected camera mode
// pick a different one, giving priority to camera facing
if (!camera.supportsCameraMode(cameraMode)) {
- camera = cameraManager.getCameraOfFacingOrFirstAvailable(
+ camera = model.getCameraOfFacingOrFirstAvailable(
camera.cameraFacing, cameraMode
) ?: run {
noCamera()
@@ -1512,7 +1507,7 @@
// Initialize the use case we want and set its properties
val cameraUseCases = when (cameraMode) {
CameraMode.QR -> {
- cameraController.setImageAnalysisAnalyzer(cameraExecutor, imageAnalyzer)
+ cameraController.setImageAnalysisAnalyzer(model.cameraExecutor, imageAnalyzer)
CameraController.IMAGE_ANALYSIS
}
@@ -1524,7 +1519,7 @@
)
)
.setAllowedResolutionMode(
- if (cameraManager.overlayConfiguration.enableHighResolution) {
+ if (model.overlayConfiguration.enableHighResolution) {
ResolutionSelector.PREFER_HIGHER_RESOLUTION_OVER_CAPTURE_RATE
} else {
ResolutionSelector.PREFER_CAPTURE_RATE_OVER_HIGHER_RESOLUTION
@@ -1576,7 +1571,7 @@
cameraMode == CameraMode.PHOTO &&
photoCaptureMode != ImageCapture.CAPTURE_MODE_ZERO_SHUTTER_LAG
) {
- cameraManager.extensionsManager.getExtensionEnabledCameraSelector(
+ model.extensionsManager.getExtensionEnabledCameraSelector(
camera.cameraSelector, photoEffect
)
} else {
@@ -1799,7 +1794,7 @@
// Update lens selector
lensSelectorLayout.setCamera(
- camera, cameraManager.getCameras(cameraMode, camera.cameraFacing)
+ camera, model.getCameras(cameraMode, camera.cameraFacing)
)
}
@@ -1825,7 +1820,7 @@
}
CameraMode.VIDEO -> {
- if (!cameraManager.videoRecordingAvailable()) {
+ if (!model.videoRecordingAvailable()) {
Snackbar.make(
cameraModeSelectorLayout,
R.string.camcorder_unsupported_toast,
@@ -1869,7 +1864,7 @@
(flipCameraButton.drawable as AnimatedVectorDrawable).start()
- camera = cameraManager.getNextCamera(camera, cameraMode) ?: run {
+ camera = model.getNextCamera(camera, cameraMode) ?: run {
noCamera()
return
}
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 2a0e4e5..2c820b0 100644
--- a/app/src/main/java/org/lineageos/aperture/camera/Camera.kt
+++ b/app/src/main/java/org/lineageos/aperture/camera/Camera.kt
@@ -26,6 +26,7 @@
import org.lineageos.aperture.models.VideoDynamicRange
import org.lineageos.aperture.models.VideoQualityInfo
import org.lineageos.aperture.models.VideoStabilizationMode
+import org.lineageos.aperture.viewmodels.CameraViewModel
import kotlin.reflect.safeCast
/**
@@ -34,7 +35,7 @@
@androidx.camera.camera2.interop.ExperimentalCamera2Interop
@androidx.camera.core.ExperimentalLensFacing
@androidx.camera.core.ExperimentalZeroShutterLag
-class Camera(cameraInfo: CameraInfo, cameraManager: CameraManager) {
+class Camera(cameraInfo: CameraInfo, model: CameraViewModel) {
val cameraSelector = cameraInfo.cameraSelector
private val camera2CameraInfo = Camera2CameraInfo.from(cameraInfo)
@@ -56,7 +57,7 @@
val isLogical = camera2CameraInfo.physicalCameraIds.size > 1
val intrinsicZoomRatio = cameraInfo.intrinsicZoomRatio
- val logicalZoomRatios = cameraManager.getLogicalZoomRatios(cameraId)
+ val logicalZoomRatios = model.getLogicalZoomRatios(cameraId)
private val supportedVideoFrameRates = cameraInfo.supportedFrameRateRanges.mapNotNull {
FrameRate.fromRange(it)
@@ -77,7 +78,7 @@
VideoQualityInfo(
it,
supportedVideoFrameRates.toMutableSet().apply {
- for ((frameRate, remove) in cameraManager.getAdditionalVideoFrameRates(
+ for ((frameRate, remove) in model.getAdditionalVideoFrameRates(
cameraId, it
)) {
if (remove) {
@@ -95,7 +96,7 @@
val supportsVideoRecording = supportedVideoQualities.isNotEmpty()
- val supportedExtensionModes = cameraManager.extensionsManager.getSupportedModes(cameraSelector)
+ val supportedExtensionModes = model.extensionsManager.getSupportedModes(cameraSelector)
val supportedVideoStabilizationModes = mutableListOf(VideoStabilizationMode.OFF).apply {
val availableVideoStabilizationModes = camera2CameraInfo.getCameraCharacteristic(
diff --git a/app/src/main/java/org/lineageos/aperture/camera/CameraManager.kt b/app/src/main/java/org/lineageos/aperture/camera/CameraManager.kt
deleted file mode 100644
index e71f284..0000000
--- a/app/src/main/java/org/lineageos/aperture/camera/CameraManager.kt
+++ /dev/null
@@ -1,193 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2022-2023 The LineageOS Project
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package org.lineageos.aperture.camera
-
-import android.content.Context
-import androidx.camera.extensions.ExtensionsManager
-import androidx.camera.lifecycle.ProcessCameraProvider
-import androidx.camera.video.Quality
-import androidx.camera.view.LifecycleCameraController
-import org.lineageos.aperture.models.CameraFacing
-import org.lineageos.aperture.models.CameraMode
-import org.lineageos.aperture.models.CameraType
-import org.lineageos.aperture.utils.OverlayConfiguration
-import java.util.concurrent.ExecutorService
-import java.util.concurrent.Executors
-
-/**
- * Class managing an app camera session
- */
-@androidx.camera.camera2.interop.ExperimentalCamera2Interop
-class CameraManager(context: Context) {
- private val cameraProvider = ProcessCameraProvider.getInstance(context).get()
- val extensionsManager: ExtensionsManager =
- ExtensionsManager.getInstanceAsync(context, cameraProvider).get()
- val cameraController = LifecycleCameraController(context)
- val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
-
- val overlayConfiguration = OverlayConfiguration(context)
-
- private val cameras: List<Camera>
- get() = cameraProvider.availableCameraInfos.map {
- Camera(it, this)
- }.sortedBy { it.cameraId }
-
- // We expect device cameras to never change
- private val internalCameras = cameras.filter {
- it.cameraType == CameraType.INTERNAL
- && !overlayConfiguration.ignoredAuxCameraIds.contains(it.cameraId)
- }
-
- private val backCameras = prepareDeviceCamerasList(CameraFacing.BACK)
- private val mainBackCamera = backCameras.firstOrNull()
- private val backCamerasSupportingVideoRecording = backCameras.filter {
- it.supportsVideoRecording
- }
-
- private val frontCameras = prepareDeviceCamerasList(CameraFacing.FRONT)
- private val mainFrontCamera = frontCameras.firstOrNull()
- private val frontCamerasSupportingVideoRecording = frontCameras.filter {
- it.supportsVideoRecording
- }
-
- private val externalCameras: List<Camera>
- get() = cameras.filter {
- it.cameraType == CameraType.EXTERNAL
- }
- private val externalCamerasSupportingVideoRecording: List<Camera>
- get() = externalCameras.filter { it.supportsVideoRecording }
-
- // Google recommends cycling between all externals, back and front
- // We're gonna do back, front and all externals instead, makes more sense
- private val availableCameras: List<Camera>
- get() = mutableListOf<Camera>().apply {
- mainBackCamera?.let {
- add(it)
- }
- mainFrontCamera?.let {
- add(it)
- }
- addAll(externalCameras)
- }
- private val availableCamerasSupportingVideoRecording: List<Camera>
- get() = availableCameras.filter { it.supportsVideoRecording }
-
- fun getAdditionalVideoFrameRates(cameraId: String, quality: Quality) =
- overlayConfiguration.additionalVideoConfigurations[cameraId]?.get(quality) ?: setOf()
-
- fun getLogicalZoomRatios(cameraId: String) = mutableMapOf(1.0f to 1.0f).apply {
- overlayConfiguration.logicalZoomRatios[cameraId]?.let {
- putAll(it)
- }
- }.toSortedMap()
-
- fun getCameras(
- cameraMode: CameraMode, cameraFacing: CameraFacing,
- ): List<Camera> {
- return when (cameraMode) {
- CameraMode.VIDEO -> when (cameraFacing) {
- CameraFacing.BACK -> backCamerasSupportingVideoRecording
- CameraFacing.FRONT -> frontCamerasSupportingVideoRecording
- CameraFacing.EXTERNAL -> externalCamerasSupportingVideoRecording
- else -> throw Exception("Unknown facing")
- }
-
- else -> when (cameraFacing) {
- CameraFacing.BACK -> backCameras
- CameraFacing.FRONT -> frontCameras
- CameraFacing.EXTERNAL -> externalCameras
- else -> throw Exception("Unknown facing")
- }
- }
- }
-
- /**
- * Get a suitable [Camera] for the provided [CameraFacing] and [CameraMode].
- * @param cameraFacing The requested [CameraFacing]
- * @param cameraMode The requested [CameraMode]
- * @return A [Camera] that is compatible with the provided configuration or null
- */
- fun getCameraOfFacingOrFirstAvailable(
- cameraFacing: CameraFacing, cameraMode: CameraMode
- ): Camera? {
- val camera = when (cameraFacing) {
- CameraFacing.BACK -> mainBackCamera
- CameraFacing.FRONT -> mainFrontCamera
- CameraFacing.EXTERNAL -> externalCameras.firstOrNull()
- else -> throw Exception("Unknown facing")
- }
- return camera?.let {
- if (cameraMode == CameraMode.VIDEO && !it.supportsVideoRecording) {
- availableCamerasSupportingVideoRecording.firstOrNull()
- } else {
- it
- }
- } ?: when (cameraMode) {
- CameraMode.VIDEO -> availableCamerasSupportingVideoRecording.firstOrNull()
- else -> availableCameras.firstOrNull()
- }
- }
-
- /**
- * Return the next camera, used for flip camera.
- * @param camera The current [Camera] used
- * @param cameraMode The current [CameraMode]
- * @return The next camera, may return null if all the cameras disappeared
- */
- fun getNextCamera(camera: Camera, cameraMode: CameraMode): Camera? {
- val cameras = when (cameraMode) {
- CameraMode.VIDEO -> availableCamerasSupportingVideoRecording
- else -> availableCameras
- }
-
- // If value is -1 it will just pick the first available camera
- // This should only happen when an external camera is disconnected
- val newCameraIndex = cameras.indexOf(
- when (camera.cameraFacing) {
- CameraFacing.BACK -> mainBackCamera
- CameraFacing.FRONT -> mainFrontCamera
- CameraFacing.EXTERNAL -> camera
- else -> throw Exception("Unknown facing")
- }
- ) + 1
-
- return if (newCameraIndex >= cameras.size) {
- cameras.firstOrNull()
- } else {
- cameras[newCameraIndex]
- }
- }
-
- fun videoRecordingAvailable() = availableCamerasSupportingVideoRecording.isNotEmpty()
-
- fun shutdown() {
- cameraExecutor.shutdown()
- }
-
- private fun prepareDeviceCamerasList(cameraFacing: CameraFacing): List<Camera> {
- val facingCameras = internalCameras.filter {
- it.cameraFacing == cameraFacing
- }
-
- if (facingCameras.isEmpty()) {
- return listOf()
- }
-
- val mainCamera = facingCameras.first()
-
- if (!overlayConfiguration.enableAuxCameras) {
- // Return only the main camera
- return listOf(mainCamera)
- }
-
- // Get the list of aux cameras
- val auxCameras = facingCameras
- .drop(1)
- .filter { !overlayConfiguration.ignoreLogicalAuxCameras || !it.isLogical }
-
- return listOf(mainCamera) + auxCameras
- }
-}
diff --git a/app/src/main/java/org/lineageos/aperture/viewmodels/CameraViewModel.kt b/app/src/main/java/org/lineageos/aperture/viewmodels/CameraViewModel.kt
index f260d27..4135bef 100644
--- a/app/src/main/java/org/lineageos/aperture/viewmodels/CameraViewModel.kt
+++ b/app/src/main/java/org/lineageos/aperture/viewmodels/CameraViewModel.kt
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: 2023 The LineageOS Project
+ * SPDX-FileCopyrightText: 2023-2024 The LineageOS Project
* SPDX-License-Identifier: Apache-2.0
*/
@@ -7,6 +7,8 @@
import android.app.Application
import android.net.Uri
+import androidx.camera.extensions.ExtensionsManager
+import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.video.Quality
import androidx.camera.video.Recording
import androidx.lifecycle.AndroidViewModel
@@ -19,8 +21,10 @@
import kotlinx.coroutines.flow.stateIn
import org.lineageos.aperture.camera.Camera
import org.lineageos.aperture.ext.*
+import org.lineageos.aperture.models.CameraFacing
import org.lineageos.aperture.models.CameraMode
import org.lineageos.aperture.models.CameraState
+import org.lineageos.aperture.models.CameraType
import org.lineageos.aperture.models.FlashMode
import org.lineageos.aperture.models.FrameRate
import org.lineageos.aperture.models.GridMode
@@ -28,15 +32,133 @@
import org.lineageos.aperture.models.TimerMode
import org.lineageos.aperture.models.VideoDynamicRange
import org.lineageos.aperture.repository.MediaRepository
+import org.lineageos.aperture.utils.OverlayConfiguration
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
/**
* [ViewModel] representing a camera session. This data is used to receive
* live data regarding the setting currently enabled.
*/
+@androidx.camera.camera2.interop.ExperimentalCamera2Interop
class CameraViewModel(application: Application) : AndroidViewModel(application) {
// Base
/**
+ * CameraX's [ProcessCameraProvider].
+ */
+ private val cameraProvider = ProcessCameraProvider.getInstance(context).get()
+
+ /**
+ * CameraX's [ExtensionsManager].
+ */
+ val extensionsManager: ExtensionsManager =
+ ExtensionsManager.getInstanceAsync(context, cameraProvider).get()
+
+ /**
+ * [ExecutorService] for camera related operations.
+ */
+ val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
+
+ /**
+ * Overlay configuration.
+ */
+ val overlayConfiguration = OverlayConfiguration(context)
+
+ /**
+ * The available [Camera]s.
+ */
+ private val cameras: List<Camera>
+ get() = cameraProvider.availableCameraInfos.map {
+ Camera(it, this)
+ }.sortedBy { it.cameraId }
+
+ /**
+ * List of internal [Camera]s.
+ * We expect device cameras to never change.
+ */
+ private val internalCameras = cameras.filter {
+ it.cameraType == CameraType.INTERNAL
+ && !overlayConfiguration.ignoredAuxCameraIds.contains(it.cameraId)
+ }
+
+ /**
+ * The list of internal back [Camera]s.
+ */
+ private val backCameras = prepareDeviceCamerasList(CameraFacing.BACK)
+
+ /**
+ * The main back camera, equals to the first one, usually ID 0.
+ */
+ private val mainBackCamera = backCameras.firstOrNull()
+
+ /**
+ * The list of internal back [Camera]s supporting video recording.
+ */
+ private val backCamerasSupportingVideoRecording = backCameras.filter {
+ it.supportsVideoRecording
+ }
+
+ /**
+ * The list of internal front [Camera]s.
+ */
+ private val frontCameras = prepareDeviceCamerasList(CameraFacing.FRONT)
+
+ /**
+ * The main front camera, equals to the first one, usually ID 1.
+ */
+ private val mainFrontCamera = frontCameras.firstOrNull()
+
+ /**
+ * The list of internal front [Camera]s supporting video recording.
+ */
+ private val frontCamerasSupportingVideoRecording = frontCameras.filter {
+ it.supportsVideoRecording
+ }
+
+ /**
+ * The list of external [Camera]s.
+ * Expected to change, do not store this anywhere.
+ */
+ private val externalCameras: List<Camera>
+ get() = cameras.filter {
+ it.cameraType == CameraType.EXTERNAL
+ }
+
+ /**
+ * The list of external [Camera]s supporting video recording.
+ * Expected to change, do not store this anywhere.
+ */
+ private val externalCamerasSupportingVideoRecording: List<Camera>
+ get() = externalCameras.filter { it.supportsVideoRecording }
+
+ /**
+ * The list of [Camera]s to use for cycling.
+ * Google recommends cycling between all externals, back and front,
+ * we do back, front and all externals instead, makes more sense.
+ * Expected to change, do not store this anywhere.
+ */
+ private val availableCameras: List<Camera>
+ get() = mutableListOf<Camera>().apply {
+ mainBackCamera?.let {
+ add(it)
+ }
+ mainFrontCamera?.let {
+ add(it)
+ }
+ addAll(externalCameras)
+ }
+
+ /**
+ * The list of [Camera]s that supports video recording to use for cycling.
+ * Google recommends cycling between all externals, back and front,
+ * we do back, front and all externals instead, makes more sense.
+ * Expected to change, do not store this anywhere.
+ */
+ private val availableCamerasSupportingVideoRecording: List<Camera>
+ get() = availableCameras.filter { it.supportsVideoRecording }
+
+ /**
* The camera currently in use.
*/
val camera = MutableLiveData<Camera>()
@@ -137,4 +259,115 @@
* Video recording duration.
*/
val videoRecordingDuration = MutableLiveData<Long>()
+
+ fun getAdditionalVideoFrameRates(cameraId: String, quality: Quality) =
+ overlayConfiguration.additionalVideoConfigurations[cameraId]?.get(quality) ?: setOf()
+
+ fun getLogicalZoomRatios(cameraId: String) = mutableMapOf(1.0f to 1.0f).apply {
+ overlayConfiguration.logicalZoomRatios[cameraId]?.let {
+ putAll(it)
+ }
+ }.toSortedMap()
+
+ fun getCameras(
+ cameraMode: CameraMode, cameraFacing: CameraFacing,
+ ) = when (cameraMode) {
+ CameraMode.VIDEO -> when (cameraFacing) {
+ CameraFacing.BACK -> backCamerasSupportingVideoRecording
+ CameraFacing.FRONT -> frontCamerasSupportingVideoRecording
+ CameraFacing.EXTERNAL -> externalCamerasSupportingVideoRecording
+ else -> throw Exception("Unknown facing")
+ }
+
+ else -> when (cameraFacing) {
+ CameraFacing.BACK -> backCameras
+ CameraFacing.FRONT -> frontCameras
+ CameraFacing.EXTERNAL -> externalCameras
+ else -> throw Exception("Unknown facing")
+ }
+ }
+
+ /**
+ * Get a suitable [Camera] for the provided [CameraFacing] and [CameraMode].
+ * @param cameraFacing The requested [CameraFacing]
+ * @param cameraMode The requested [CameraMode]
+ * @return A [Camera] that is compatible with the provided configuration or null
+ */
+ fun getCameraOfFacingOrFirstAvailable(
+ cameraFacing: CameraFacing, cameraMode: CameraMode
+ ) = when (cameraFacing) {
+ CameraFacing.BACK -> mainBackCamera
+ CameraFacing.FRONT -> mainFrontCamera
+ CameraFacing.EXTERNAL -> externalCameras.firstOrNull()
+ else -> throw Exception("Unknown facing")
+ }?.let {
+ if (cameraMode == CameraMode.VIDEO && !it.supportsVideoRecording) {
+ availableCamerasSupportingVideoRecording.firstOrNull()
+ } else {
+ it
+ }
+ } ?: when (cameraMode) {
+ CameraMode.VIDEO -> availableCamerasSupportingVideoRecording.firstOrNull()
+ else -> availableCameras.firstOrNull()
+ }
+
+ /**
+ * Return the next camera, used for flip camera.
+ * @param camera The current [Camera] used
+ * @param cameraMode The current [CameraMode]
+ * @return The next camera, may return null if all the cameras disappeared
+ */
+ fun getNextCamera(camera: Camera, cameraMode: CameraMode): Camera? {
+ val cameras = when (cameraMode) {
+ CameraMode.VIDEO -> availableCamerasSupportingVideoRecording
+ else -> availableCameras
+ }
+
+ // If value is -1 it will just pick the first available camera
+ // This should only happen when an external camera is disconnected
+ val newCameraIndex = cameras.indexOf(
+ when (camera.cameraFacing) {
+ CameraFacing.BACK -> mainBackCamera
+ CameraFacing.FRONT -> mainFrontCamera
+ CameraFacing.EXTERNAL -> camera
+ else -> throw Exception("Unknown facing")
+ }
+ ) + 1
+
+ return if (newCameraIndex >= cameras.size) {
+ cameras.firstOrNull()
+ } else {
+ cameras[newCameraIndex]
+ }
+ }
+
+ fun videoRecordingAvailable() = availableCamerasSupportingVideoRecording.isNotEmpty()
+
+ fun shutdown() {
+ cameraExecutor.shutdown()
+ }
+
+ private fun prepareDeviceCamerasList(cameraFacing: CameraFacing): List<Camera> {
+ val facingCameras = internalCameras.filter {
+ it.cameraFacing == cameraFacing
+ }
+
+ if (facingCameras.isEmpty()) {
+ return listOf()
+ }
+
+ val mainCamera = facingCameras.first()
+
+ if (!overlayConfiguration.enableAuxCameras) {
+ // Return only the main camera
+ return listOf(mainCamera)
+ }
+
+ // Get the list of aux cameras
+ val auxCameras = facingCameras
+ .drop(1)
+ .filter { !overlayConfiguration.ignoreLogicalAuxCameras || !it.isLogical }
+
+ return listOf(mainCamera) + auxCameras
+ }
}