Add an option to choose a custom beep tone

As requested in issue #642.
This commit is contained in:
Markus Fisch
2026-06-01 22:39:11 +02:00
parent 2035c935dc
commit 13eabaee19
4 changed files with 195 additions and 2 deletions
@@ -5,10 +5,15 @@ import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.database.Cursor
import android.net.Uri
import android.net.wifi.WifiManager
import android.os.Build
import android.os.Bundle
import android.provider.OpenableColumns
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.core.net.toUri
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
@@ -27,6 +32,7 @@ import de.markusfisch.android.binaryeye.app.hasBluetoothPermission
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.bluetooth.setBluetoothHosts
import de.markusfisch.android.binaryeye.media.beepConfirm
import de.markusfisch.android.binaryeye.preference.Preferences
import de.markusfisch.android.binaryeye.preference.UrlPreference
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.view.systemBarRecyclerViewScrollListener
@@ -34,6 +40,13 @@ import de.markusfisch.android.binaryeye.widget.toast
class PreferencesFragment : PreferenceFragmentCompat() {
private var lastProfile: String? = null
private val pickBeepTone = registerForActivityResult(
ActivityResultContracts.OpenDocument()
) { uri ->
if (uri != null) {
setCustomBeepTone(uri)
}
}
private val changeListener = object : OnSharedPreferenceChangeListener {
override fun onSharedPreferenceChanged(
@@ -87,6 +100,7 @@ class PreferencesFragment : PreferenceFragmentCompat() {
setSummaries(preferenceScreen)
setIcons()
wireBeepTonePicker()
wireProfiles()
wireAutomatedActions()
wireIgnoreCodes()
@@ -107,6 +121,27 @@ class PreferencesFragment : PreferenceFragmentCompat() {
}
}
private fun wireBeepTonePicker() {
findPreference<ListPreference>(BEEP_TONE_NAME)?.apply {
onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
val toneName = newValue as? String ?: return@OnPreferenceChangeListener false
if (toneName == Preferences.CUSTOM_BEEP_TONE_NAME) {
pickBeepTone.launch(arrayOf("audio/*"))
false
} else {
if (prefs.usesCustomBeepTone()) {
clearCustomBeepTone()
}
prefs.beepToneName = toneName
value = toneName
setSummary(this)
activity?.beepConfirm()
false
}
}
}
}
private fun wireAutomatedActions() {
findPreference<Preference>(AUTOMATED_ACTIONS)?.apply {
updateAutomatedActionsSummary(this)
@@ -158,7 +193,7 @@ class PreferencesFragment : PreferenceFragmentCompat() {
// adding network suggestions on Q as well, so we
// need to keep this option.
setOnPreferenceClickListener {
context?.askToClearNetworkSuggestions()
context.askToClearNetworkSuggestions()
true
}
} else {
@@ -247,6 +282,10 @@ class PreferencesFragment : PreferenceFragmentCompat() {
when (preference.key) {
AUTOMATED_ACTIONS -> return updateAutomatedActionsSummary(preference)
IGNORE_CODES -> return updateIgnoreCodesSummary(preference)
BEEP_TONE_NAME -> {
setBeepToneSummary(preference)
return
}
}
when (preference) {
is EditTextPreference -> preference.summary = preference.text
@@ -259,6 +298,21 @@ class PreferencesFragment : PreferenceFragmentCompat() {
}
}
private fun setBeepToneSummary(preference: Preference) {
preference.summary = if (prefs.usesCustomBeepTone()) {
getBeepToneLabel(prefs.beepToneUri.toUri())
} else {
findPreference<ListPreference>(BEEP_TONE_NAME)?.let {
val index = it.findIndexOfValue(prefs.beepToneName)
if (index >= 0) {
it.entries[index]
} else {
""
}
} ?: ""
}
}
private fun getSortedSummary(preference: MultiSelectListPreference): String {
val labelsByValue = mutableMapOf<String, String>()
val entries = preference.entries
@@ -353,6 +407,73 @@ class PreferencesFragment : PreferenceFragmentCompat() {
lastProfile = prefs.profile
}
private fun setCustomBeepTone(uri: Uri) {
val context = requireContext()
val resolver = context.contentResolver
clearCustomBeepTone()
try {
resolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
} catch (e: SecurityException) {
val message = e.message
if (!message.isNullOrEmpty()) {
context.toast(message)
}
return
}
prefs.beepToneUri = uri.toString()
prefs.beepToneName = Preferences.CUSTOM_BEEP_TONE_NAME
findPreference<ListPreference>(BEEP_TONE_NAME)?.apply {
value = Preferences.CUSTOM_BEEP_TONE_NAME
setSummary(this)
}
activity?.beepConfirm()
}
private fun clearCustomBeepTone(releaseSelection: Boolean = true) {
if (releaseSelection) {
releaseBeepTonePermission(prefs.beepToneUri)
}
prefs.beepToneUri = ""
}
private fun releaseBeepTonePermission(uriString: String) {
if (uriString.isEmpty()) {
return
}
try {
requireContext().contentResolver.releasePersistableUriPermission(
uriString.toUri(),
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
} catch (_: SecurityException) {
}
}
private fun getBeepToneLabel(uri: Uri): String {
var cursor: Cursor? = null
return try {
cursor = requireContext().contentResolver.query(
uri,
arrayOf(OpenableColumns.DISPLAY_NAME),
null,
null,
null
)
if (cursor?.moveToFirst() == true) {
cursor.getString(0)
} else {
uri.lastPathSegment ?: uri.toString()
}
} catch (_: Exception) {
uri.lastPathSegment ?: uri.toString()
} finally {
cursor?.close()
}
}
companion object {
private const val PROFILE = "profile"
private const val AUTOMATED_ACTIONS = "automated_actions"
@@ -1,15 +1,25 @@
package de.markusfisch.android.binaryeye.media
import android.content.Context
import android.media.AudioAttributes
import android.media.AudioManager
import android.media.MediaPlayer
import android.media.ToneGenerator
import androidx.core.net.toUri
import de.markusfisch.android.binaryeye.app.prefs
private var confirmToneGenerator: ToneGenerator? = null
private var confirmToneStream = AudioManager.STREAM_MUSIC
private var confirmMediaPlayer: MediaPlayer? = null
private var errorToneGenerator: ToneGenerator? = null
fun beepConfirm() {
fun Context.beepConfirm() {
val stream = prefs.beepStream()
if (prefs.usesCustomBeepTone() &&
playCustomConfirmTone(prefs.beepToneUri, stream)
) {
return
}
val tg = if (confirmToneGenerator == null || confirmToneStream != stream) {
confirmToneGenerator?.release()
ToneGenerator(stream, ToneGenerator.MAX_VOLUME).also {
@@ -23,6 +33,48 @@ fun beepConfirm() {
tg.startTone(prefs.beepTone())
}
private fun Context.playCustomConfirmTone(
uriString: String,
stream: Int
): Boolean {
return try {
confirmToneGenerator?.release()
confirmToneGenerator = null
confirmToneStream = stream
confirmMediaPlayer?.release()
confirmMediaPlayer = MediaPlayer().also { mp ->
mp.apply {
setAudioAttributes(
AudioAttributes.Builder()
.setLegacyStreamType(stream)
.build()
)
setDataSource(this@playCustomConfirmTone, uriString.toUri())
setOnCompletionListener { completed ->
if (confirmMediaPlayer === completed) {
confirmMediaPlayer = null
}
completed.release()
}
setOnErrorListener { failed, _, _ ->
if (confirmMediaPlayer === failed) {
confirmMediaPlayer = null
}
failed.release()
true
}
prepare()
start()
}
}
true
} catch (_: Exception) {
confirmMediaPlayer?.release()
confirmMediaPlayer = null
false
}
}
fun beepError() {
val tg = errorToneGenerator ?: ToneGenerator(
AudioManager.STREAM_ALARM,
@@ -36,6 +88,8 @@ fun releaseToneGenerators() {
confirmToneGenerator?.release()
confirmToneGenerator = null
confirmToneStream = AudioManager.STREAM_MUSIC
confirmMediaPlayer?.release()
confirmMediaPlayer = null
errorToneGenerator?.release()
errorToneGenerator = null
}
@@ -129,6 +129,11 @@ class Preferences {
apply(BEEP_STREAM_NAME, value)
field = value
}
var beepToneUri = ""
set(value) {
apply(BEEP_TONE_URI, value)
field = value
}
var useHistory = false
set(value) {
apply(USE_HISTORY, value)
@@ -373,6 +378,7 @@ class Preferences {
json.put(BEEP, p, defaults.beep)
json.put(BEEP_TONE_NAME, p, defaults.beepToneName)
json.put(BEEP_STREAM_NAME, p, defaults.beepStreamName)
json.put(BEEP_TONE_URI, p, defaults.beepToneUri)
json.put(USE_HISTORY, p, defaults.useHistory)
json.put(IGNORE_DUPLICATES_NAME, p, defaults.ignoreDuplicatesName)
json.put(IGNORE_CODES, p, ignoreCodesToJsonArray(defaults.ignoreCodes))
@@ -471,6 +477,7 @@ class Preferences {
putBoolean(BEEP, json)
putString(BEEP_TONE_NAME, json)
putString(BEEP_STREAM_NAME, json)
putString(BEEP_TONE_URI, json)
putBoolean(USE_HISTORY, json)
putString(IGNORE_DUPLICATES_NAME, json)
putString(IGNORE_CODES, json)
@@ -562,6 +569,9 @@ class Preferences {
preferences.getString(BEEP_STREAM_NAME, beepStreamName)?.also {
beepStreamName = it
}
preferences.getString(BEEP_TONE_URI, beepToneUri)?.also {
beepToneUri = it
}
useHistory = preferences.getBoolean(USE_HISTORY, useHistory)
// Map old setting to new one if it wasn't on default value.
@@ -709,6 +719,9 @@ class Preferences {
else -> android.media.AudioManager.STREAM_MUSIC
}
fun usesCustomBeepTone() = beepToneName == CUSTOM_BEEP_TONE_NAME &&
beepToneUri.isNotEmpty()
fun ignoreDuplicates() = when (ignoreDuplicatesName) {
"ignore_consecutive_duplicates" -> IgnoreDuplicates.Consecutive
"ignore_any_duplicates" -> IgnoreDuplicates.Any
@@ -789,6 +802,7 @@ class Preferences {
putBoolean(BEEP, defaults.beep)
putString(BEEP_TONE_NAME, defaults.beepToneName)
putString(BEEP_STREAM_NAME, defaults.beepStreamName)
putString(BEEP_TONE_URI, defaults.beepToneUri)
putBoolean(USE_HISTORY, defaults.useHistory)
putString(IGNORE_DUPLICATES_NAME, defaults.ignoreDuplicatesName)
putString(IGNORE_CODES, ignoreCodesToJsonArray(defaults.ignoreCodes))
@@ -895,6 +909,7 @@ class Preferences {
private const val BEEP = "beep"
private const val BEEP_TONE_NAME = "beep_tone_name"
private const val BEEP_STREAM_NAME = "beep_stream_name"
private const val BEEP_TONE_URI = "beep_tone_uri"
private const val USE_HISTORY = "use_history"
private const val IGNORE_DUPLICATES_NAME = "ignore_duplicates_name"
private const val IGNORE_CODES = "ignore_codes"
@@ -925,6 +940,7 @@ class Preferences {
private const val AUTOMATED_ACTIONS = "automated_actions"
private const val DYNAMIC_COLORS = "dynamic_colors"
private const val DEFAULT_IGNORE_CODE_PATTERN = "(?i)^fido:/.*"
const val CUSTOM_BEEP_TONE_NAME = "tone_custom_file"
}
}
+2
View File
@@ -5,6 +5,7 @@
<item>@string/tone_prop_ack</item>
<item>@string/tone_prop_beep</item>
<item>@string/tone_prop_beep2</item>
<item>@string/pick_file</item>
</string-array>
<string-array name="beep_tones_values">
<item>tone_cdma_confirm</item>
@@ -12,5 +13,6 @@
<item>tone_prop_ack</item>
<item>tone_prop_beep</item>
<item>tone_prop_beep2</item>
<item>tone_custom_file</item>
</string-array>
</resources>