Aperture: New camera mode selector UI

Change-Id: I8abe5a8b19bef7dcd46457ad2abb42dc73f9a551
diff --git a/app/src/main/java/org/lineageos/aperture/CameraActivity.kt b/app/src/main/java/org/lineageos/aperture/CameraActivity.kt
index b52a709..5032c0e 100644
--- a/app/src/main/java/org/lineageos/aperture/CameraActivity.kt
+++ b/app/src/main/java/org/lineageos/aperture/CameraActivity.kt
@@ -67,7 +67,6 @@
 import androidx.camera.view.video.AudioConfig
 import androidx.cardview.widget.CardView
 import androidx.constraintlayout.widget.ConstraintLayout
-import androidx.constraintlayout.widget.Group
 import androidx.core.animation.addListener
 import androidx.core.content.ContextCompat
 import androidx.core.location.LocationListenerCompat
@@ -79,7 +78,6 @@
 import androidx.core.view.WindowInsetsCompat
 import androidx.core.view.WindowInsetsControllerCompat
 import androidx.core.view.children
-import androidx.core.view.doOnLayout
 import androidx.core.view.isInvisible
 import androidx.core.view.isVisible
 import androidx.core.view.updateLayoutParams
@@ -90,7 +88,6 @@
 import coil.request.ImageRequest
 import coil.request.SuccessResult
 import coil.size.Scale
-import com.google.android.material.button.MaterialButton
 import com.google.android.material.snackbar.Snackbar
 import kotlinx.coroutines.sync.Mutex
 import org.lineageos.aperture.camera.CameraFacing
@@ -109,6 +106,7 @@
 import org.lineageos.aperture.camera.VideoStabilizationMode
 import org.lineageos.aperture.ext.*
 import org.lineageos.aperture.qr.QrImageAnalyzer
+import org.lineageos.aperture.ui.CameraModeSelectorLayout
 import org.lineageos.aperture.ui.CapturePreviewLayout
 import org.lineageos.aperture.ui.CountDownView
 import org.lineageos.aperture.ui.GridView
@@ -132,7 +130,6 @@
 import org.lineageos.aperture.utils.Rotation
 import org.lineageos.aperture.utils.ShortcutsUtils
 import org.lineageos.aperture.utils.StorageUtils
-import org.lineageos.aperture.utils.TimeUtils
 import org.lineageos.aperture.utils.TimerMode
 import java.io.ByteArrayInputStream
 import java.io.ByteArrayOutputStream
@@ -148,8 +145,7 @@
 open class CameraActivity : AppCompatActivity() {
     // Views
     private val aspectRatioButton by lazy { findViewById<Button>(R.id.aspectRatioButton) }
-    private val cameraModeButtonsGroup by lazy { findViewById<Group>(R.id.cameraModeButtonsGroup) }
-    private val cameraModeHighlight by lazy { findViewById<MaterialButton>(R.id.cameraModeHighlight) }
+    private val cameraModeSelectorLayout by lazy { findViewById<CameraModeSelectorLayout>(R.id.cameraModeSelectorLayout) }
     private val capturePreviewLayout by lazy { findViewById<CapturePreviewLayout>(R.id.capturePreviewLayout) }
     private val countDownView by lazy { findViewById<CountDownView>(R.id.countDownView) }
     private val effectButton by lazy { findViewById<Button>(R.id.effectButton) }
@@ -166,20 +162,15 @@
     private val levelerView by lazy { findViewById<LevelerView>(R.id.levelerView) }
     private val mainLayout by lazy { findViewById<ConstraintLayout>(R.id.mainLayout) }
     private val micButton by lazy { findViewById<Button>(R.id.micButton) }
-    private val modeSelectorLayout by lazy { findViewById<ConstraintLayout>(R.id.modeSelectorLayout) }
-    private val photoModeButton by lazy { findViewById<MaterialButton>(R.id.photoModeButton) }
     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 qrModeButton by lazy { findViewById<MaterialButton>(R.id.qrModeButton) }
     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) }
     private val shutterButton by lazy { findViewById<ImageButton>(R.id.shutterButton) }
     private val timerButton by lazy { findViewById<Button>(R.id.timerButton) }
