feat(alarm): add musical alarm foundation
@@ -5,6 +5,9 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
|
||||
@@ -52,6 +55,16 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".PluriWaveAlarmReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="es.freetimelab.pluriwave.alarm.FIRE"/>
|
||||
<action android:name="es.freetimelab.pluriwave.alarm.PRE_NOTICE"/>
|
||||
<action android:name="es.freetimelab.pluriwave.alarm.SKIP_NEXT"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package es.freetimelab.pluriwave
|
||||
|
||||
import android.app.AlarmManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
|
||||
class AlarmScheduler(private val context: Context) {
|
||||
private val alarmManager =
|
||||
context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
|
||||
fun scheduleAlarm(id: String, title: String, triggerAtMillis: Long, preNoticeAtMillis: Long) {
|
||||
val alarmIntent = PendingIntent.getBroadcast(
|
||||
context,
|
||||
requestCode(id, 1),
|
||||
Intent(context, PluriWaveAlarmReceiver::class.java).apply {
|
||||
action = PluriWaveAlarmReceiver.ACTION_FIRE
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_ID, id)
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_TITLE, title)
|
||||
},
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val showIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
requestCode(id, 2),
|
||||
Intent(context, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_ID, id)
|
||||
},
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
alarmManager.setAlarmClock(
|
||||
AlarmManager.AlarmClockInfo(triggerAtMillis, showIntent),
|
||||
alarmIntent
|
||||
)
|
||||
|
||||
if (preNoticeAtMillis > System.currentTimeMillis()) {
|
||||
alarmManager.setExactAndAllowWhileIdle(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
preNoticeAtMillis,
|
||||
PendingIntent.getBroadcast(
|
||||
context,
|
||||
requestCode(id, 3),
|
||||
Intent(context, PluriWaveAlarmReceiver::class.java).apply {
|
||||
action = PluriWaveAlarmReceiver.ACTION_PRE_NOTICE
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_ID, id)
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_TITLE, title)
|
||||
},
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelAlarm(id: String) {
|
||||
for (slot in 1..3) {
|
||||
alarmManager.cancel(
|
||||
PendingIntent.getBroadcast(
|
||||
context,
|
||||
requestCode(id, slot),
|
||||
Intent(context, PluriWaveAlarmReceiver::class.java),
|
||||
PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE
|
||||
) ?: continue
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun canScheduleExactAlarms(): Boolean {
|
||||
return Build.VERSION.SDK_INT < Build.VERSION_CODES.S ||
|
||||
alarmManager.canScheduleExactAlarms()
|
||||
}
|
||||
|
||||
private fun requestCode(id: String, slot: Int): Int = 31 * id.hashCode() + slot
|
||||
}
|
||||
@@ -9,9 +9,11 @@ import android.os.Looper
|
||||
import com.ryanheise.audioservice.AudioServiceActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class MainActivity : AudioServiceActivity() {
|
||||
private val visualizerChannel = "pluriwave/audio_visualizer"
|
||||
private val alarmChannel = "pluriwave/alarm_scheduler"
|
||||
private val permissionRequestCode = 4821
|
||||
private var visualizer: Visualizer? = null
|
||||
private var pendingSink: EventChannel.EventSink? = null
|
||||
@@ -36,6 +38,47 @@ class MainActivity : AudioServiceActivity() {
|
||||
pendingArgs = null
|
||||
}
|
||||
})
|
||||
|
||||
val alarmScheduler = AlarmScheduler(this)
|
||||
MethodChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger,
|
||||
alarmChannel
|
||||
).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"scheduleAlarm" -> {
|
||||
val id = call.argument<String>("id")
|
||||
val title = call.argument<String>("title") ?: "PluriWave"
|
||||
val triggerAtMillis = call.argument<Long>("triggerAtMillis")
|
||||
val preNoticeAtMillis = call.argument<Long>("preNoticeAtMillis") ?: 0L
|
||||
if (id == null || triggerAtMillis == null) {
|
||||
result.error("INVALID_ALARM", "Missing alarm id or trigger time", null)
|
||||
} else {
|
||||
alarmScheduler.scheduleAlarm(id, title, triggerAtMillis, preNoticeAtMillis)
|
||||
result.success(null)
|
||||
}
|
||||
}
|
||||
"cancelAlarm" -> {
|
||||
val id = call.argument<String>("id")
|
||||
if (id == null) {
|
||||
result.error("INVALID_ALARM", "Missing alarm id", null)
|
||||
} else {
|
||||
alarmScheduler.cancelAlarm(id)
|
||||
result.success(null)
|
||||
}
|
||||
}
|
||||
"diagnostics" -> {
|
||||
result.success(
|
||||
mapOf(
|
||||
"canScheduleExactAlarms" to alarmScheduler.canScheduleExactAlarms(),
|
||||
"notificationsEnabled" to true,
|
||||
"manufacturer" to Build.MANUFACTURER,
|
||||
"sdkInt" to Build.VERSION.SDK_INT
|
||||
)
|
||||
)
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startVisualizerWhenAllowed() {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package es.freetimelab.pluriwave
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
||||
class PluriWaveAlarmReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val alarmId = intent.getStringExtra(EXTRA_ALARM_ID) ?: return
|
||||
val title = intent.getStringExtra(EXTRA_ALARM_TITLE) ?: "PluriWave"
|
||||
|
||||
when (intent.action) {
|
||||
ACTION_FIRE -> {
|
||||
val launch = Intent(context, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
putExtra(EXTRA_ALARM_ID, alarmId)
|
||||
putExtra(EXTRA_ALARM_TITLE, title)
|
||||
putExtra(EXTRA_ALARM_ACTION, ACTION_FIRE)
|
||||
}
|
||||
context.startActivity(launch)
|
||||
}
|
||||
ACTION_PRE_NOTICE -> {
|
||||
// MVP: native delivery exists; Flutter will own skip-next UX.
|
||||
// Next batch: notification channel + action button.
|
||||
}
|
||||
ACTION_SKIP_NEXT -> {
|
||||
// Next batch: forward skip-next to Flutter persistence or native store.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ACTION_FIRE = "es.freetimelab.pluriwave.alarm.FIRE"
|
||||
const val ACTION_PRE_NOTICE = "es.freetimelab.pluriwave.alarm.PRE_NOTICE"
|
||||
const val ACTION_SKIP_NEXT = "es.freetimelab.pluriwave.alarm.SKIP_NEXT"
|
||||
const val EXTRA_ALARM_ID = "alarmId"
|
||||
const val EXTRA_ALARM_TITLE = "alarmTitle"
|
||||
const val EXTRA_ALARM_ACTION = "alarmAction"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 656 B After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 656 B After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 452 B After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 452 B After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 838 B After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 838 B After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 84 KiB |