Per wekker eigen geluid instelbaar via systeem-ringtone-kiezer; standaard blijft de systeem-alarmtoon

This commit is contained in:
eddy 2026-10-14 20:31:54 +02:00
commit b537724b75
5 changed files with 139 additions and 11 deletions

View file

@ -11,6 +11,7 @@ import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import nl.eddydevink.buschauffeurswekker.core.AlarmScheduler import nl.eddydevink.buschauffeurswekker.core.AlarmScheduler
import nl.eddydevink.buschauffeurswekker.core.Notifier import nl.eddydevink.buschauffeurswekker.core.Notifier
import nl.eddydevink.buschauffeurswekker.core.Prefs
import nl.eddydevink.buschauffeurswekker.core.Ringer import nl.eddydevink.buschauffeurswekker.core.Ringer
import nl.eddydevink.buschauffeurswekker.receivers.AlarmRingReceiver import nl.eddydevink.buschauffeurswekker.receivers.AlarmRingReceiver
import java.time.Instant import java.time.Instant
@ -22,6 +23,7 @@ import java.time.format.DateTimeFormatter
class AlarmActivity : AppCompatActivity() { class AlarmActivity : AppCompatActivity() {
private var ringer: Ringer? = null private var ringer: Ringer? = null
private var alarmName: String = ""
private val clockTick = object : Runnable { private val clockTick = object : Runnable {
override fun run() { override fun run() {
@ -61,6 +63,7 @@ class AlarmActivity : AppCompatActivity() {
private fun bindUi() { private fun bindUi() {
val name = intent?.getStringExtra(AlarmScheduler.EXTRA_NAME) ?: "Wekker" val name = intent?.getStringExtra(AlarmScheduler.EXTRA_NAME) ?: "Wekker"
alarmName = name
val ringTime = intent?.getLongExtra(AlarmScheduler.EXTRA_RING_TIME, System.currentTimeMillis()) val ringTime = intent?.getLongExtra(AlarmScheduler.EXTRA_RING_TIME, System.currentTimeMillis())
?: System.currentTimeMillis() ?: System.currentTimeMillis()
val snoozed = intent?.getBooleanExtra(AlarmScheduler.EXTRA_SNOOZED, false) ?: false val snoozed = intent?.getBooleanExtra(AlarmScheduler.EXTRA_SNOOZED, false) ?: false
@ -79,7 +82,8 @@ class AlarmActivity : AppCompatActivity() {
} }
private fun startRinging() { private fun startRinging() {
ringer = Ringer(this).also { it.start() } val uri = Prefs(this).ringtoneFor(alarmName)
ringer = Ringer(this, uri).also { it.start() }
handler.removeCallbacks(clockTick) handler.removeCallbacks(clockTick)
handler.post(clockTick) handler.post(clockTick)
} }

View file

@ -1,20 +1,41 @@
package nl.eddydevink.buschauffeurswekker package nl.eddydevink.buschauffeurswekker
import android.content.Intent
import android.media.RingtoneManager
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.widget.EditText import android.widget.EditText
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import nl.eddydevink.buschauffeurswekker.core.Prefs import nl.eddydevink.buschauffeurswekker.core.Prefs
/** Apart instellingenscherm: CalDAV-gegevens (URL, gebruiker, app-wachtwoord). */ /**
* Instellingen: CalDAV-gegevens en per wekker een eigen geluid
* (via de systeem-ringtone-kiezer; "Standaard geluid" = systeem-alarmtoon).
*/
class SettingsActivity : AppCompatActivity() { class SettingsActivity : AppCompatActivity() {
private lateinit var prefs: Prefs
private var pendingSoundName: String? = null
private val ringtonePicker =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val name = pendingSoundName ?: return@registerForActivityResult
pendingSoundName = null
if (result.resultCode == RESULT_OK) {
val uri = result.data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI) as Uri?
prefs.setRingtone(name, uri)
updateSoundRow(name)
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings) setContentView(R.layout.activity_settings)
val prefs = Prefs(this) prefs = Prefs(this)
findViewById<EditText>(R.id.et_url).setText(prefs.calendarUrl.orEmpty()) findViewById<EditText>(R.id.et_url).setText(prefs.calendarUrl.orEmpty())
findViewById<EditText>(R.id.et_user).setText(prefs.username.orEmpty()) findViewById<EditText>(R.id.et_user).setText(prefs.username.orEmpty())
findViewById<EditText>(R.id.et_pass).setText(prefs.password.orEmpty()) findViewById<EditText>(R.id.et_pass).setText(prefs.password.orEmpty())
@ -26,5 +47,52 @@ class SettingsActivity : AppCompatActivity() {
Toast.makeText(this, "Instellingen opgeslagen", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Instellingen opgeslagen", Toast.LENGTH_SHORT).show()
finish() finish()
} }
findViewById<MaterialButton>(R.id.btn_sound_opstaan).setOnClickListener { openSoundPicker("Opstaan") }
findViewById<MaterialButton>(R.id.btn_sound_wally).setOnClickListener { openSoundPicker("Wally") }
findViewById<MaterialButton>(R.id.btn_sound_vertrek).setOnClickListener { openSoundPicker("Vertrek") }
}
override fun onResume() {
super.onResume()
SOUND_ALARMS.forEach { updateSoundRow(it) }
}
private fun openSoundPicker(name: String) {
pendingSoundName = name
val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply {
putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALARM)
putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, "Geluid voor $name")
putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true)
putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false)
putExtra(
RingtoneManager.EXTRA_RINGTONE_EXISTING_URI,
prefs.ringtoneFor(name)
?: RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM),
)
}
ringtonePicker.launch(intent)
}
private fun updateSoundRow(name: String) {
val uri = prefs.ringtoneFor(name)
val label = if (uri != null) {
RingtoneManager.getRingtone(this, uri)?.getTitle(this) ?: "Eigen geluid"
} else {
"Standaard"
}
findViewById<MaterialButton>(SOUND_BUTTON_ID[name]!!).text = "$name — $label"
}
private companion object {
const val ALARM_OPSTAAN = "Opstaan"
const val ALARM_WALLY = "Wally"
const val ALARM_VERTREK = "Vertrek"
val SOUND_ALARMS = listOf(ALARM_OPSTAAN, ALARM_WALLY, ALARM_VERTREK)
val SOUND_BUTTON_ID = mapOf(
ALARM_OPSTAAN to R.id.btn_sound_opstaan,
ALARM_WALLY to R.id.btn_sound_wally,
ALARM_VERTREK to R.id.btn_sound_vertrek,
)
} }
} }