-    private val videoDurationButton by lazy { findViewById<MaterialButton>(R.id.videoDurationButton) }
     private val videoFrameRateButton by lazy { findViewById<Button>(R.id.videoFrameRateButton) }
-    private val videoModeButton by lazy { findViewById<MaterialButton>(R.id.videoModeButton) }
     private val videoQualityButton by lazy { findViewById<Button>(R.id.videoQualityButton) }
     private val videoRecordingStateButton by lazy { findViewById<ImageButton>(R.id.videoRecordingStateButton) }
     private val viewFinder by lazy { findViewById<PreviewView>(R.id.viewFinder) }
@@ -222,6 +213,7 @@
     private var videoFrameRate by nullablePropertyDelegate { model.videoFrameRate }
     private var videoMicMode by nonNullablePropertyDelegate { model.videoMicMode }
     private var videoRecording by nullablePropertyDelegate { model.videoRecording }
+    private var videoDuration by nonNullablePropertyDelegate { model.videoRecordingDuration }
 
     private lateinit var initialCameraFacing: CameraFacing
 
@@ -568,6 +560,7 @@
         initialCameraFacing = sharedPreferences.lastCameraFacing
 
         // Pass the view model to the views
+        cameraModeSelectorLayout.cameraViewModel = model
         capturePreviewLayout.cameraViewModel = model
         countDownView.cameraViewModel = model
         infoChipView.cameraViewModel = model
@@ -596,20 +589,16 @@
             }
         }
 
-        if (cameraManager.internalCamerasSupportingVideoRecoding.isEmpty()) {
-            // Hide video mode button if no internal camera supports video recoding
-            videoModeButton.isVisible = false
-            if (cameraMode == CameraMode.VIDEO) {
-                // If an app asked for a video we have to bail out
-                if (singleCaptureMode) {
-                    Toast.makeText(
-                        this, getString(R.string.camcorder_unsupported_toast), Toast.LENGTH_LONG
-                    ).show()
-                    finish()
-                }
-                // Fallback to photo mode
-                cameraMode = CameraMode.PHOTO
+        if (cameraMode == CameraMode.VIDEO && !cameraManager.videoRecordingAvailable()) {
+            // If an app asked for a video we have to bail out
+            if (singleCaptureMode) {
+                Toast.makeText(
+                    this, getString(R.string.camcorder_unsupported_toast), Toast.LENGTH_LONG
+                ).show()
+                finish()
             }
+            // Fallback to photo mode
+            cameraMode = CameraMode.PHOTO
         }
 
         // Select a camera
@@ -619,7 +608,7 @@
         ViewCompat.setOnApplyWindowInsetsListener(mainLayout) { _, windowInsets ->
             val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
 
-            modeSelectorLayout.updateLayoutParams<ViewGroup.MarginLayoutParams> {
+            cameraModeSelectorLayout.updateLayoutParams<ViewGroup.MarginLayoutParams> {
                 bottomMargin = insets.bottom
                 leftMargin = insets.left
                 rightMargin = insets.right
@@ -658,15 +647,6 @@
             }
         }
 
-        // Initialize camera mode highlight position
-        (cameraModeHighlight.parent as View).doOnLayout {
-            cameraModeHighlight.x = when (cameraMode) {
-                CameraMode.QR -> qrModeButton.x
-                CameraMode.PHOTO -> photoModeButton.x
-                CameraMode.VIDEO -> videoModeButton.x
-            }
-        }
-
         // Attach CameraController to PreviewView
         viewFinder.controller = cameraController
 
@@ -787,10 +767,6 @@
         }
 
         // Set primary bar button callbacks
-        qrModeButton.setOnClickListener { changeCameraMode(CameraMode.QR) }
-        photoModeButton.setOnClickListener { changeCameraMode(CameraMode.PHOTO) }
-        videoModeButton.setOnClickListener { changeCameraMode(CameraMode.VIDEO) }
-
         flipCameraButton.setOnClickListener { flipCamera() }
         googleLensButton.setOnClickListener {
             dismissKeyguardAndRun {
@@ -866,6 +842,11 @@
             }
         }
 
+        // Set mode selector callback
+        cameraModeSelectorLayout.onModeSelectedCallback = {
+            changeCameraMode(it)
+        }
+
         // Bind viewfinder and preview blur view
         previewBlurView.previewView = viewFinder
 
@@ -901,26 +882,6 @@
             googleLensButton.isVisible = cameraMode == CameraMode.QR && isGoogleLensAvailable
 
             updatePrimaryBarButtons()
-
-            // Update camera mode buttons
-            qrModeButton.isEnabled = cameraMode != CameraMode.QR
-            photoModeButton.isEnabled = cameraMode != CameraMode.PHOTO
-            videoModeButton.isEnabled = cameraMode != CameraMode.VIDEO
-
-            // Animate camera mode change
-            (cameraModeHighlight.parent as View).doOnLayout {
-                ValueAnimator.ofFloat(
-                    cameraModeHighlight.x, when (cameraMode) {
-                        CameraMode.QR -> qrModeButton.x
-                        CameraMode.PHOTO -> photoModeButton.x
-                        CameraMode.VIDEO -> videoModeButton.x
-                    }
-                ).apply {
-                    addUpdateListener { valueAnimator ->
-                        cameraModeHighlight.x = valueAnimator.animatedValue as Float
-                    }
-                }.start()
-            }
         }
 
         // Observe single capture mode
@@ -929,9 +890,6 @@
 
             // Update primary bar buttons
             galleryButtonCardView.isInvisible = inSingleCaptureMode
-
-            // Update camera mode buttons
-            updateCameraModeButtons()
         }
 
         // Observe camera state
