From bcaea201690da7e7439f52781da04b888b80f9c5 Mon Sep 17 00:00:00 2001
From: Andrew Gunnerson <chillermillerlong@hotmail.com>
Date: Mon, 30 May 2022 19:48:11 -0400
Subject: [PATCH 1/3] Add support for configurable sample rate

Signed-off-by: Andrew Gunnerson <chillermillerlong@hotmail.com>
---
 .../chiller3/bcr/FormatBottomSheetFragment.kt | 51 ++++++++++++++++---
 .../main/java/com/chiller3/bcr/Preferences.kt | 41 +++++++++++++++
 .../java/com/chiller3/bcr/RecorderThread.kt   | 14 +++--
 .../java/com/chiller3/bcr/SettingsActivity.kt | 13 +++--
 .../com/chiller3/bcr/format/SampleRates.kt    | 46 +++++++++++++++++
 .../main/res/layout/format_bottom_sheet.xml   | 14 +++++
 app/src/main/res/values/strings.xml           |  1 +
 7 files changed, 163 insertions(+), 17 deletions(-)
 create mode 100644 app/src/main/java/com/chiller3/bcr/format/SampleRates.kt

diff --git a/app/src/main/java/com/chiller3/bcr/FormatBottomSheetFragment.kt b/app/src/main/java/com/chiller3/bcr/FormatBottomSheetFragment.kt
index a89de929d..854e181e2 100644
--- a/app/src/main/java/com/chiller3/bcr/FormatBottomSheetFragment.kt
+++ b/app/src/main/java/com/chiller3/bcr/FormatBottomSheetFragment.kt
@@ -1,3 +1,6 @@
+@file:Suppress("OPT_IN_IS_NOT_ENABLED")
+@file:OptIn(ExperimentalUnsignedTypes::class)
+
 package com.chiller3.bcr
 
 import android.os.Bundle
@@ -10,12 +13,10 @@ import com.chiller3.bcr.databinding.FormatBottomSheetChipBinding
 import com.chiller3.bcr.format.*
 import com.google.android.material.bottomsheet.BottomSheetDialogFragment
 import com.google.android.material.chip.ChipGroup
-import com.google.android.material.slider.LabelFormatter
 import com.google.android.material.slider.Slider
 
 class FormatBottomSheetFragment : BottomSheetDialogFragment(),
-    ChipGroup.OnCheckedStateChangeListener, LabelFormatter, Slider.OnChangeListener,
-    View.OnClickListener {
+    ChipGroup.OnCheckedStateChangeListener, Slider.OnChangeListener, View.OnClickListener {
     private var _binding: FormatBottomSheetBinding? = null
     private val binding
         get() = _binding!!
@@ -31,9 +32,16 @@ class FormatBottomSheetFragment : BottomSheetDialogFragment(),
     ): View {
         _binding = FormatBottomSheetBinding.inflate(inflater, container, false)
 
-        binding.paramSlider.setLabelFormatter(this)
+        binding.paramSlider.setLabelFormatter {
+            formatParamInfo.format(it.toUInt())
+        }
         binding.paramSlider.addOnChangeListener(this)
 
+        binding.sampleRateSlider.setLabelFormatter {
+            SampleRates.format(sampleRateFromIndex(it.toInt()))
+        }
+        binding.sampleRateSlider.addOnChangeListener(this)
+
         binding.reset.setOnClickListener(this)
 
         for (format in Formats.all) {
@@ -55,6 +63,7 @@ class FormatBottomSheetFragment : BottomSheetDialogFragment(),
         binding.nameGroup.setOnCheckedStateChangeListener(this)
 
         refreshFormat()
+        refreshSampleRate()
 
         return binding.root
     }
@@ -109,35 +118,61 @@ class FormatBottomSheetFragment : BottomSheetDialogFragment(),
         }
     }
 
+    private fun refreshSampleRate() {
+        val sampleRate = SampleRates.fromPreferences(requireContext())
+
+        // Index == SampleRates.all.size is used to represent the native sample rate option
+        binding.sampleRateSlider.valueFrom = 0f
+        binding.sampleRateSlider.valueTo = SampleRates.all.size.toFloat()
+        binding.sampleRateSlider.stepSize = 1f
+
+        if (sampleRate == null) {
+            binding.sampleRateSlider.value = SampleRates.all.size.toFloat()
+        } else {
+            binding.sampleRateSlider.value = SampleRates.all.indexOf(sampleRate).toFloat()
+        }
+    }
+
     override fun onCheckedChanged(group: ChipGroup, checkedIds: MutableList<Int>) {
         Preferences.setFormatName(requireContext(), chipIdToFormat[checkedIds.first()]!!.name)
         refreshParam()
     }
 
