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() /** * 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 = activeAlarms.values.toList() /** * Cleanup resources. */ fun destroy() { dismissAll() } }