Skip to content

Commit

Permalink
Merge pull request #332 from SuhasDissa/timer-service
Browse files Browse the repository at this point in the history
fix: timer service
  • Loading branch information
SuhasDissa authored Apr 16, 2024
2 parents fa50a0d + 9c465fa commit 6c25e97
Show file tree
Hide file tree
Showing 9 changed files with 313 additions and 297 deletions.
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("kotlin-parcelize")
id("com.google.devtools.ksp")
id("org.jetbrains.kotlin.plugin.serialization") version "1.9.23"
}
Expand Down
18 changes: 18 additions & 0 deletions app/src/main/java/com/bnyro/clock/domain/model/TimerDescriptor.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.bnyro.clock.domain.model

import android.os.Parcelable
import androidx.compose.runtime.mutableStateOf
import kotlinx.parcelize.Parcelize

@Parcelize
data class TimerDescriptor(
var id: Int = 0,
var currentPosition: Int = 0,
) : Parcelable {
fun asScheduledObject(): TimerObject {
return TimerObject(
id = id,
currentPosition = mutableStateOf(currentPosition),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import android.net.Uri
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf

data class ScheduledObject(
data class TimerObject(
var id: Int = 0,
var label: MutableState<String?> = mutableStateOf(null),
var currentPosition: MutableState<Int> = mutableStateOf(0),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
Expand All @@ -33,7 +33,7 @@ import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
Expand Down Expand Up @@ -65,15 +65,14 @@ fun TimerScreen(onClickSettings: () -> Unit, timerModel: TimerModel) {
val useScrollPicker = Preferences.instance.getBoolean(Preferences.timerUsePickerKey, false)
val showExampleTimers = Preferences.instance.getBoolean(Preferences.timerShowExamplesKey, true)

LaunchedEffect(Unit) {
timerModel.tryConnect(context)
}
var createNew by remember {
mutableStateOf(false)
}

val scheduledObjects by timerModel.scheduledObjects.collectAsState()

TopBarScaffold(title = stringResource(R.string.timer), onClickSettings, actions = {
if (timerModel.scheduledObjects.isEmpty()) {
if (scheduledObjects.isEmpty()) {
ClickableIcon(
imageVector = Icons.Rounded.AddAlarm,
contentDescription = stringResource(R.string.add_preset_timer)
Expand All @@ -89,7 +88,7 @@ fun TimerScreen(onClickSettings: () -> Unit, timerModel: TimerModel) {
}
}
}) { paddingValues ->
if (timerModel.scheduledObjects.isEmpty()) {
if (scheduledObjects.isEmpty()) {
Column(
Modifier
.padding(paddingValues)
Expand All @@ -112,8 +111,8 @@ fun TimerScreen(onClickSettings: () -> Unit, timerModel: TimerModel) {
.padding(paddingValues),
verticalArrangement = Arrangement.Top
) {
itemsIndexed(timerModel.scheduledObjects) { index, obj ->
TimerItem(obj, index, timerModel)
items(scheduledObjects, key = { it.id }) { obj ->
TimerItem(obj, timerModel)
}
}
KeepScreenOn()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.bnyro.clock.R
import com.bnyro.clock.domain.model.ScheduledObject
import com.bnyro.clock.domain.model.TimerObject
import com.bnyro.clock.domain.model.WatchState
import com.bnyro.clock.presentation.components.ClickableIcon
import com.bnyro.clock.presentation.components.DialogButton
Expand All @@ -44,7 +44,7 @@ import com.bnyro.clock.presentation.screens.timer.model.TimerModel
import com.bnyro.clock.util.extensions.addZero

@Composable
fun TimerItem(obj: ScheduledObject, index: Int, timerModel: TimerModel) {
fun TimerItem(obj: TimerObject, timerModel: TimerModel) {
val context = LocalContext.current
val hours = obj.currentPosition.value / 3600000
val minutes = (obj.currentPosition.value % 3600000) / 60000
Expand Down Expand Up @@ -94,15 +94,11 @@ fun TimerItem(obj: ScheduledObject, index: Int, timerModel: TimerModel) {
showRingtoneEditor = true
}
ClickableIcon(imageVector = Icons.Default.Close) {
timerModel.stopTimer(context, index)
timerModel.stopTimer(context, obj.id)
}
FilledIconButton(
onClick = {
when (obj.state.value) {
WatchState.PAUSED -> timerModel.resumeTimer(index)
WatchState.RUNNING -> timerModel.pauseTimer(index)
else -> timerModel.startTimer(context)
}
timerModel.pauseResumeTimer(context, obj.id)
}
) {
Icon(
Expand Down Expand Up @@ -135,7 +131,7 @@ fun TimerItem(obj: ScheduledObject, index: Int, timerModel: TimerModel) {
onDismissRequest = { showLabelEditor = false },
confirmButton = {
DialogButton(android.R.string.ok) {
timerModel.service?.updateLabel(obj.id, newLabel)
timerModel.updateLabel(obj.id, newLabel)
newLabel = ""
showLabelEditor = false
}
Expand Down Expand Up @@ -173,14 +169,14 @@ fun TimerItem(obj: ScheduledObject, index: Int, timerModel: TimerModel) {
Checkbox(
checked = obj.vibrate,
onCheckedChange = {
timerModel.service?.updateVibrate(obj.id, it)
timerModel.updateVibrate(obj.id, it)
}
)
Text(text = stringResource(R.string.vibrate))
}
}
) { _, uri ->
timerModel.service?.updateRingtone(obj.id, uri)
timerModel.updateRingtone(obj.id, uri)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,29 +1,28 @@
package com.bnyro.clock.presentation.screens.timer.model

import android.annotation.SuppressLint
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.net.Uri
import androidx.compose.runtime.SnapshotMutationPolicy
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModel
import com.bnyro.clock.domain.model.PersistentTimer
import com.bnyro.clock.domain.model.ScheduledObject
import com.bnyro.clock.util.services.ScheduleService
import com.bnyro.clock.domain.model.TimerDescriptor
import com.bnyro.clock.domain.model.TimerObject
import com.bnyro.clock.util.services.TimerService
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow

class TimerModel : ViewModel() {
var scheduledObjects = mutableStateListOf<ScheduledObject>()
private var objectToEnqueue: ScheduledObject? = null
val _timerObjects = MutableStateFlow(emptyList<TimerObject>())
val scheduledObjects = _timerObjects.asStateFlow()

@SuppressLint("StaticFieldLeak")
var service: TimerService? = null
var onEnqueue: ((timer: TimerObject) -> Unit)? = null
var updateLabel: (id: Int, newLabel: String) -> Unit = { _, _ -> }
var updateRingtone: (id: Int, newRingtoneUri: Uri?) -> Unit = { _, _ -> }
var updateVibrate: (id: Int, vibrate: Boolean) -> Unit = { _, _ -> }

var persistentTimers by mutableStateOf(
PersistentTimer.getTimers(),
Expand Down Expand Up @@ -55,21 +54,9 @@ class TimerModel : ViewModel() {
timePickerSeconds += (value - seconds)
}

private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(component: ComponentName, binder: IBinder) {
service = (binder as ScheduleService.LocalBinder).getService() as? TimerService
service?.changeListener = { objects ->
this@TimerModel.scheduledObjects.clear()
this@TimerModel.scheduledObjects.addAll(objects)
}
objectToEnqueue?.let { service?.enqueueNew(it) }
objectToEnqueue = null
}

override fun onServiceDisconnected(p0: ComponentName?) {
scheduledObjects.clear()
service = null
}
fun onChangeTimers(objects: Array<TimerObject>) {
_timerObjects.value = listOf(*objects)
}

fun removePersistentTimer(index: Int) {
Expand All @@ -84,60 +71,52 @@ class TimerModel : ViewModel() {
val totalSeconds = delay ?: timePickerSeconds
if (totalSeconds == 0) return

if (scheduledObjects.isEmpty()) {
runCatching {
context.unbindService(serviceConnection)
}
service = null
}

val newTimer = ScheduledObject(
label = mutableStateOf(null),
id = System.currentTimeMillis().toInt(),
currentPosition = mutableStateOf(totalSeconds * 1000)
val newTimer = TimerDescriptor(
// id randomized by system current time; used modulo to compensate for integer overflow
id = (System.currentTimeMillis() % Int.MAX_VALUE).toInt(),
currentPosition = totalSeconds * 1000
)

timePickerSeconds = 0
timePickerFakeUnits = 0

if (service == null) {
startService(context)
objectToEnqueue = newTimer
if (onEnqueue == null) {
startService(context, newTimer)
} else {
service?.enqueueNew(newTimer)
onEnqueue?.invoke(newTimer.asScheduledObject())
}
}

private fun startService(context: Context) {
private fun startService(context: Context, timerDescriptor: TimerDescriptor) {
val intent = Intent(context, TimerService::class.java)
runCatching {
context.stopService(intent)
}
runCatching {
context.unbindService(serviceConnection)
}
ContextCompat.startForegroundService(context, intent)
context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}

fun tryConnect(context: Context) {
val intent = Intent(context, TimerService::class.java)
context.bindService(intent, serviceConnection, Context.BIND_ABOVE_CLIENT)
}

fun pauseTimer(index: Int) {
service?.pause(scheduledObjects[index])
.putExtra(TimerService.INITIAL_TIMER_EXTRA_KEY, timerDescriptor)
context.startService(intent)
}

fun resumeTimer(index: Int) {
service?.resume(scheduledObjects[index])
fun pauseResumeTimer(context: Context, index: Int) {
val pauseResumeIntent = Intent(TimerService.UPDATE_STATE_ACTION)
.putExtra(
TimerService.ID_EXTRA_KEY,
index
)
.putExtra(
TimerService.ACTION_EXTRA_KEY,
TimerService.ACTION_PAUSE_RESUME
)
context.sendBroadcast(pauseResumeIntent)
}

fun stopTimer(context: Context, index: Int) {
val obj = scheduledObjects[index]
scheduledObjects.removeAt(index)
service?.stop(obj)
if (scheduledObjects.isEmpty()) context.unbindService(serviceConnection)
val stopIntent = Intent(TimerService.UPDATE_STATE_ACTION)
.putExtra(
TimerService.ID_EXTRA_KEY,
index
)
.putExtra(
TimerService.ACTION_EXTRA_KEY,
TimerService.ACTION_STOP
)
context.sendBroadcast(stopIntent)
}

/* =============== Numpad time picker ======================== */
Expand Down
33 changes: 33 additions & 0 deletions app/src/main/java/com/bnyro/clock/ui/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,21 @@ import com.bnyro.clock.presentation.features.AlarmReceiverDialog
import com.bnyro.clock.presentation.features.TimerReceiverDialog
import com.bnyro.clock.presentation.screens.settings.model.SettingsModel
import com.bnyro.clock.presentation.screens.stopwatch.model.StopwatchModel
import com.bnyro.clock.presentation.screens.timer.model.TimerModel
import com.bnyro.clock.ui.theme.ClockYouTheme
import com.bnyro.clock.util.Preferences
import com.bnyro.clock.util.ThemeUtil
import com.bnyro.clock.util.services.StopwatchService
import com.bnyro.clock.util.services.TimerService

class MainActivity : ComponentActivity() {

val stopwatchModel by viewModels<StopwatchModel>()
val timerModel by viewModels<TimerModel>()
private var initialTab: NavRoutes = NavRoutes.Alarm

lateinit var stopwatchService: StopwatchService

private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
val binder = (service as StopwatchService.LocalBinder)
Expand All @@ -62,6 +66,31 @@ class MainActivity : ComponentActivity() {
}
}

lateinit var timerService: TimerService

private val timerServiceConnection = object : ServiceConnection {
override fun onServiceConnected(component: ComponentName, service: IBinder) {
val binder = (service as TimerService.LocalBinder)
timerService = binder.getService()
timerService.onChangeTimers = timerModel::onChangeTimers

timerModel.onEnqueue = {
timerService.enqueueNew(it)
}
timerModel.updateLabel = timerService::updateLabel
timerModel.updateRingtone = timerService::updateRingtone
timerModel.updateVibrate = timerService::updateVibrate
}

override fun onServiceDisconnected(p0: ComponentName?) {
timerService.onChangeTimers = {}
timerModel.onEnqueue = null
timerModel.updateLabel = { _, _ -> }
timerModel.updateRingtone = { _, _ -> }
timerModel.updateVibrate = { _, _ -> }
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

Expand Down Expand Up @@ -119,11 +148,15 @@ class MainActivity : ComponentActivity() {
Intent(this, StopwatchService::class.java).also { intent ->
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}
Intent(this, TimerService::class.java).also { intent ->
bindService(intent, timerServiceConnection, Context.BIND_AUTO_CREATE)
}
}

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


Expand Down
Loading

0 comments on commit 6c25e97

Please sign in to comment.