Skip to content

Commit

Permalink
fix: improved stopwatch service and notification
Browse files Browse the repository at this point in the history
  • Loading branch information
SuhasDissa committed Mar 12, 2024
1 parent 23690b6 commit 4b7b655
Show file tree
Hide file tree
Showing 5 changed files with 280 additions and 94 deletions.
217 changes: 191 additions & 26 deletions app/src/main/java/com/bnyro/clock/services/StopwatchService.kt
Original file line number Diff line number Diff line change
@@ -1,45 +1,210 @@
package com.bnyro.clock.services

import androidx.compose.runtime.mutableStateOf
import android.Manifest
import android.annotation.SuppressLint
import android.app.Notification
import android.app.PendingIntent
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.os.Binder
import android.os.Build
import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.bnyro.clock.R
import com.bnyro.clock.obj.ScheduledObject
import com.bnyro.clock.obj.WatchState
import com.bnyro.clock.ui.MainActivity
import com.bnyro.clock.util.NotificationHelper
import java.util.Timer
import java.util.TimerTask

class StopwatchService : ScheduleService() {
override val notificationId = 1
class StopwatchService : Service() {
private val notificationId = 1
var currentPosition = 0
private set
var state = WatchState.IDLE
private set

private val timer = Timer()

private lateinit var contentIntent: PendingIntent
private lateinit var notificationManager: NotificationManagerCompat
private var notificationPermission = true

var onPositionChange: (Int) -> Unit = {}
var onStateChange: (WatchState) -> Unit = {}

private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val extra = intent.getStringExtra(ACTION_EXTRA_KEY)
Log.d("Stopwatch Actions", extra.toString())
when (extra) {
ACTION_START -> start()
ACTION_STOP -> stop()
ACTION_PAUSE_RESUME -> {
if (state == WatchState.PAUSED) resume() else pause()
}
}
}
}

