Initial commit: Alfred Mobile - AI Assistant Android App
- 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
This commit is contained in:
255
app/src/main/java/com/openclaw/alfred/alarm/AlarmManager.kt
Normal file
255
app/src/main/java/com/openclaw/alfred/alarm/AlarmManager.kt
Normal file
@@ -0,0 +1,255 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user