Aperture: Use CameraX APIs for logical cameras

Took 'em long enough

Change-Id: I52c11adb8e296a74e19e20856b12f22abefb2633
diff --git a/app/src/main/java/org/lineageos/aperture/camera/BaseCamera.kt b/app/src/main/java/org/lineageos/aperture/camera/BaseCamera.kt
new file mode 100644
index 0000000..7f52c32
--- /dev/null
+++ b/app/src/main/java/org/lineageos/aperture/camera/BaseCamera.kt
@@ -0,0 +1,60 @@
+/*
+ * SPDX-FileCopyrightText: 2024 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.aperture.camera
+
+import androidx.camera.camera2.interop.Camera2CameraInfo
+import androidx.camera.core.CameraInfo
+import androidx.camera.core.CameraSelector
+import org.lineageos.aperture.models.CameraFacing
+import org.lineageos.aperture.models.CameraType
+import org.lineageos.aperture.viewmodels.CameraViewModel
+import kotlin.reflect.safeCast
+
+/**
+ * A generic camera device.
+ * The only contract in place is that the camera ID must be unique also between different
+ * implementations (guaranteed by Android).
+ */
+@androidx.camera.camera2.interop.ExperimentalCamera2Interop
+@androidx.camera.core.ExperimentalLensFacing
+abstract class BaseCamera(cameraInfo: CameraInfo, model: CameraViewModel) {
+    /**
+     * The [CameraSelector] for this camera.
+     */
+    abstract val cameraSelector: CameraSelector
+
+    /**
+     * The [Camera2CameraInfo] of this camera.
+     */
+    protected val camera2CameraInfo = Camera2CameraInfo.from(cameraInfo)
+
+    /**
+     * Camera2's camera ID.
+     */
+    val cameraId = camera2CameraInfo.cameraId
+
+    /**
+     * The [CameraFacing] of this camera.
+     */
+    val cameraFacing = when (cameraInfo.lensFacing) {
+        CameraSelector.LENS_FACING_UNKNOWN -> CameraFacing.UNKNOWN
+        CameraSelector.LENS_FACING_FRONT -> CameraFacing.FRONT
+        CameraSelector.LENS_FACING_BACK -> CameraFacing.BACK
+        CameraSelector.LENS_FACING_EXTERNAL -> CameraFacing.EXTERNAL
+        else -> throw Exception("Unknown lens facing value")
+    }
+
+    /**
+     * The [CameraType] of this camera.
+     */
+    val cameraType = cameraFacing.cameraType
+
+    override fun equals(other: Any?) = this::class.safeCast(other)?.let {
+        this.cameraId == it.cameraId
+    } ?: false
+
+    override fun hashCode() = this::class.qualifiedName.hashCode() + cameraId.hashCode()
+}
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 2c820b0..556575b 100644
--- a/app/src/main/java/org/lineageos/aperture/camera/Camera.kt
+++ b/app/src/main/java/org/lineageos/aperture/camera/Camera.kt
@@ -1,5 +1,5 @@
 /*
- * SPDX-FileCopyrightText: 2022-2023 The LineageOS Project
+ * SPDX-FileCopyrightText: 2022-2024 The LineageOS Project
  * SPDX-License-Identifier: Apache-2.0
  */
 
@@ -8,9 +8,7 @@
 import android.hardware.camera2.CameraCharacteristics
 import android.hardware.camera2.CameraMetadata
 import android.os.Build
-import androidx.camera.camera2.interop.Camera2CameraInfo
 import androidx.camera.core.CameraInfo
-import androidx.camera.core.CameraSelector
 import androidx.camera.video.Recorder
 import org.lineageos.aperture.ext.*
 import org.lineageos.aperture.models.CameraFacing
@@ -27,7 +25,6 @@
 import org.lineageos.aperture.models.VideoQualityInfo
 import org.lineageos.aperture.models.VideoStabilizationMode
 import org.lineageos.aperture.viewmodels.CameraViewModel