override fun onCreate() {
super.onCreate()
scheduledObjects.add(
ScheduledObject(
state = mutableStateOf(WatchState.RUNNING),
id = notificationId
)
notificationManager = NotificationManagerCompat.from(this)

contentIntent = PendingIntent.getActivity(
this,
8,
Intent(this, MainActivity::class.java).setAction(MainActivity.SHOW_STOPWATCH_ACTION),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
notificationPermission = ActivityCompat.checkSelfPermission(
this,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
registerReceiver(receiver, IntentFilter(STOPWATCH_INTENT_ACTION), RECEIVER_NOT_EXPORTED)
} else {
registerReceiver(receiver, IntentFilter(STOPWATCH_INTENT_ACTION))
}
}

override fun updateState() {
scheduledObjects.forEach {
if (it.state.value == WatchState.RUNNING) {
it.currentPosition.value += updateDelay
invokeChangeListener()
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(receiver)
counterTask?.cancel()
counterTask = null
timer.cancel()
}

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
startForeground(notificationId, getNotification())
return START_STICKY
}

private var counterTask: TimerTask? = null

private fun start() {
currentPosition = 0
updateState(WatchState.RUNNING)
counterTask = object : TimerTask() {
override fun run() {
if (state != WatchState.PAUSED) {
currentPosition += UPDATE_DELAY
onPositionChange(currentPosition)
}
}
}
timer.scheduleAtFixedRate(counterTask, 0, UPDATE_DELAY.toLong())
}

private fun stop() {
updateState(WatchState.IDLE)
notificationManager.cancel(notificationId)
currentPosition = 0
onPositionChange(currentPosition)
counterTask?.cancel()
counterTask = null
stopSelf()
}

@SuppressLint("MissingPermission")
private fun updateNotification() {
if (notificationPermission) {
notificationManager.notify(notificationId, getNotification())
}
}

@SuppressLint("DefaultLocale")
private fun getNotification(): Notification {
return NotificationCompat.Builder(
this,
NotificationHelper.STOPWATCH_CHANNEL
)
.setContentTitle(getText(R.string.stopwatch))
.setSmallIcon(R.drawable.ic_notification)
.setContentIntent(contentIntent)
.apply {
if (state != WatchState.IDLE) {
addAction(stopAction())
addAction(pauseResumeAction())
}
}
.setUsesChronometer(state == WatchState.RUNNING)
.setWhen(System.currentTimeMillis() - currentPosition)
.build()
}

private fun pause() {
updateState(WatchState.PAUSED)
}

private fun resume() {
updateState(WatchState.RUNNING)
}

private fun updateState(newState: WatchState) {
state = newState
updateNotification()
onStateChange(newState)
}

private fun getAction(title: String, requestCode: Int, action: String) =
NotificationCompat.Action.Builder(
null,
title,
getPendingIntent(
Intent(STOPWATCH_INTENT_ACTION).putExtra(
ACTION_EXTRA_KEY,
action
), requestCode
)
).build()

private fun stopAction() =
getAction(getString(R.string.stop), STOP_ACTION_REQUEST_CODE, ACTION_STOP)

private fun pauseResumeAction(): NotificationCompat.Action {
val text =
if (state == WatchState.RUNNING) getString(R.string.pause) else getString(R.string.resume)
return getAction(
text,
PAUSE_RESUME_ACTION_REQUEST_CODE,
ACTION_PAUSE_RESUME
)
}

override fun getNotification(scheduledObject: ScheduledObject) = NotificationCompat.Builder(
this,
NotificationHelper.STOPWATCH_CHANNEL
)
.setContentTitle(getText(R.string.stopwatch))
.setUsesChronometer(scheduledObject.state.value == WatchState.RUNNING)
.setWhen(System.currentTimeMillis() - scheduledObject.currentPosition.value)
.addAction(stopAction(scheduledObject))
.addAction(pauseResumeAction(scheduledObject))
.setSmallIcon(R.drawable.ic_notification)
.build()
private fun getPendingIntent(intent: Intent, requestCode: Int): PendingIntent =
PendingIntent.getBroadcast(
this,
requestCode,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)

private val binder = LocalBinder()
override fun onBind(intent: Intent) = binder

override fun getStartNotification() = getNotification(ScheduledObject())
inner class LocalBinder : Binder() {
fun getService() = this@StopwatchService
}

companion object {
private const val UPDATE_DELAY = 10

const val STOPWATCH_INTENT_ACTION = "com.bnyro.clock.STOPWATCH_ACTION"
const val ACTION_EXTRA_KEY = "action"
const val ACTION_PAUSE_RESUME = "pause_resume"
const val ACTION_STOP = "stop"
const val ACTION_START = "start"

const val STOP_ACTION_REQUEST_CODE = 6
const val PAUSE_RESUME_ACTION_REQUEST_CODE = 7
}
}
52 changes: 51 additions & 1 deletion app/src/main/java/com/bnyro/clock/ui/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
package com.bnyro.clock.ui

import android.Manifest
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.provider.AlarmClock
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
Expand All @@ -17,9 +22,11 @@ import androidx.compose.ui.Modifier
import androidx.core.app.ActivityCompat
import androidx.lifecycle.viewmodel.compose.viewModel
import com.bnyro.clock.obj.Alarm
import com.bnyro.clock.services.StopwatchService
import com.bnyro.clock.ui.dialog.AlarmReceiverDialog
import com.bnyro.clock.ui.dialog.TimerReceiverDialog
import com.bnyro.clock.ui.model.SettingsModel
import com.bnyro.clock.ui.model.StopwatchModel
import com.bnyro.clock.ui.nav.NavContainer
import com.bnyro.clock.ui.nav.NavRoutes
import com.bnyro.clock.ui.nav.bottomNavItems
Expand All @@ -28,12 +35,38 @@ import com.bnyro.clock.util.Preferences
import com.bnyro.clock.util.ThemeUtil

class MainActivity : ComponentActivity() {

val stopwatchModel by viewModels<StopwatchModel>()

lateinit var stopwatchService: StopwatchService
private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
val binder = (service as StopwatchService.LocalBinder)
stopwatchService = binder.getService()
stopwatchModel.state = stopwatchService.state
stopwatchModel.currentPosition = stopwatchService.currentPosition

stopwatchService.onStateChange = {
stopwatchModel.state = it
}
stopwatchService.onPositionChange = {
stopwatchModel.currentPosition = it
}
}

override fun onServiceDisconnected(p0: ComponentName?) {
stopwatchService.onStateChange = {}
stopwatchService.onPositionChange = {}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContent {
val settingsModel: SettingsModel = viewModel()

val initialTab = when (intent?.action) {
SHOW_STOPWATCH_ACTION -> NavRoutes.Stopwatch
AlarmClock.ACTION_SET_ALARM, AlarmClock.ACTION_SHOW_ALARMS -> NavRoutes.Alarm
AlarmClock.ACTION_SET_TIMER, AlarmClock.ACTION_SHOW_TIMERS -> NavRoutes.Timer
else -> bottomNavItems.first {
Expand Down Expand Up @@ -75,6 +108,19 @@ class MainActivity : ComponentActivity() {
}
}

override fun onStart() {
super.onStart()
Intent(this, StopwatchService::class.java).also { intent ->
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}
}

override fun onStop() {
super.onStop()
unbindService(serviceConnection)
}


private fun getInitialAlarm(): Alarm? {
if (intent?.action != AlarmClock.ACTION_SET_ALARM) return null

Expand Down Expand Up @@ -116,4 +162,8 @@ class MainActivity : ComponentActivity() {
)
}
}

companion object {
const val SHOW_STOPWATCH_ACTION = "com.bnyro.clock.SHOW_STOPWATCH_ACTION"
}
}
Loading

0 comments on commit 4b7b655

Please sign in to comment.