11package com.utsapoddar.sift
22
3+ import android.app.AlarmManager
34import android.app.Notification
45import android.app.NotificationChannel
56import android.app.NotificationManager
@@ -8,9 +9,13 @@ import android.app.Service
89import android.content.Context
910import android.content.Intent
1011import android.content.pm.ServiceInfo
12+ import android.media.AudioAttributes
13+ import android.media.MediaPlayer
14+ import android.net.Uri
1115import android.os.Build
1216import android.os.IBinder
1317import androidx.core.app.NotificationCompat
18+ import java.io.File
1419
1520class TimerService : Service () {
1621
@@ -19,8 +24,17 @@ class TimerService : Service() {
1924 const val NOTIF_ID = 42
2025 const val ACTION_STOP = " com.sift.timer.stop"
2126 const val ACTION_SILENCE = " com.sift.timer.silence"
27+ const val ACTION_ALARM_FIRED = " com.sift.timer.alarm_fired"
28+ const val ACTION_SCHEDULE_ALARMS = " com.sift.timer.schedule_alarms"
29+ const val ACTION_CANCEL_ALARMS = " com.sift.timer.cancel_alarms"
30+ const val EXTRA_PHASE_NAMES = " phase_names"
31+ const val EXTRA_PHASE_END_TIMES = " phase_end_times"
32+ const val EXTRA_PHASE_NAME = " phase_name"
2233 }
2334
35+ private var mediaPlayer: MediaPlayer ? = null
36+ private var currentPhaseName: String = " Work"
37+
2438 override fun onBind (intent : Intent ? ): IBinder ? = null
2539
2640 override fun onCreate () {
@@ -34,11 +48,35 @@ class TimerService : Service() {
3448 }
3549
3650 override fun onStartCommand (intent : Intent ? , flags : Int , startId : Int ): Int {
51+ when (intent?.action) {
52+ ACTION_ALARM_FIRED -> {
53+ currentPhaseName = intent.getStringExtra(EXTRA_PHASE_NAME ) ? : currentPhaseName
54+ updateNotification()
55+ playAlarm()
56+ // Notify Flutter if it's alive
57+ sendBroadcast(Intent (ACTION_STOP ).setPackage(packageName).apply {
58+ action = " com.sift.timer.alarm_notify"
59+ })
60+ }
61+ ACTION_SCHEDULE_ALARMS -> {
62+ val names = intent.getStringArrayExtra(EXTRA_PHASE_NAMES ) ? : return START_STICKY
63+ val times = intent.getLongArrayExtra(EXTRA_PHASE_END_TIMES ) ? : return START_STICKY
64+ currentPhaseName = if (names.isNotEmpty()) names[0 ] else " Work"
65+ updateNotification()
66+ scheduleAlarms(names, times)
67+ }
68+ ACTION_CANCEL_ALARMS -> {
69+ cancelAlarms()
70+ stopAlarmSound()
71+ stopSelf()
72+ }
73+ }
3774 return START_STICKY
3875 }
3976
4077 override fun onDestroy () {
4178 super .onDestroy()
79+ stopAlarmSound()
4280 if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .N ) {
4381 stopForeground(STOP_FOREGROUND_REMOVE )
4482 } else {
@@ -47,6 +85,79 @@ class TimerService : Service() {
4785 }
4886 }
4987
88+ private fun scheduleAlarms (names : Array <String >, times : LongArray ) {
89+ val alarmManager = getSystemService(Context .ALARM_SERVICE ) as AlarmManager
90+ times.forEachIndexed { i, timeMs ->
91+ val alarmIntent = Intent (this , AlarmReceiver ::class .java).apply {
92+ putExtra(EXTRA_PHASE_NAME , if (i + 1 < names.size) names[i + 1 ] else " Done" )
93+ }
94+ val pi = PendingIntent .getBroadcast(
95+ this , i, alarmIntent,
96+ PendingIntent .FLAG_IMMUTABLE or PendingIntent .FLAG_UPDATE_CURRENT
97+ )
98+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .M ) {
99+ alarmManager.setExactAndAllowWhileIdle(AlarmManager .RTC_WAKEUP , timeMs, pi)
100+ } else {
101+ alarmManager.setExact(AlarmManager .RTC_WAKEUP , timeMs, pi)
102+ }
103+ }
104+ }
105+
106+ private fun cancelAlarms () {
107+ val alarmManager = getSystemService(Context .ALARM_SERVICE ) as AlarmManager
108+ for (i in 0 .. 6 ) {
109+ val pi = PendingIntent .getBroadcast(
110+ this , i, Intent (this , AlarmReceiver ::class .java),
111+ PendingIntent .FLAG_IMMUTABLE or PendingIntent .FLAG_NO_CREATE
112+ )
113+ pi?.let { alarmManager.cancel(it) }
114+ }
115+ }
116+
117+ private fun playAlarm () {
118+ stopAlarmSound()
119+ val prefs = getSharedPreferences(" FlutterSharedPreferences" , Context .MODE_PRIVATE )
120+ val ringtonePath = prefs.getString(" flutter.ringtone_path" , null )
121+
122+ try {
123+ mediaPlayer = if (ringtonePath != null && File (ringtonePath).exists()) {
124+ MediaPlayer ().apply {
125+ setAudioAttributes(AudioAttributes .Builder ()
126+ .setUsage(AudioAttributes .USAGE_ALARM )
127+ .setContentType(AudioAttributes .CONTENT_TYPE_SONIFICATION )
128+ .build())
129+ setDataSource(ringtonePath)
130+ prepare()
131+ }
132+ } else {
133+ val afd = assets.openFd(" flutter_assets/assets/alarm.mp3" )
134+ MediaPlayer ().apply {
135+ setAudioAttributes(AudioAttributes .Builder ()
136+ .setUsage(AudioAttributes .USAGE_ALARM )
137+ .setContentType(AudioAttributes .CONTENT_TYPE_SONIFICATION )
138+ .build())
139+ setDataSource(afd.fileDescriptor, afd.startOffset, afd.length)
140+ prepare()
141+ }
142+ }
143+ mediaPlayer?.apply {
144+ isLooping = false
145+ setOnCompletionListener { mp -> mp.release(); mediaPlayer = null }
146+ start()
147+ }
148+ } catch (_: Exception ) {}
149+ }
150+
151+ private fun stopAlarmSound () {
152+ mediaPlayer?.apply { if (isPlaying) stop(); release() }
153+ mediaPlayer = null
154+ }
155+
156+ private fun updateNotification () {
157+ val nm = getSystemService(Context .NOTIFICATION_SERVICE ) as NotificationManager
158+ nm.notify(NOTIF_ID , buildNotification())
159+ }
160+
50161 private fun buildNotification (): Notification {
51162 val stopIntent = Intent (ACTION_STOP ).setPackage(packageName)
52163 val stopPi = PendingIntent .getBroadcast(
@@ -59,7 +170,7 @@ class TimerService : Service() {
59170 PendingIntent .FLAG_IMMUTABLE or PendingIntent .FLAG_UPDATE_CURRENT
60171 )
61172 return NotificationCompat .Builder (this , CHANNEL_ID )
62- .setContentTitle(" Sift" )
173+ .setContentTitle(" Sift — $currentPhaseName " )
63174 .setContentText(" Timer running" )
64175 .setSmallIcon(R .drawable.ic_notification)
65176 .setOngoing(true )
@@ -70,12 +181,8 @@ class TimerService : Service() {
70181
71182 private fun createChannel () {
72183 val channel = NotificationChannel (
73- CHANNEL_ID ,
74- " Timer Service" ,
75- NotificationManager .IMPORTANCE_LOW
76- ).apply {
77- description = " Keeps timer running in background"
78- }
184+ CHANNEL_ID , " Timer Service" , NotificationManager .IMPORTANCE_LOW
185+ ).apply { description = " Keeps timer running in background" }
79186 val nm = getSystemService(Context .NOTIFICATION_SERVICE ) as NotificationManager
80187 nm.createNotificationChannel(channel)
81188 }
0 commit comments