-
-
Notifications
You must be signed in to change notification settings - Fork 33
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: improved stopwatch service and notification
- Loading branch information
1 parent
23690b6
commit 4b7b655
Showing
5 changed files
with
280 additions
and
94 deletions.
There are no files selected for viewing
217 changes: 191 additions & 26 deletions
217
app/src/main/java/com/bnyro/clock/services/StopwatchService.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.