- OAuth authentication via Authentik - WebSocket connection to OpenClaw gateway - Configurable gateway URL with first-run setup - User preferences sync across devices - Multi-user support with custom assistant names - ElevenLabs TTS integration (local + remote) - FCM push notifications for alarms - Voice input via Google Speech API - No hardcoded secrets or internal IPs in tracked files
256 lines
7.9 KiB
Kotlin
256 lines
7.9 KiB
Kotlin
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()
|
|
}
|
|
}
|