@@ -953,12 +911,6 @@
             videoRecordingStateButton.isVisible = cameraState.isRecordingVideo
 
             updatePrimaryBarButtons()
-
-            // Update camera mode buttons
-            updateCameraModeButtons()
-
-            // Update video duration button
-            videoDurationButton.isVisible = cameraState.isRecordingVideo
         }
 
         // Observe screen rotation
@@ -1442,7 +1394,7 @@
         cameraState = CameraState.PRE_RECORDING_VIDEO
 
         // Update duration text
-        videoDurationButton.text = TimeUtils.convertNanosToString(0)
+        videoDuration = 0L
 
         // Create output options object which contains file + metadata
         val outputOptions = StorageUtils.getVideoMediaStoreOutputOptions(
@@ -1477,8 +1429,7 @@
                     }
 
                     is VideoRecordEvent.Status -> runOnUiThread {
-                        videoDurationButton.text =
-                            TimeUtils.convertNanosToString(it.recordingStats.recordedDurationNanos)
+                        videoDuration = it.recordingStats.recordedDurationNanos
                     }
 
                     is VideoRecordEvent.Finalize -> {
@@ -1847,6 +1798,21 @@
             }
 
             CameraMode.VIDEO -> {
+                if (!cameraManager.videoRecordingAvailable()) {
+                    Snackbar.make(
+                        cameraModeSelectorLayout,
+                        R.string.camcorder_unsupported_toast,
+                        Snackbar.LENGTH_SHORT,
+                    ).apply {
+                        anchorView = cameraModeSelectorLayout
+                        setAction(android.R.string.ok) {
+                            // Do nothing
+                        }
+                    }.show()
+
+                    return
+                }
+
                 if (this.cameraMode == CameraMode.PHOTO) {
                     startShutterAnimation(ShutterAnimation.PhotoToVideo)
                 } else {
@@ -1928,19 +1894,6 @@
         }
     }
 
-    /**
-     * Some UI elements requires checking more than one value, this function will be called
-     * when one of these values will change.
-     */
-    private fun updateCameraModeButtons() {
-        runOnUiThread {
-            val inSingleCaptureMode = model.inSingleCaptureMode.value ?: return@runOnUiThread
-            val cameraState = model.cameraState.value ?: return@runOnUiThread
-
-            cameraModeButtonsGroup.isInvisible = cameraState.isRecordingVideo || inSingleCaptureMode
-        }
-    }
-
     private fun cycleAspectRatio() {
         if (!canRestartCamera()) {
             return
diff --git a/app/src/main/java/org/lineageos/aperture/camera/CameraManager.kt b/app/src/main/java/org/lineageos/aperture/camera/CameraManager.kt
index 9772f52..07f4c80 100644
--- a/app/src/main/java/org/lineageos/aperture/camera/CameraManager.kt
+++ b/app/src/main/java/org/lineageos/aperture/camera/CameraManager.kt
@@ -120,9 +120,6 @@
         it.supportsVideoRecording
     }
 
-    val internalCamerasSupportingVideoRecoding =
-        backCamerasSupportingVideoRecording + frontCamerasSupportingVideoRecording
-
     private val externalCameras: List<Camera>
         get() = cameras.values.filter {
             it.cameraFacing == CameraFacing.EXTERNAL
@@ -219,6 +216,8 @@
         }
     }
 