View file

@ -2,6 +2,7 @@ package nl.eddydevink.buschauffeurswekker.core
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.Uri
import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey import androidx.security.crypto.MasterKey
import java.time.Instant import java.time.Instant
@ -83,6 +84,16 @@ class Prefs(context: Context) {
} else null } else null
} }
/** Gekozen geluid voor een wekker (naam), of null voor het systeemgeluid. */
fun ringtoneFor(name: String): Uri? =
sp.getString("ringtone_$name", null)?.let {
runCatching { Uri.parse(it) }.getOrNull()
}
fun setRingtone(name: String, uri: Uri?) {
sp.edit().putString("ringtone_$name", uri?.toString()).apply()
}
fun storePlan(day: LocalDate, alarms: List<AlarmSpec>) { fun storePlan(day: LocalDate, alarms: List<AlarmSpec>) {
val zone = ZoneId.systemDefault() val zone = ZoneId.systemDefault()
plannedDayIso = day.toString() plannedDayIso = day.toString()

View file

@ -10,22 +10,35 @@ import android.os.VibrationEffect
import android.os.Vibrator import android.os.Vibrator
import android.os.VibratorManager import android.os.VibratorManager
/** Speelt het standaard wekkergeluid (herhalend) en laat de telefoon trillen. */ /**
class Ringer(private val context: Context) { * Speelt het wekkergeluid (herhalend) en laat de telefoon trillen.
* Een eigen gekozen geluid ([chosenUri], null = systeem-alarmtoon) wordt
* afgespeeld; valt die weg (verwijderd bestand), dan valt het terug op de
* standaard alarmtoon en daarna de beltoon.
*/
class Ringer(
private val context: Context,
private val chosenUri: Uri? = null,
) {
private var player: MediaPlayer? = null private var player: MediaPlayer? = null
private var vibrator: Vibrator? = null private var vibrator: Vibrator? = null
fun start() { fun start() {
val uri = RingtoneManager.getActualDefaultRingtoneUri(context, RingtoneManager.TYPE_ALARM) val candidates = listOfNotNull(
?: RingtoneManager.getActualDefaultRingtoneUri(context, RingtoneManager.TYPE_RINGTONE) chosenUri,
playLooping(uri) RingtoneManager.getActualDefaultRingtoneUri(context, RingtoneManager.TYPE_ALARM),
RingtoneManager.getActualDefaultRingtoneUri(context, RingtoneManager.TYPE_RINGTONE),
)
for (uri in candidates) {
if (playLooping(uri)) break
}
vibrate() vibrate()
} }
private fun playLooping(uri: Uri?) { /** @return true als het geluid is gestart. */
if (uri == null) return private fun playLooping(uri: Uri): Boolean {
try { return try {
player = MediaPlayer().apply { player = MediaPlayer().apply {
setAudioAttributes( setAudioAttributes(
AudioAttributes.Builder() AudioAttributes.Builder()
@ -38,8 +51,10 @@ class Ringer(private val context: Context) {
prepare() prepare()
start() start()
} }
true
} catch (t: Throwable) { } catch (t: Throwable) {
player = null player = null
false
} }
} }

View file

@ -46,5 +46,35 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:text="Opslaan" /> android:text="Opslaan" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Wekkergeluiden"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textStyle="bold" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_sound_opstaan"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Opstaan — Standaard" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_sound_wally"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Wally — Standaard" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_sound_vertrek"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Vertrek — Standaard" />
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>