-    override fun getFormattedValue(value: Float): String =
-        formatParamInfo.format(value.toUInt())
-
     override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
         when (slider) {
             binding.paramSlider -> {
                 val format = chipIdToFormat[binding.nameGroup.checkedChipId]!!
                 Preferences.setFormatParam(requireContext(), format.name, value.toUInt())
             }
+            binding.sampleRateSlider -> {
+                val sampleRate = sampleRateFromIndex(value.toInt())
+                Preferences.setSampleRate(requireContext(), sampleRate)
+            }
         }
     }
 
     override fun onClick(v: View?) {
         when (v) {
             binding.reset -> {
-                Preferences.resetAllFormats(requireContext())
+                val context = requireContext()
+                Preferences.resetAllFormats(context)
+                Preferences.setSampleRate(context, null)
                 refreshFormat()
                 // Need to explicitly refresh the parameter when the default format is already chosen
                 refreshParam()
+                refreshSampleRate()
             }
         }
     }
 
     companion object {
         val TAG: String = FormatBottomSheetFragment::class.java.simpleName
+
+        private fun sampleRateFromIndex(index: Int) =
+            if (index == SampleRates.all.size) {
+                null
+            } else {
+                SampleRates.all[index]
+            }
     }
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/chiller3/bcr/Preferences.kt b/app/src/main/java/com/chiller3/bcr/Preferences.kt
index f27100ccf..c1274b32f 100644
--- a/app/src/main/java/com/chiller3/bcr/Preferences.kt
+++ b/app/src/main/java/com/chiller3/bcr/Preferences.kt
@@ -16,6 +16,7 @@ object Preferences {
     // Not associated with a UI preference
     private const val PREF_FORMAT_NAME = "codec_name"
     private const val PREF_FORMAT_PARAM_PREFIX = "codec_param_"
+    const val PREF_SAMPLE_RATE = "sample_rate"
 
     /**
      * Get the default output directory. The directory should always be writable and is suitable for
@@ -184,4 +185,44 @@ object Preferences {
 
         editor.apply()
     }
+
+    /**
+     * Get the saved sample rate.
+     */
+    fun getSampleRate(context: Context): UInt? {
+        val prefs = PreferenceManager.getDefaultSharedPreferences(context)
+        // Use a sentinel value because doing contains + getInt results in TOCTOU issues
+        val value = prefs.getInt(PREF_SAMPLE_RATE, -1)
+
+        return if (value == -1) {
+            null
+        } else {
+            value.toUInt()
+        }
+    }
+
+    /**
+     * Set the sample rate.
+     *
+     * @param sampleRate Must not be [UInt.MAX_VALUE]
+     *
+     * @throws IllegalArgumentException if [sampleRate] is [UInt.MAX_VALUE]
+     */
+    fun setSampleRate(context: Context, sampleRate: UInt?) {
+        // -1 (when casted to int) is used as a sentinel value
+        if (sampleRate == UInt.MAX_VALUE) {
+            throw IllegalArgumentException("Sample rate cannot be ${UInt.MAX_VALUE}")
+        }
+
+        val prefs = PreferenceManager.getDefaultSharedPreferences(context)
+        val editor = prefs.edit()
+
+        if (sampleRate == null) {
+            editor.remove(PREF_SAMPLE_RATE)
+        } else {
+            editor.putInt(PREF_SAMPLE_RATE, sampleRate.toInt())
+        }
+
+        editor.apply()
+    }
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/chiller3/bcr/RecorderThread.kt b/app/src/main/java/com/chiller3/bcr/RecorderThread.kt
index e5e22b9b6..8b1e86cb1 100644
--- a/app/src/main/java/com/chiller3/bcr/RecorderThread.kt
+++ b/app/src/main/java/com/chiller3/bcr/RecorderThread.kt
@@ -13,6 +13,7 @@ import androidx.documentfile.provider.DocumentFile
 import com.chiller3.bcr.format.Container
 import com.chiller3.bcr.format.Format
 import com.chiller3.bcr.format.Formats
+import com.chiller3.bcr.format.SampleRates
 import java.io.IOException
 import java.lang.Integer.min
 import java.nio.ByteBuffer
@@ -54,6 +55,7 @@ class RecorderThread(
     // Format
     private val format: Format
     private val formatParam: UInt?
+    private val sampleRate = SampleRates.fromPreferences(context)
 
     init {
         logI("Created thread for call: $call")
@@ -261,10 +263,14 @@ class RecorderThread(
     private fun recordUntilCancelled(pfd: ParcelFileDescriptor) {
         AndroidProcess.setThreadPriority(AndroidProcess.THREAD_PRIORITY_AUDIO)
 
-        val audioFormat = AudioFormat.Builder()
-            .setEncoding(ENCODING)
-            .setChannelMask(CHANNEL_CONFIG)
-            .build()
+        val audioFormat = AudioFormat.Builder().run {
+            setEncoding(ENCODING)
+            setChannelMask(CHANNEL_CONFIG)
+            if (sampleRate != null) {
+                setSampleRate(sampleRate.toInt())
+            }
+            build()
+        }
         val audioRecord = AudioRecord.Builder()
             .setAudioSource(MediaRecorder.AudioSource.VOICE_CALL)
             .setAudioFormat(audioFormat)
diff --git a/app/src/main/java/com/chiller3/bcr/SettingsActivity.kt b/app/src/main/java/com/chiller3/bcr/SettingsActivity.kt
index edd6b41e9..281a83ce2 100644
--- a/app/src/main/java/com/chiller3/bcr/SettingsActivity.kt
+++ b/app/src/main/java/com/chiller3/bcr/SettingsActivity.kt
@@ -12,6 +12,7 @@ import androidx.preference.SwitchPreferenceCompat
 import com.chiller3.bcr.format.Formats
 import com.chiller3.bcr.format.NoParamInfo
 import com.chiller3.bcr.format.RangedParamInfo
+import com.chiller3.bcr.format.SampleRates
 
 class SettingsActivity : AppCompatActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
@@ -109,15 +110,17 @@ class SettingsActivity : AppCompatActivity() {
         }
 
         private fun refreshOutputFormat() {
-            val (format, formatParamSaved) = Formats.fromPreferences(requireContext())
+            val context = requireContext()
+            val (format, formatParamSaved) = Formats.fromPreferences(context)
             val formatParam = formatParamSaved ?: format.paramInfo.default
             val summary = getString(R.string.pref_output_format_desc)
-            val suffix = when (val info = format.paramInfo) {
-                is RangedParamInfo -> " (${info.format(formatParam)})"
+            val prefix = when (val info = format.paramInfo) {
+                is RangedParamInfo -> "${info.format(formatParam)}, "
                 NoParamInfo -> ""
             }
+            val sampleRate = SampleRates.format(SampleRates.fromPreferences(context))
 
-            prefOutputFormat.summary = "${summary}\n\n${format.name}${suffix}"
+            prefOutputFormat.summary = "${summary}\n\n${format.name} (${prefix}${sampleRate})"
         }
 
         private fun refreshInhibitBatteryOptState() {
@@ -193,7 +196,7 @@ class SettingsActivity : AppCompatActivity() {
                     }
                 }
                 // Update the output format state when it's changed by the bottom sheet
-                Preferences.isFormatKey(key) -> {
+                Preferences.isFormatKey(key) || key == Preferences.PREF_SAMPLE_RATE -> {
                     refreshOutputFormat()
                 }
             }
diff --git a/app/src/main/java/com/chiller3/bcr/format/SampleRates.kt b/app/src/main/java/com/chiller3/bcr/format/SampleRates.kt
new file mode 100644
index 000000000..3c7af22ae
--- /dev/null
+++ b/app/src/main/java/com/chiller3/bcr/format/SampleRates.kt
@@ -0,0 +1,46 @@
+@file:Suppress("OPT_IN_IS_NOT_ENABLED")
+@file:OptIn(ExperimentalUnsignedTypes::class)
+
+package com.chiller3.bcr.format
+
+import android.content.Context
+import com.chiller3.bcr.Preferences
+
+object SampleRates {
+    /**
+     * Hardcoded list of sample rates supported by every [Format].
+     *
+     * Ideally, there would be a way to query what sample rates are supported for a given audio
+     * source and then filter that list based on what the [Format] supports. Unfortunately, no such
+     * API exists.
+     */
+    val all = uintArrayOf(
+        8_000u,
+        12_000u,
+        16_000u,
+        24_000u,
+        48_000u,
+    )
+
+    /**
+     * Get the saved sample rate from the preferences.
+     *
+     * If the saved sample rate is no longer valid, then null is returned.
+     */
+    fun fromPreferences(context: Context): UInt? {
+        val savedSampleRate = Preferences.getSampleRate(context)
+
+        if (savedSampleRate != null && all.contains(savedSampleRate)) {
+            return savedSampleRate
+        }
+
+        return null
+    }
+
+    fun format(sampleRate: UInt?): String =
+        if (sampleRate == null) {
+            "native"
+        } else {
+            "$sampleRate Hz"
+        }
+}
\ No newline at end of file
diff --git a/app/src/main/res/layout/format_bottom_sheet.xml b/app/src/main/res/layout/format_bottom_sheet.xml
index f28479448..81e11e636 100644
--- a/app/src/main/res/layout/format_bottom_sheet.xml
+++ b/app/src/main/res/layout/format_bottom_sheet.xml
@@ -42,6 +42,20 @@
             app:labelBehavior="visible" />
     </LinearLayout>
 
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/bottom_sheet_section_separation"
+        android:layout_marginBottom="@dimen/bottom_sheet_title_margin_bottom"
+        android:text="@string/bottom_sheet_sample_rate"
+        android:textAppearance="?attr/textAppearanceHeadline6" />
+
+    <com.google.android.material.slider.Slider
+        android:id="@+id/sample_rate_slider"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        app:labelBehavior="visible" />
+
     <com.google.android.material.button.MaterialButton
         android:id="@+id/reset"
         android:layout_width="wrap_content"
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index cda8c0d3c..6502b06a4 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -26,6 +26,7 @@
     <string name="bottom_sheet_output_format">Output format</string>
     <string name="bottom_sheet_compression_level">Compression level</string>
     <string name="bottom_sheet_bitrate">Bitrate</string>
+    <string name="bottom_sheet_sample_rate">Sample rate</string>
     <string name="bottom_sheet_reset">Reset to defaults</string>
 
     <!-- Notifications -->

From b65727cf562debc8a1ce4095f125ada1b265cf9f Mon Sep 17 00:00:00 2001
From: Andrew Gunnerson <chillermillerlong@hotmail.com>
Date: Mon, 30 May 2022 20:13:54 -0400
Subject: [PATCH 2/3] Use chips for sample rate instead of a second slider

Signed-off-by: Andrew Gunnerson <chillermillerlong@hotmail.com>
---
 .../chiller3/bcr/FormatBottomSheetFragment.kt |  91 +++++++++-------
 .../java/com/chiller3/bcr/SettingsActivity.kt |   2 +-
 .../com/chiller3/bcr/format/SampleRates.kt    |   5 +-
 .../main/res/layout/format_bottom_sheet.xml   | 100 ++++++++++--------
 app/src/main/res/values/strings.xml           |   1 +
 5 files changed, 108 insertions(+), 91 deletions(-)

diff --git a/app/src/main/java/com/chiller3/bcr/FormatBottomSheetFragment.kt b/app/src/main/java/com/chiller3/bcr/FormatBottomSheetFragment.kt
index 854e181e2..ac92cc331 100644
--- a/app/src/main/java/com/chiller3/bcr/FormatBottomSheetFragment.kt
+++ b/app/src/main/java/com/chiller3/bcr/FormatBottomSheetFragment.kt
@@ -25,6 +25,9 @@ class FormatBottomSheetFragment : BottomSheetDialogFragment(),
     private val formatToChipId = HashMap<Format, Int>()
     private lateinit var formatParamInfo: FormatParamInfo
 
+    private val chipIdToSampleRate = HashMap<Int, UInt?>()
+    private val sampleRateToChipId = HashMap<UInt?, Int>()
+
     override fun onCreateView(
         inflater: LayoutInflater,
         container: ViewGroup?,
@@ -37,11 +40,6 @@ class FormatBottomSheetFragment : BottomSheetDialogFragment(),
         }
         binding.paramSlider.addOnChangeListener(this)
 
-        binding.sampleRateSlider.setLabelFormatter {
-            SampleRates.format(sampleRateFromIndex(it.toInt()))
-        }
-        binding.sampleRateSlider.addOnChangeListener(this)
-
         binding.reset.setOnClickListener(this)
 
         for (format in Formats.all) {
@@ -49,19 +47,18 @@ class FormatBottomSheetFragment : BottomSheetDialogFragment(),
                 continue
             }
 
-            val chipBinding = FormatBottomSheetChipBinding.inflate(
-                inflater, binding.nameGroup, false)
-            val id = View.generateViewId()
-            chipBinding.root.id = id
-            chipBinding.root.text = format.name
-            chipBinding.root.layoutDirection = View.LAYOUT_DIRECTION_LOCALE
-            binding.nameGroup.addView(chipBinding.root)
-            chipIdToFormat[id] = format
-            formatToChipId[format] = id
+            addFormatChip(inflater, format)
         }
 
         binding.nameGroup.setOnCheckedStateChangeListener(this)
 
+        addSampleRateChip(inflater, null)
+        for (sampleRate in SampleRates.all) {
+            addSampleRateChip(inflater, sampleRate)
+        }
+
+        binding.sampleRateGroup.setOnCheckedStateChangeListener(this)
+
         refreshFormat()
         refreshSampleRate()
 
@@ -73,6 +70,30 @@ class FormatBottomSheetFragment : BottomSheetDialogFragment(),
         _binding = null
     }
 
+    private fun addFormatChip(inflater: LayoutInflater, format: Format) {
+        val chipBinding = FormatBottomSheetChipBinding.inflate(
+            inflater, binding.nameGroup, false)
+        val id = View.generateViewId()
+        chipBinding.root.id = id
+        chipBinding.root.text = format.name
+        chipBinding.root.layoutDirection = View.LAYOUT_DIRECTION_LOCALE
+        binding.nameGroup.addView(chipBinding.root)
+        chipIdToFormat[id] = format
+        formatToChipId[format] = id
+    }
+
+    private fun addSampleRateChip(inflater: LayoutInflater, sampleRate: UInt?) {
+        val chipBinding = FormatBottomSheetChipBinding.inflate(
+            inflater, binding.sampleRateGroup, false)
+        val id = View.generateViewId()
+        chipBinding.root.id = id
+        chipBinding.root.text = SampleRates.format(requireContext(), sampleRate)
+        chipBinding.root.layoutDirection = View.LAYOUT_DIRECTION_LOCALE
+        binding.sampleRateGroup.addView(chipBinding.root)
+        chipIdToSampleRate[id] = sampleRate
+        sampleRateToChipId[sampleRate] = id
+    }
+
     /**
      * Update UI based on currently selected format in the preferences.
      *
@@ -83,6 +104,11 @@ class FormatBottomSheetFragment : BottomSheetDialogFragment(),
         binding.nameGroup.check(formatToChipId[format]!!)
     }
 
+    private fun refreshSampleRate() {
+        val sampleRate = SampleRates.fromPreferences(requireContext())
+        binding.sampleRateGroup.check(sampleRateToChipId[sampleRate]!!)
+    }
+
     /**
      * Update parameter title and slider to match format parameter specifications.
      */
@@ -118,36 +144,26 @@ class FormatBottomSheetFragment : BottomSheetDialogFragment(),
         }
     }
 
-    private fun refreshSampleRate() {
-        val sampleRate = SampleRates.fromPreferences(requireContext())
-
-        // Index == SampleRates.all.size is used to represent the native sample rate option
-        binding.sampleRateSlider.valueFrom = 0f
-        binding.sampleRateSlider.valueTo = SampleRates.all.size.toFloat()
-        binding.sampleRateSlider.stepSize = 1f
+    override fun onCheckedChanged(group: ChipGroup, checkedIds: MutableList<Int>) {
+        val context = requireContext()
 
-        if (sampleRate == null) {
-            binding.sampleRateSlider.value = SampleRates.all.size.toFloat()
-        } else {
-            binding.sampleRateSlider.value = SampleRates.all.indexOf(sampleRate).toFloat()
+        when (group) {
+            binding.nameGroup -> {
+                Preferences.setFormatName(context, chipIdToFormat[checkedIds.first()]!!.name)
+                refreshParam()
+            }
+            binding.sampleRateGroup -> {
+                Preferences.setSampleRate(context, chipIdToSampleRate[checkedIds.first()])
+            }
         }
     }
 
-    override fun onCheckedChanged(group: ChipGroup, checkedIds: MutableList<Int>) {
-        Preferences.setFormatName(requireContext(), chipIdToFormat[checkedIds.first()]!!.name)
-        refreshParam()
-    }
-
     override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
         when (slider) {
             binding.paramSlider -> {
                 val format = chipIdToFormat[binding.nameGroup.checkedChipId]!!
                 Preferences.setFormatParam(requireContext(), format.name, value.toUInt())
             }
-            binding.sampleRateSlider -> {
-                val sampleRate = sampleRateFromIndex(value.toInt())
-                Preferences.setSampleRate(requireContext(), sampleRate)
-            }
         }
     }
 
@@ -167,12 +183,5 @@ class FormatBottomSheetFragment : BottomSheetDialogFragment(),
 
     companion object {
         val TAG: String = FormatBottomSheetFragment::class.java.simpleName
-
-        private fun sampleRateFromIndex(index: Int) =
-            if (index == SampleRates.all.size) {
-                null
-            } else {
-                SampleRates.all[index]
-            }
     }
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/chiller3/bcr/SettingsActivity.kt b/app/src/main/java/com/chiller3/bcr/SettingsActivity.kt
index 281a83ce2..08871a895 100644
--- a/app/src/main/java/com/chiller3/bcr/SettingsActivity.kt
+++ b/app/src/main/java/com/chiller3/bcr/SettingsActivity.kt
@@ -118,7 +118,7 @@ class SettingsActivity : AppCompatActivity() {
                 is RangedParamInfo -> "${info.format(formatParam)}, "
                 NoParamInfo -> ""
             }
-            val sampleRate = SampleRates.format(SampleRates.fromPreferences(context))
+            val sampleRate = SampleRates.format(context, SampleRates.fromPreferences(context))
 
             prefOutputFormat.summary = "${summary}\n\n${format.name} (${prefix}${sampleRate})"
         }
diff --git a/app/src/main/java/com/chiller3/bcr/format/SampleRates.kt b/app/src/main/java/com/chiller3/bcr/format/SampleRates.kt
index 3c7af22ae..2ff4a8251 100644
--- a/app/src/main/java/com/chiller3/bcr/format/SampleRates.kt
+++ b/app/src/main/java/com/chiller3/bcr/format/SampleRates.kt
@@ -5,6 +5,7 @@ package com.chiller3.bcr.format
 
 import android.content.Context
 import com.chiller3.bcr.Preferences
+import com.chiller3.bcr.R
 
 object SampleRates {
     /**
@@ -37,9 +38,9 @@ object SampleRates {
         return null
     }
 
-    fun format(sampleRate: UInt?): String =
+    fun format(context: Context, sampleRate: UInt?): String =
         if (sampleRate == null) {
-            "native"
+            context.getString(R.string.bottom_sheet_sample_rate_native)
         } else {
             "$sampleRate Hz"
         }
diff --git a/app/src/main/res/layout/format_bottom_sheet.xml b/app/src/main/res/layout/format_bottom_sheet.xml
index 81e11e636..dd476a745 100644
--- a/app/src/main/res/layout/format_bottom_sheet.xml
+++ b/app/src/main/res/layout/format_bottom_sheet.xml
@@ -1,66 +1,72 @@
 <?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:orientation="vertical"
-    android:gravity="center_horizontal"
-    android:padding="@dimen/bottom_sheet_overall_padding">
-
-    <TextView
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginBottom="@dimen/bottom_sheet_title_margin_bottom"
-        android:text="@string/bottom_sheet_output_format"
-        android:textAppearance="?attr/textAppearanceHeadline6" />
-
-    <com.chiller3.bcr.ChipGroupCentered
-        android:id="@+id/name_group"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        app:selectionRequired="true"
-        app:singleSelection="true" />
+    android:layout_height="wrap_content">
 
     <LinearLayout
-        android:id="@+id/param_group"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:orientation="vertical"
-        android:gravity="center_horizontal">
+        android:gravity="center_horizontal"
+        android:padding="@dimen/bottom_sheet_overall_padding">
+
         <TextView
-            android:id="@+id/param_title"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_marginTop="@dimen/bottom_sheet_section_separation"
             android:layout_marginBottom="@dimen/bottom_sheet_title_margin_bottom"
+            android:text="@string/bottom_sheet_output_format"
             android:textAppearance="?attr/textAppearanceHeadline6" />
 
-        <com.google.android.material.slider.Slider
-            android:id="@+id/param_slider"
+        <com.chiller3.bcr.ChipGroupCentered
+            android:id="@+id/name_group"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            app:labelBehavior="visible" />
-    </LinearLayout>
+            app:selectionRequired="true"
+            app:singleSelection="true" />
 
-    <TextView
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="@dimen/bottom_sheet_section_separation"
-        android:layout_marginBottom="@dimen/bottom_sheet_title_margin_bottom"
-        android:text="@string/bottom_sheet_sample_rate"
-        android:textAppearance="?attr/textAppearanceHeadline6" />
+        <LinearLayout
+            android:id="@+id/param_group"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical"
+            android:gravity="center_horizontal">
+            <TextView
+                android:id="@+id/param_title"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="@dimen/bottom_sheet_section_separation"
+                android:layout_marginBottom="@dimen/bottom_sheet_title_margin_bottom"
+                android:textAppearance="?attr/textAppearanceHeadline6" />
 
-    <com.google.android.material.slider.Slider
-        android:id="@+id/sample_rate_slider"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        app:labelBehavior="visible" />
+            <com.google.android.material.slider.Slider
+                android:id="@+id/param_slider"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                app:labelBehavior="visible" />
+        </LinearLayout>
 
-    <com.google.android.material.button.MaterialButton
-        android:id="@+id/reset"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="@dimen/bottom_sheet_section_separation"
-        android:text="@string/bottom_sheet_reset"
-        style="?attr/materialButtonOutlinedStyle" />
-</LinearLayout>
\ No newline at end of file
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="@dimen/bottom_sheet_section_separation"
+            android:layout_marginBottom="@dimen/bottom_sheet_title_margin_bottom"
+            android:text="@string/bottom_sheet_sample_rate"
+            android:textAppearance="?attr/textAppearanceHeadline6" />
+
+        <com.chiller3.bcr.ChipGroupCentered
+            android:id="@+id/sample_rate_group"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:selectionRequired="true"
+            app:singleSelection="true" />
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/reset"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="@dimen/bottom_sheet_section_separation"
+            android:text="@string/bottom_sheet_reset"
+            style="?attr/materialButtonOutlinedStyle" />
+    </LinearLayout>
+</androidx.core.widget.NestedScrollView>
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 6502b06a4..d9278a5db 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -27,6 +27,7 @@
     <string name="bottom_sheet_compression_level">Compression level</string>
     <string name="bottom_sheet_bitrate">Bitrate</string>
     <string name="bottom_sheet_sample_rate">Sample rate</string>
+    <string name="bottom_sheet_sample_rate_native">Native sample rate</string>
     <string name="bottom_sheet_reset">Reset to defaults</string>
 
     <!-- Notifications -->

From 057a5fef49a8f4f447ba1f4ee767878287d8ce36 Mon Sep 17 00:00:00 2001
From: Andrew Gunnerson <chillermillerlong@hotmail.com>
Date: Mon, 30 May 2022 20:15:36 -0400
Subject: [PATCH 3/3] changelog.txt: Add entry for PR #56

Signed-off-by: Andrew Gunnerson <chillermillerlong@hotmail.com>
---
 app/magisk/updates/release/changelog.txt | 1 +
 1 file changed, 1 insertion(+)

diff --git a/app/magisk/updates/release/changelog.txt b/app/magisk/updates/release/changelog.txt
index 9bef52e8c..3cd648379 100644
--- a/app/magisk/updates/release/changelog.txt
+++ b/app/magisk/updates/release/changelog.txt
@@ -1,6 +1,7 @@
 ### Unreleased
 
 * Change output format button group to material chips to prevent text from being cut off with narrower screen widths (Issue: #52, PR: #55, @chenxiaolong)
+* Add support for configuring the capture sample rate (PR: #56, @chenxiaolong)
 
 ### Version 1.6