the great warrior
07/09/2022, 7:25 PMclass TimerServiceManager @Inject constructor(
@ApplicationContext private val applicationContext: Context,
) {
fun startTimerService() {
val serviceIntent = Intent(applicationContext, TimerService::class.java)
ContextCompat.startForegroundService(applicationContext, serviceIntent)
}
fun stopTimerService() {
val serviceIntent = Intent(applicationContext, TimerService::class.java)
applicationContext.stopService(serviceIntent)
}
}
When TimerService stops. it invokes removeTimerNotificationService()
@AndroidEntryPoint
class TimerService : Service() {
private val serviceScope = CoroutineScope(SupervisorJob())
private var stopService = false
private var isServiceRunning = false
@Inject
lateinit var timerManager: TimerManager
@Inject
lateinit var notificationHelper: NotificationHelper
override fun onCreate() {
super.onCreate()
stopService = false
isServiceRunning = true
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
startForeground(TIMER_SERVICE_NOTIFICATION_ID, notificationHelper.getBaseNotification().build())
serviceScope.launch {
timerManager.timerState.collectLatest {
if (!it.isDone && it.progress != 1.0F) {
notificationHelper.updateTimerServiceNotification(
timerRunning = it.isPlaying,
time = it.time,
)
}
if (stopService) {
stopForeground(true)
if (isServiceRunning) {
stopSelf()
}
}
}
}
return START_STICKY
}
override fun onBind(p0: Intent?): IBinder? = null
override fun onDestroy() {
super.onDestroy()
serviceScope.cancel()
timerManager.onChangeDone()
notificationHelper.removeTimerNotificationService()
stopService = true
isServiceRunning = false
}
}
NotificationHelper manages timer service notification and completed notification
@Singleton
class NotificationHelper @Inject constructor(
@ApplicationContext private val applicationContext: Context
) {
private val notificationManager = NotificationManagerCompat.from(applicationContext)
private val pendingIntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
private val openTimerIntent = Intent(
Intent.ACTION_VIEW,
"<https://www.clock.com/Timer|https://www.clock.com/Timer>".toUri(),
applicationContext,
MainActivity::class.java
)
private val openTimerPendingIntent = PendingIntent.getActivity(
applicationContext, 0, openTimerIntent, pendingIntentFlags
)
init {
createNotificationChannels()
}
fun getBaseNotification() = NotificationCompat.Builder(applicationContext, TIMER_SERVICE_CHANNEL_ID)
.setContentTitle("Timer")
.setSmallIcon(R.drawable.ic_hourglass_empty)
.setContentIntent(openTimerPendingIntent)
.setAutoCancel(true)
.setColor(ContextCompat.getColor(applicationContext, R.color.blue))
.setColorized(true)
.setSilent(true)
.setOnlyAlertOnce(true)
fun updateTimerServiceNotification(
time: String,
timerRunning: Boolean
) {
val actionIntent = getTimerNotificationActionIntent(time, timerRunning)
val remove = remove()
val startStopIcon = if (timerRunning) R.drawable.ic_stop else R.drawable.ic_play
val startStopLabel = if (timerRunning) "Pause" else "Resume"
val notificationUpdate = getBaseNotification()
.setContentText(time)
.addAction(R.drawable.ic_close, "Cancel", remove)
.addAction(
startStopIcon,
startStopLabel,
actionIntent
)
.build()
notificationManager.notify(TIMER_SERVICE_NOTIFICATION_ID, notificationUpdate)
}
private fun remove() : PendingIntent {
val broadcastIntent =
Intent(applicationContext, TimerNotificationBroadcastReceiver::class.java).apply {
action = ACTION_DELETE
}
return PendingIntent.getBroadcast(
applicationContext,
0,
broadcastIntent,
pendingIntentFlags
)
}
private fun getTimerNotificationActionIntent(
time: String,
timerRunning: Boolean,
): PendingIntent {
val broadcastIntent =
Intent(applicationContext, TimerNotificationBroadcastReceiver::class.java).apply {
putExtra(EXTRA_TIME, time)
putExtra(EXTRA_TIMER_RUNNING, timerRunning)
}
return PendingIntent.getBroadcast(
applicationContext,
0,
broadcastIntent,
pendingIntentFlags
)
}
fun showTimerCompletedNotification(time: String) {
val timerCompletedNotification = NotificationCompat.Builder(applicationContext, TIMER_COMPLETED_CHANNEL_ID)
.setContentTitle("Time's up")
.setContentText(time)
.setContentIntent(openTimerPendingIntent)
.setSmallIcon(R.drawable.ic_hourglass_empty)
.build()
notificationManager.notify(TIMER_COMPLETED_NOTIFICATION_ID, timerCompletedNotification)
}
fun removeTimerNotificationService() {
notificationManager.cancel(TIMER_SERVICE_NOTIFICATION_ID)
}
fun removeTimerCompletedNotification() {
notificationManager.cancel(TIMER_COMPLETED_NOTIFICATION_ID)
}
private fun createNotificationChannels() {
val timerServiceChannel = NotificationChannelCompat.Builder(
TIMER_SERVICE_CHANNEL_ID,
NotificationManagerCompat.IMPORTANCE_DEFAULT
)
.setName(applicationContext.getString(R.string.timer_service_channel_name))
.setDescription(applicationContext.getString(R.string.timer_service_channel_description))
.setSound(null, null)
.build()
val timerCompletedChannel = NotificationChannelCompat.Builder(
TIMER_COMPLETED_CHANNEL_ID,
NotificationManagerCompat.IMPORTANCE_HIGH
)
.setName(applicationContext.getString(R.string.timer_completed_channel_name))
.setDescription(applicationContext.getString(R.string.timer_completed_channel_description))
.build()
notificationManager.createNotificationChannelsCompat(
listOf(
timerServiceChannel,
timerCompletedChannel
)
)
}
}
private const val TIMER_SERVICE_CHANNEL_ID = "timer_service_channel"
private const val TIMER_COMPLETED_CHANNEL_ID = "timer_completed_notification_channel"
const val TIMER_SERVICE_NOTIFICATION_ID = -1
private const val TIMER_COMPLETED_NOTIFICATION_ID = -2
TimerNotificationBroadcastReceiver is called when the user presses the notification actions buttons pause, resume and cancel
@AndroidEntryPoint
class TimerNotificationBroadcastReceiver : BroadcastReceiver() {
@Inject
lateinit var timerManager: TimerManager
@Inject
lateinit var notificationHelper: NotificationHelper
override fun onReceive(p0: Context?, intent: Intent?) {
val timerRunning = intent?.getBooleanExtra(EXTRA_TIMER_RUNNING, false)
val time = intent?.getStringExtra(EXTRA_TIME)
val action = intent?.action
timerManager.handleCountDownTimer()
if (timerRunning == true) {
if (time != null) {
notificationHelper.updateTimerServiceNotification(
timerRunning = false,
time = time
)
Log.d(TAG, "onReceive() called with: p0 = $p0, intent = $intent")
}
}
if (action.equals(ACTION_DELETE)) {
timerManager.onChangeDone()
timerManager.stopService()
}
}
}
const val EXTRA_TIME = "EXTRA_TIME"
const val EXTRA_TIMER_RUNNING = "EXTRA_TIMER_RUNNING"
const val ACTION_DELETE = "ACTION_DELETE"