-import kotlin.reflect.safeCast
 
 /**
  * Class representing a device camera
@@ -35,26 +32,16 @@
 @androidx.camera.camera2.interop.ExperimentalCamera2Interop
 @androidx.camera.core.ExperimentalLensFacing
 @androidx.camera.core.ExperimentalZeroShutterLag
-class Camera(cameraInfo: CameraInfo, model: CameraViewModel) {
-    val cameraSelector = cameraInfo.cameraSelector
-
-    private val camera2CameraInfo = Camera2CameraInfo.from(cameraInfo)
-    val cameraId = camera2CameraInfo.cameraId
-
-    val cameraFacing = when (cameraInfo.lensFacing) {
-        CameraSelector.LENS_FACING_UNKNOWN -> CameraFacing.UNKNOWN
-        CameraSelector.LENS_FACING_FRONT -> CameraFacing.FRONT
-        CameraSelector.LENS_FACING_BACK -> CameraFacing.BACK
-        CameraSelector.LENS_FACING_EXTERNAL -> CameraFacing.EXTERNAL
-        else -> throw Exception("Unknown lens facing value")
-    }
-
-    val cameraType = cameraFacing.cameraType
+class Camera(cameraInfo: CameraInfo, model: CameraViewModel) : BaseCamera(cameraInfo, model) {
+    override val cameraSelector = cameraInfo.cameraSelector
 
     val exposureCompensationRange = cameraInfo.exposureState.exposureCompensationRange
     private val hasFlashUnit = cameraInfo.hasFlashUnit()
 
-    val isLogical = camera2CameraInfo.physicalCameraIds.size > 1
+    private val physicalCameras = cameraInfo.physicalCameraInfos.map {
+        PhysicalCamera(it, model, this)
+    }
+    val isLogical = physicalCameras.size > 1
 
     val intrinsicZoomRatio = cameraInfo.intrinsicZoomRatio
     val logicalZoomRatios = model.getLogicalZoomRatios(cameraId)
@@ -254,15 +241,6 @@
         }
     }
 
-    override fun equals(other: Any?): Boolean {
-        val camera = this::class.safeCast(other) ?: return false
-        return this.cameraId == camera.cameraId
-    }
-
-    override fun hashCode(): Int {
-        return this::class.qualifiedName.hashCode() + cameraId.hashCode()
-    }
-
     fun supportsExtensionMode(extensionMode: Int): Boolean {
         return supportedExtensionModes.contains(extensionMode)
     }
diff --git a/app/src/main/java/org/lineageos/aperture/camera/PhysicalCamera.kt b/app/src/main/java/org/lineageos/aperture/camera/PhysicalCamera.kt
new file mode 100644
index 0000000..91e35d8
--- /dev/null
+++ b/app/src/main/java/org/lineageos/aperture/camera/PhysicalCamera.kt
@@ -0,0 +1,25 @@
+/*
+ * SPDX-FileCopyrightText: 2024 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.aperture.camera
+
+import androidx.camera.core.CameraInfo
+import androidx.camera.core.CameraSelector
+import org.lineageos.aperture.viewmodels.CameraViewModel
+
+/**
+ * A logical camera's backing physical camera.
+ */
+@androidx.camera.camera2.interop.ExperimentalCamera2Interop
+@androidx.camera.core.ExperimentalLensFacing
+class PhysicalCamera(
+    cameraInfo: CameraInfo,
+    model: CameraViewModel,
+    logicalCamera: Camera,
+) : BaseCamera(cameraInfo, model) {
+    override val cameraSelector = CameraSelector.Builder()
+        .setPhysicalCameraId(cameraId)
+        .build()
+}
diff --git a/app/src/main/java/org/lineageos/aperture/ext/Camera2CameraInfo.kt b/app/src/main/java/org/lineageos/aperture/ext/Camera2CameraInfo.kt
deleted file mode 100644
index a08eff7..0000000
--- a/app/src/main/java/org/lineageos/aperture/ext/Camera2CameraInfo.kt
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2022 The LineageOS Project
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package org.lineageos.aperture.ext
-
-import android.hardware.camera2.CameraCharacteristics
-import android.os.Build
-import androidx.camera.camera2.interop.Camera2CameraInfo
-
-/**
- * We're adding this here since it's private. We're supposed to use
- * CameraCharacteristics.getPhysicalCameraIds() but it's not exposed by CameraX yet.
- */
-private val LOGICAL_MULTI_CAMERA_PHYSICAL_IDS by lazy {
-    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
-        CameraCharacteristics.Key(
-            "android.logicalMultiCamera.physicalIds",
-            ByteArray::class.java
-        )
-    } else {
-        throw Exception("Requesting LOGICAL_MULTI_CAMERA_PHYSICAL_IDS on older Android version")
-    }
-}
-
-/**
- * Return the set of physical camera ids that this logical {@link CameraDevice} is made
- * up of.
- *
- * If the camera device isn't a logical camera, return an empty set.
- */
-val Camera2CameraInfo.physicalCameraIds: Set<String>
-    @androidx.camera.camera2.interop.ExperimentalCamera2Interop
-    get() {
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
-            return setOf()
-        }
-
-        val availableCapabilities = getCameraCharacteristic(
-            CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES
-        ) ?: throw AssertionError(
-            "android.request.availableCapabilities must be non-null in the characteristics"
-        )
-        if (!availableCapabilities.contains(
-                CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA
-            )
-        ) {
-            return setOf()
-        }
-
-        val physicalCamIds: ByteArray = getCameraCharacteristic(
-            LOGICAL_MULTI_CAMERA_PHYSICAL_IDS
-        ) ?: throw AssertionError(
-            "android.logicalMultiCamera.physicalIds must be non-null in the characteristics"
-        )
-
-        val physicalCamIdString = String(physicalCamIds, Charsets.UTF_8)
-        val physicalCameraIdArray = physicalCamIdString.split(0.toChar())
-
-        return physicalCameraIdArray.toSet()
-    }