Files
alfred-mobile/app/src/main/java/com/openclaw/alfred/alarm/AlarmManager.kt

256 lines
7.9 KiB
Kotlin
Raw Normal View History

package com.openclaw.alfred.alarm
import android.content.Context
import android.media.AudioAttributes
import android.media.MediaPlayer
import android.media.RingtoneManager
import android.net.Uri
import android.os.VibrationEffect
import android.os.Vibrator
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
/**
* Manages alarm playback with repeating sound and vibration.
* Singleton pattern so BroadcastReceiver can access the same instance.
*/
class AlarmManager private constructor(private val context: Context) {
companion object {
@Volatile
private var instance: AlarmManager? = null
fun getInstance(context: Context): AlarmManager {
return instance ?: synchronized(this) {
instance ?: AlarmManager(context.applicationContext).also { instance = it }
}
}
}
private val TAG = "AlarmManager"
private var mediaPlayer: MediaPlayer? = null
private var vibrator: Vibrator? = null
private var vibrateJob: Job? = null
private val scope = CoroutineScope(Dispatchers.Main)
// Callback for when alarms are dismissed
var onAlarmDismissed: ((String) -> Unit)? = null
data class ActiveAlarm(
val id: String,
val title: String,
val message: String,
val timestamp: Long
)
private val activeAlarms = mutableMapOf<String, ActiveAlarm>()
/**
* Start an alarm with repeating sound and vibration.
*/
fun startAlarm(
alarmId: String,
title: String,
message: String,
enableSound: Boolean = true,
enableVibrate: Boolean = true
) {
Log.d(TAG, "Starting alarm: $alarmId - $title: $message")
// Store active alarm
activeAlarms[alarmId] = ActiveAlarm(alarmId, title, message, System.currentTimeMillis())
// Check user preferences
val prefs = context.getSharedPreferences("alfred_settings", android.content.Context.MODE_PRIVATE)
val soundEnabled = prefs.getBoolean("alarm_sound_enabled", true)
val vibrateEnabled = prefs.getBoolean("alarm_vibrate_enabled", true)
// Start sound if enabled in both function param and settings
if (enableSound && soundEnabled) {
startAlarmSound()
}
// Start vibration if enabled in both function param and settings
if (enableVibrate && vibrateEnabled) {
startAlarmVibration()
}
}
/**
* Start repeating alarm sound.
*/
private fun startAlarmSound() {
try {
// Stop any existing playback
stopAlarmSound()
// Get alarm sound URI from preferences, or use default
val prefs = context.getSharedPreferences("alfred_settings", android.content.Context.MODE_PRIVATE)
val customUriString = prefs.getString("alarm_sound_uri", null)
val alarmUri = if (customUriString != null) {
try {
Uri.parse(customUriString)
} catch (e: Exception) {
Log.w(TAG, "Failed to parse custom alarm URI, using default", e)
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM)
?: RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
}
} else {
// Get default alarm sound URI
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM)
?: RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
}
// Create MediaPlayer
mediaPlayer = MediaPlayer().apply {
setDataSource(context, alarmUri)
// Set audio attributes for alarm
setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ALARM)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build()
)
// Loop the sound
isLooping = true
// Prepare and start
prepare()
start()
}
Log.d(TAG, "Alarm sound started: $alarmUri")
} catch (e: Exception) {
Log.e(TAG, "Failed to start alarm sound", e)
}
}
/**
* Start repeating alarm vibration.
*/
private fun startAlarmVibration() {
try {
vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
if (vibrator?.hasVibrator() == true) {
// Vibrate pattern: [delay, vibrate, sleep, vibrate, sleep]
// 0ms delay, 500ms vibrate, 500ms sleep, repeat
val pattern = longArrayOf(0, 500, 500)
// Create vibration effect with repeating pattern
val effect = VibrationEffect.createWaveform(pattern, 0) // 0 = repeat from index 0
vibrator?.vibrate(effect)
Log.d(TAG, "Alarm vibration started")
}
} catch (e: Exception) {
Log.e(TAG, "Failed to start alarm vibration", e)
}
}
/**
* Stop alarm sound.
*/
private fun stopAlarmSound() {
try {
mediaPlayer?.apply {
if (isPlaying) {
stop()
}
release()
}
mediaPlayer = null
Log.d(TAG, "Alarm sound stopped")
} catch (e: Exception) {
Log.e(TAG, "Failed to stop alarm sound", e)
}
}
/**
* Stop alarm vibration.
*/
private fun stopAlarmVibration() {
try {
vibrateJob?.cancel()
vibrateJob = null
vibrator?.cancel()
vibrator = null
Log.d(TAG, "Alarm vibration stopped")
} catch (e: Exception) {
Log.e(TAG, "Failed to stop alarm vibration", e)
}
}
/**
* Dismiss a specific alarm.
*/
fun dismissAlarm(alarmId: String) {
Log.d(TAG, "Dismissing alarm: $alarmId")
activeAlarms.remove(alarmId)
// Cancel the notification
com.openclaw.alfred.notifications.NotificationHelper.cancelAlarmNotification(context, alarmId)
// Notify callback (for cross-device sync)
onAlarmDismissed?.invoke(alarmId)
// If no more active alarms, stop sound and vibration
if (activeAlarms.isEmpty()) {
stopAll()
}
}
/**
* Dismiss all active alarms.
*/
fun dismissAll() {
Log.d(TAG, "Dismissing all alarms")
// Notify callback for each alarm (for cross-device sync)
activeAlarms.keys.forEach { alarmId ->
onAlarmDismissed?.invoke(alarmId)
}
activeAlarms.clear()
// Cancel ALL alarm notifications (handles any lingering notifications)
com.openclaw.alfred.notifications.NotificationHelper.cancelAllAlarmNotifications(context)
stopAll()
}
/**
* Stop all alarm sounds and vibrations.
*/
private fun stopAll() {
stopAlarmSound()
stopAlarmVibration()
}
/**
* Check if there are any active alarms.
*/
fun hasActiveAlarms(): Boolean = activeAlarms.isNotEmpty()
/**
* Get all active alarms.
*/
fun getActiveAlarms(): List<ActiveAlarm> = activeAlarms.values.toList()
/**
* Cleanup resources.
*/
fun destroy() {
dismissAll()
}
}