+    fun videoRecordingAvailable() = availableCamerasSupportingVideoRecording.isNotEmpty()
+
     fun shutdown() {
         cameraExecutor.shutdown()
     }
diff --git a/app/src/main/java/org/lineageos/aperture/camera/CameraMode.kt b/app/src/main/java/org/lineageos/aperture/camera/CameraMode.kt
index dddb89b..de5a43f 100644
--- a/app/src/main/java/org/lineageos/aperture/camera/CameraMode.kt
+++ b/app/src/main/java/org/lineageos/aperture/camera/CameraMode.kt
@@ -5,8 +5,11 @@
 
 package org.lineageos.aperture.camera
 
-enum class CameraMode {
-    PHOTO,
-    VIDEO,
-    QR,
+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),
+    QR(R.string.camera_mode_qr),
 }
diff --git a/app/src/main/java/org/lineageos/aperture/camera/CameraViewModel.kt b/app/src/main/java/org/lineageos/aperture/camera/CameraViewModel.kt
index dfafed6..2e88b2d 100644
--- a/app/src/main/java/org/lineageos/aperture/camera/CameraViewModel.kt
+++ b/app/src/main/java/org/lineageos/aperture/camera/CameraViewModel.kt
@@ -100,4 +100,9 @@
      * Video [Recording].
      */
     val videoRecording = MutableLiveData<Recording?>()
+
+    /**
+     * Video recording duration.
+     */
+    val videoRecordingDuration = MutableLiveData<Long>()
 }
