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
+    }
 }