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:
2026-02-09 11:12:51 -08:00
commit 6d4ae2e5c3
92 changed files with 15173 additions and 0 deletions

View 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()
}
}