diff --git a/app/src/main/java/org/lineageos/aperture/ui/CameraModeSelectorLayout.kt b/app/src/main/java/org/lineageos/aperture/ui/CameraModeSelectorLayout.kt
new file mode 100644
index 0000000..2cb8121
--- /dev/null
+++ b/app/src/main/java/org/lineageos/aperture/ui/CameraModeSelectorLayout.kt
@@ -0,0 +1,132 @@
+/*
+ * SPDX-FileCopyrightText: 2022-2023 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.aperture.ui
+
+import android.animation.ValueAnimator
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.widget.FrameLayout
+import android.widget.LinearLayout
+import androidx.core.view.doOnLayout
+import androidx.core.view.isInvisible
+import androidx.core.view.isVisible
+import androidx.lifecycle.Observer
+import androidx.lifecycle.findViewTreeLifecycleOwner
+import com.google.android.material.button.MaterialButton
+import org.lineageos.aperture.R
+import org.lineageos.aperture.camera.CameraMode
+import org.lineageos.aperture.camera.CameraState
+import org.lineageos.aperture.camera.CameraViewModel
+import org.lineageos.aperture.ext.px
+import org.lineageos.aperture.utils.TimeUtils
+import java.lang.Exception
+import kotlin.reflect.cast
+
+class CameraModeSelectorLayout @JvmOverloads constructor(
+    context: Context, attrs: AttributeSet? = null
+) : FrameLayout(context, attrs) {
+    // Views
+    private val cameraModeHighlightButton by lazy { findViewById<MaterialButton>(R.id.cameraModeHighlightButton) }
+    private val cameraModeButtonsLinearLayout by lazy { findViewById<LinearLayout>(R.id.cameraModeButtonsLinearLayout) }
+    private val videoDurationButton by lazy { findViewById<MaterialButton>(R.id.videoDurationButton) }
+
+    // System services
+    private val layoutInflater by lazy { context.getSystemService(LayoutInflater::class.java) }
+
+    private val cameraToButton = mutableMapOf<CameraMode, MaterialButton>()
+
+    private val cameraModeObserver = Observer { cameraMode: CameraMode ->
+        val currentCameraModeButton =
+            cameraToButton[cameraMode] ?: throw Exception("No button for $cameraMode")
+
+        cameraToButton.forEach {
+            it.value.isEnabled = cameraMode != it.key
+        }
+
+        // Animate camera mode change
+        doOnLayout {
+            // Animate position
+            ValueAnimator.ofFloat(
+                cameraModeHighlightButton.x, currentCameraModeButton.x + 16.px
+            ).apply {
+                addUpdateListener { valueAnimator ->
+                    cameraModeHighlightButton.x = valueAnimator.animatedValue as Float
+                }
+            }.start()
+
+            // Animate width
+            ValueAnimator.ofInt(
+                cameraModeHighlightButton.width, currentCameraModeButton.width
+            ).apply {
+                addUpdateListener { valueAnimator ->
+                    cameraModeHighlightButton.width = valueAnimator.animatedValue as Int
+                }
+            }.start()
+        }
+    }
+
+    private val inSingleCaptureModeObserver = Observer { _: Boolean ->
+        updateButtons()
+    }
+
+    private val cameraStateObserver = Observer { cameraState: CameraState ->
+        updateButtons()
+
+        // Update video duration button
+        videoDurationButton.isVisible = cameraState.isRecordingVideo
+    }
+
+    private val videoDurationObserver = Observer { videoDuration: Long ->
+        videoDurationButton.text = TimeUtils.convertNanosToString(videoDuration)
+    }
+
+    internal var cameraViewModel: CameraViewModel? = null
+        set(value) {
+            // Unregister
+            field?.cameraMode?.removeObserver(cameraModeObserver)
+            field?.inSingleCaptureMode?.removeObserver(inSingleCaptureModeObserver)
+            field?.cameraState?.removeObserver(cameraStateObserver)
+            field?.videoRecordingDuration?.removeObserver(videoDurationObserver)
+
+            field = value
+
+            val lifecycleOwner = findViewTreeLifecycleOwner() ?: return
+
+            value?.cameraMode?.observe(lifecycleOwner, cameraModeObserver)
+            value?.inSingleCaptureMode?.observe(lifecycleOwner, inSingleCaptureModeObserver)
+            value?.cameraState?.observe(lifecycleOwner, cameraStateObserver)
+            value?.videoRecordingDuration?.observe(lifecycleOwner, videoDurationObserver)
+        }
+
+    var onModeSelectedCallback: (cameraMode: CameraMode) -> Unit = {}
+
+    init {
+        inflate(context, R.layout.camera_mode_selector_layout, this)
+
+        for (cameraMode in CameraMode.values()) {
+            cameraToButton[cameraMode] = MaterialButton::class.cast(
+                layoutInflater.inflate(
+                    R.layout.camera_mode_button, this, false
+                )
+            ).apply {
+                setText(cameraMode.title)
+                setOnClickListener { onModeSelectedCallback(cameraMode) }
+            }.also {
+                cameraModeButtonsLinearLayout.addView(it)
+            }
+        }
+    }
+
+    private fun updateButtons() {
+        val inSingleCaptureMode = cameraViewModel?.inSingleCaptureMode?.value ?: return
+        val cameraState = cameraViewModel?.cameraState?.value ?: return
+
+        cameraToButton.forEach {
+            it.value.isInvisible = cameraState.isRecordingVideo || inSingleCaptureMode
+        }
+    }
+}
diff --git a/app/src/main/res/layout/activity_camera.xml b/app/src/main/res/layout/activity_camera.xml
index 139ee23..96783f4 100644
--- a/app/src/main/res/layout/activity_camera.xml
+++ b/app/src/main/res/layout/activity_camera.xml
@@ -274,7 +274,7 @@
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:paddingTop="16dp"
-        app:layout_constraintBottom_toTopOf="@+id/modeSelectorLayout"
+        app:layout_constraintBottom_toTopOf="@+id/cameraModeSelectorLayout"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent">
 
@@ -349,103 +349,16 @@
         android:scaleType="center"
         android:src="@drawable/ic_google_lens"
         android:visibility="gone"
-        app:layout_constraintBottom_toTopOf="@+id/modeSelectorLayout"
+        app:layout_constraintBottom_toTopOf="@+id/cameraModeSelectorLayout"
         app:layout_constraintStart_toStartOf="parent" />
 
-    <androidx.constraintlayout.widget.ConstraintLayout
-        android:id="@+id/modeSelectorLayout"
-        android:layout_width="match_parent"
-        android:layout_height="0dp"
-        android:padding="16dp"
+    <org.lineageos.aperture.ui.CameraModeSelectorLayout
+        android:id="@+id/cameraModeSelectorLayout"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toStartOf="parent">
-
-        <androidx.constraintlayout.widget.Group
-            android:id="@+id/cameraModeButtonsGroup"
-            android:layout_width="0dp"
-            android:layout_height="0dp"
-            app:constraint_referenced_ids="cameraModeHighlight,photoModeButton,videoModeButton,qrModeButton" />
-
-        <Button
-            android:id="@+id/cameraModeHighlight"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:backgroundTint="?attr/colorSecondaryContainer"
-            android:contentDescription="@string/camera_mode_highlight_description"
-            android:enabled="false"
-            android:padding="0dp"
-            app:iconPadding="0dp"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintEnd_toStartOf="@+id/videoModeButton"
-            app:layout_constraintHorizontal_bias="0.5"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintTop_toTopOf="parent" />
-
-        <Button
-            android:id="@+id/photoModeButton"
-            style="@style/Theme.Aperture.Camera.CameraModeSelectorButton"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:contentDescription="@string/photo_mode_button_description"
-            android:enabled="false"
-            android:padding="0dp"
-            android:text="@string/selector_photo"
-            app:iconPadding="0dp"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintEnd_toStartOf="@+id/videoModeButton"
-            app:layout_constraintHorizontal_bias="0.5"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintTop_toTopOf="parent" />
-
-        <Button
-            android:id="@+id/videoModeButton"
-            style="@style/Theme.Aperture.Camera.CameraModeSelectorButton"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:contentDescription="@string/video_mode_button_description"
-            android:padding="0dp"
-            android:text="@string/select_video"
-            app:iconPadding="0dp"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintEnd_toStartOf="@+id/qrModeButton"
-            app:layout_constraintHorizontal_bias="0.5"
-            app:layout_constraintStart_toEndOf="@+id/photoModeButton"
-            app:layout_constraintTop_toTopOf="parent" />
-
-        <Button
-            android:id="@+id/qrModeButton"
-            style="@style/Theme.Aperture.Camera.CameraModeSelectorButton"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:contentDescription="@string/qr_mode_button_description"
-            android:padding="0dp"
-            android:text="@string/select_scan"
-            app:iconPadding="0dp"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintHorizontal_bias="0.5"
-            app:layout_constraintStart_toEndOf="@+id/videoModeButton"
-            app:layout_constraintTop_toTopOf="parent" />
-
-        <Button
-            android:id="@+id/videoDurationButton"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:backgroundTint="@color/rec_red"
-            android:contentDescription="@string/video_mode_button_description"
-            android:enabled="false"
-            android:padding="0dp"
-            android:textColor="@android:color/white"
-            android:visibility="gone"
-            app:iconPadding="0dp"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintEnd_toEndOf="@+id/videoModeButton"
-            app:layout_constraintHorizontal_bias="0.5"
-            app:layout_constraintStart_toStartOf="@+id/videoModeButton"
-            app:layout_constraintTop_toTopOf="parent"
-            tools:text="3:13:37" />
-    </androidx.constraintlayout.widget.ConstraintLayout>
+        app:layout_constraintStart_toStartOf="parent" />
 
     <include
         android:id="@+id/capturePreviewLayout"
diff --git a/app/src/main/res/layout/camera_mode_button.xml b/app/src/main/res/layout/camera_mode_button.xml
new file mode 100644
index 0000000..d6ff34b
--- /dev/null
+++ b/app/src/main/res/layout/camera_mode_button.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     SPDX-FileCopyrightText: 2023 The LineageOS Project
+     SPDX-License-Identifier: Apache-2.0
+-->
+<com.google.android.material.button.MaterialButton xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:backgroundTint="@android:color/transparent"
+    android:padding="0dp"
+    android:textAllCaps="true"
+    android:textColor="@color/camera_mode_selector_text"
+    android:textStyle="bold"
+    android:typeface="monospace"
+    app:iconPadding="0dp" />
diff --git a/app/src/main/res/layout/camera_mode_selector_layout.xml b/app/src/main/res/layout/camera_mode_selector_layout.xml
new file mode 100644
index 0000000..b99a361
--- /dev/null
+++ b/app/src/main/res/layout/camera_mode_selector_layout.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     SPDX-FileCopyrightText: 2023 The LineageOS Project
+     SPDX-License-Identifier: Apache-2.0
+-->
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:padding="16dp">
+
+    <com.google.android.material.button.MaterialButton
+        android:id="@+id/cameraModeHighlightButton"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:backgroundTint="?attr/colorSecondaryContainer"
+        android:enabled="false"
+        android:padding="0dp"
+        app:iconPadding="0dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <LinearLayout
+        android:id="@+id/cameraModeButtonsLinearLayout"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:gravity="center"
+        android:orientation="horizontal"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <com.google.android.material.button.MaterialButton
+        android:id="@+id/videoDurationButton"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:backgroundTint="@color/rec_red"
+        android:enabled="false"
+        android:padding="0dp"
+        android:textColor="@android:color/white"
+        android:visibility="gone"
+        app:iconPadding="0dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        tools:text="3:13:37" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 16e26ad..7b5c5c5 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -9,7 +9,6 @@
     <string name="video_camera_label">Camcorder</string>
 
     <!-- Content descriptions -->
-    <string name="camera_mode_highlight_description">Camera mode highlight</string>
     <string name="cancel_button_description">Cancel</string>
     <string name="confirm_button_description">Confirm</string>
     <string name="flash_button_description">Flash mode</string>
@@ -17,11 +16,8 @@
     <string name="gallery_button_description">Gallery</string>
     <string name="google_lens_button_description">Open Google Lens</string>
     <string name="image_view_description">Image preview</string>
-    <string name="photo_mode_button_description">Switch to photo mode</string>
     <string name="pro_button_description">Pro settings</string>
-    <string name="qr_mode_button_description">Switch to QR scanner mode</string>
     <string name="shutter_button_description">Shutter</string>
-    <string name="video_mode_button_description">Switch to video mode</string>
     <string name="video_recording_state_button_description">Pause/resume video recording</string>
 
     <!-- Secondary button text -->
@@ -57,11 +53,6 @@
 
     <string name="settings">SETTINGS</string>
 
-    <!-- Selector chip -->
-    <string name="selector_photo">PHOTO</string>
-    <string name="select_video">VIDEO</string>
-    <string name="select_scan">SCAN</string>
-
     <!-- Toast messages -->
     <string name="app_permissions_toast">Permissions not granted by the user.</string>
     <string name="camcorder_unsupported_toast">No camera supports video recording.</string>
@@ -189,4 +180,9 @@
 
     <!-- Force torch help -->
     <string name="force_torch_help">On photo mode, you can long press the flash button to switch into torch mode.</string>
+
+    <!-- Camera modes -->
+    <string name="camera_mode_photo">Photo</string>
+    <string name="camera_mode_video">Video</string>
+    <string name="camera_mode_qr">Scan</string>
 </resources>
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 220d744..c015a64 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -90,14 +90,6 @@
         <item name="android:layout_height">40dp</item>
     </style>
 
-    <!-- Camera mode bar icons theme -->
-    <style name="Theme.Aperture.Camera.CameraModeSelectorButton" parent="@style/Widget.Material3.Button.TonalButton">
-        <item name="android:backgroundTint">@android:color/transparent</item>
-        <item name="android:textColor">@color/camera_mode_selector_text</item>
-        <item name="android:textStyle">bold</item>
-        <item name="android:typeface">monospace</item>
-    </style>
-
     <!-- Collapsing toolbar style -->
     <style name="Theme.Aperture.Camera.ToolbarCollapsed" parent="@android:style/TextAppearance.DeviceDefault.Widget.ActionBar.Title">
         <item name="android:fontFamily">sans-serif</item>