Aperture: Implement shutter sound

Change-Id: I3716739c616bc49901f96922cd0e8000b2460747
diff --git a/app/src/main/java/org/lineageos/aperture/MainActivity.kt b/app/src/main/java/org/lineageos/aperture/MainActivity.kt
index 1536f30..43fa9e6 100644
--- a/app/src/main/java/org/lineageos/aperture/MainActivity.kt
+++ b/app/src/main/java/org/lineageos/aperture/MainActivity.kt
@@ -61,6 +61,7 @@
 import org.lineageos.aperture.ui.GridView
 import org.lineageos.aperture.utils.CameraFacing
 import org.lineageos.aperture.utils.CameraMode
+import org.lineageos.aperture.utils.CameraSoundsUtils
 import org.lineageos.aperture.utils.GridMode
 import org.lineageos.aperture.utils.PhysicalCamera
 import org.lineageos.aperture.utils.StorageUtils
@@ -126,6 +127,8 @@
         PreferenceManager.getDefaultSharedPreferences(this)
     }
 
+    private lateinit var cameraSoundsUtils: CameraSoundsUtils
+
     private val handler = object : Handler(Looper.getMainLooper()) {
         override fun handleMessage(msg: Message) {
             super.handleMessage(msg)
@@ -168,6 +171,9 @@
         // Get vendor extensions manager
         extensionsManager = ExtensionsManager.getInstanceAsync(this, cameraProvider).get()
 
+        // Initialize sounds utils
+        cameraSoundsUtils = CameraSoundsUtils(sharedPreferences)
+
         // Bind camera controller to lifecycle
         cameraController.bindToLifecycle(this)
 
@@ -258,7 +264,7 @@
             startTimerAndRun {
                 when (cameraMode) {
                     CameraMode.PHOTO -> takePhoto()
-                    CameraMode.VIDEO -> captureVideo()
+                    CameraMode.VIDEO -> lifecycleScope.launch { captureVideo() }
                     else -> {}
                 }
             }
@@ -332,6 +338,7 @@
                 }
 
                 override fun onImageSaved(output: ImageCapture.OutputFileResults) {
+                    cameraSoundsUtils.playShutterClick()
                     viewFinder.foreground = ColorDrawable(Color.BLACK)
                     ValueAnimator.ofInt(0, 255, 0).apply {
                         addUpdateListener { anim ->
@@ -351,7 +358,7 @@
     }
 
     @androidx.camera.view.video.ExperimentalVideo
-    private fun captureVideo() {
+    private suspend fun captureVideo() {
         if (cameraController.isRecording) {
             // Stop the current recording session.
             cameraController.stopRecording()
@@ -361,12 +368,19 @@
         // Create output options object which contains file + metadata
         val outputOptions = StorageUtils.getVideoMediaStoreOutputOptions(contentResolver)
 
+        // Play shutter sound
+        if (cameraSoundsUtils.playStartVideoRecording()) {
+            // Delay startRecording() by 500ms to avoid recording shutter sound
+            delay(500)
+        }
+
         // Start recording
         cameraController.startRecording(
             outputOptions,
             cameraExecutor,
             object : OnVideoSavedCallback {
                 override fun onVideoSaved(output: OutputFileResults) {
+                    cameraSoundsUtils.playStopVideoRecording()
                     stopRecordingTimer()
                     val msg = "Video capture succeeded: ${output.savedUri}"
                     sharedPreferences.lastSavedUri = output.savedUri
@@ -376,6 +390,7 @@
                 }
 
                 override fun onError(videoCaptureError: Int, message: String, cause: Throwable?) {
+                    cameraSoundsUtils.playStopVideoRecording()
                     stopRecordingTimer()
                     Log.e(LOG_TAG, "Video capture ends with error: $message")
                 }
diff --git a/app/src/main/java/org/lineageos/aperture/SettingsActivity.kt b/app/src/main/java/org/lineageos/aperture/SettingsActivity.kt
index 5e4ecdd..437b40e 100644
--- a/app/src/main/java/org/lineageos/aperture/SettingsActivity.kt
+++ b/app/src/main/java/org/lineageos/aperture/SettingsActivity.kt
@@ -10,6 +10,8 @@
 import android.view.MenuItem
 import androidx.appcompat.app.AppCompatActivity
 import androidx.preference.PreferenceFragmentCompat
+import androidx.preference.SwitchPreference
+import org.lineageos.aperture.utils.CameraSoundsUtils
 
 class SettingsActivity : AppCompatActivity() {
 
@@ -36,8 +38,11 @@
     }
 
     class SettingsFragment : PreferenceFragmentCompat() {
+        private val shutterSound by lazy { findPreference<SwitchPreference>("shutter_sound") }
+
         override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
             setPreferencesFromResource(R.xml.root_preferences, rootKey)
+            shutterSound?.isEnabled = !CameraSoundsUtils.mustPlaySounds
         }
     }
 }
\ No newline at end of file
diff --git a/app/src/main/java/org/lineageos/aperture/SharedPreferencesExt.kt b/app/src/main/java/org/lineageos/aperture/SharedPreferencesExt.kt
index 3cb445c..1b090a8 100644
--- a/app/src/main/java/org/lineageos/aperture/SharedPreferencesExt.kt
+++ b/app/src/main/java/org/lineageos/aperture/SharedPreferencesExt.kt
@@ -271,6 +271,20 @@
         }
     }
 
+// Shutter sound
+private const val SHUTTER_SOUND_KEY = "shutter_sound"
+private const val SHUTTER_SOUND_DEFAULT = true
+
+internal var SharedPreferences.shutterSound: Boolean
+    get() {
+        return getBoolean(SHUTTER_SOUND_KEY, SHUTTER_SOUND_DEFAULT)
+    }
+    set(value) {
+        edit {
+            putBoolean(SHUTTER_SOUND_KEY, value)
+        }
+    }
+
 // Last saved URI
 private const val LAST_SAVED_URI_KEY = "saved_uri"
 
diff --git a/app/src/main/java/org/lineageos/aperture/utils/CameraSoundsUtils.kt b/app/src/main/java/org/lineageos/aperture/utils/CameraSoundsUtils.kt
new file mode 100644
index 0000000..d6d896e
--- /dev/null
+++ b/app/src/main/java/org/lineageos/aperture/utils/CameraSoundsUtils.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2022 The LineageOS Project
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.aperture.utils
+
+import android.content.res.Resources
+import android.content.SharedPreferences
+import android.media.MediaActionSound
+import org.lineageos.aperture.shutterSound
+
+class CameraSoundsUtils(private val sharedPreferences: SharedPreferences) {
+    private val mediaActionSound = MediaActionSound().apply {
+        // Preload all sounds to reduce latency
+        load(MediaActionSound.SHUTTER_CLICK)
+        load(MediaActionSound.START_VIDEO_RECORDING)
+        load(MediaActionSound.STOP_VIDEO_RECORDING)
+    }
+
+    fun playShutterClick() {
+        if (sharedPreferences.shutterSound || mustPlaySounds) {
+            mediaActionSound.play(MediaActionSound.SHUTTER_CLICK)
+        }
+    }
+
+    fun playStartVideoRecording(): Boolean {
+        if (sharedPreferences.shutterSound || mustPlaySounds) {
+            mediaActionSound.play(MediaActionSound.START_VIDEO_RECORDING)
+            return true
+        }
+        return false
+    }
+
+    fun playStopVideoRecording() {
+        if (sharedPreferences.shutterSound || mustPlaySounds) {
+            mediaActionSound.play(MediaActionSound.STOP_VIDEO_RECORDING)
+        }
+    }
+
+    companion object {
+        val mustPlaySounds: Boolean
+            get() {
+                val resources = Resources.getSystem()
+                val id = resources.getIdentifier("config_camera_sound_forced", "bool", "android")
+                return id > 0 && resources.getBoolean(id)
+            }
+    }
+}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 4606547..dcb3283 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -27,9 +27,14 @@
     <string name="title_activity_settings">Settings</string>
 
     <!-- Preference Titles -->
+    <string name="general_header">General</string>
     <string name="photos_header">Photos</string>
     <string name="videos_header">Videos</string>
 
+    <!-- General Preferences -->
+    <string name="shutter_sound_title">Shutter sound</string>
+    <string name="shutter_sound_summary">Note: In some countries this setting will be ignored</string>
+
     <!-- Photo Preferences -->
     <string name="photo_capture_mode_title">Capture mode</string>
 
diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml
index 5d88563..c2611f4 100644
--- a/app/src/main/res/xml/root_preferences.xml
+++ b/app/src/main/res/xml/root_preferences.xml
@@ -1,5 +1,15 @@
 <PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
 
+    <PreferenceCategory app:title="@string/general_header">
+
+        <SwitchPreference
+            app:defaultValue="true"
+            app:key="shutter_sound"
+            app:title="@string/shutter_sound_title"
+            app:summary="@string/shutter_sound_summary" />
+
+    </PreferenceCategory>
+
     <PreferenceCategory app:title="@string/photos_header">
 
         <ListPreference