Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: timer service #332

Merged
merged 1 commit into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 ->
[email protected]()
[email protected](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
Loading