handle multiple timer states

- feat: separate timer actions from states by adding a new class TimerEvent to hold actions Reset, Start, Pause, Finish, Refesh that could be performed on a timer.
- feat: handle multiple countdown timers in the App file by creating a map of the timer id to the countdown timer.
- fix: use gson instance from  TypeAdapter in Room's Converters class
- ref: remove scroll view parent from each timer item, a fix for the keyboard obscuring a label will be implemented in a future commit
This commit is contained in:
Paul Akhamiogu 2021-09-01 19:58:38 +01:00
parent 921ca92885
commit 8474e6a800
15 changed files with 266 additions and 232 deletions

View File

@ -5,16 +5,18 @@ import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.os.CountDownTimer import android.os.CountDownTimer
import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import com.facebook.stetho.Stetho import com.facebook.stetho.Stetho
import com.simplemobiletools.clock.extensions.config
import com.simplemobiletools.clock.extensions.getOpenTimerTabIntent import com.simplemobiletools.clock.extensions.getOpenTimerTabIntent
import com.simplemobiletools.clock.extensions.getTimerNotification import com.simplemobiletools.clock.extensions.getTimerNotification
import com.simplemobiletools.clock.extensions.timerHelper
import com.simplemobiletools.clock.helpers.TIMER_NOTIF_ID import com.simplemobiletools.clock.helpers.TIMER_NOTIF_ID
import com.simplemobiletools.clock.models.TimerEvent
import com.simplemobiletools.clock.models.TimerState import com.simplemobiletools.clock.models.TimerState
import com.simplemobiletools.clock.services.TimerStopService import com.simplemobiletools.clock.services.TimerStopService
import com.simplemobiletools.clock.services.startTimerService import com.simplemobiletools.clock.services.startTimerService
@ -25,8 +27,7 @@ import org.greenrobot.eventbus.ThreadMode
class App : Application(), LifecycleObserver { class App : Application(), LifecycleObserver {
private var timer: CountDownTimer? = null private var timers = mutableMapOf<Long, CountDownTimer>()
private var lastTick = 0L
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -48,63 +49,83 @@ class App : Application(), LifecycleObserver {
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
@OnLifecycleEvent(Lifecycle.Event.ON_STOP) @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
private fun onAppBackgrounded() { private fun onAppBackgrounded() {
if (config.timerState is TimerState.Running) { timerHelper.getTimers { timers ->
startTimerService(this) if (timers.any { it.state is TimerState.Running }) {
startTimerService(this)
}
} }
} }
@OnLifecycleEvent(Lifecycle.Event.ON_START) @OnLifecycleEvent(Lifecycle.Event.ON_START)
private fun onAppForegrounded() { private fun onAppForegrounded() {
EventBus.getDefault().post(TimerStopService) EventBus.getDefault().post(TimerStopService)
timerHelper.getTimers { timers ->
val runningTimers = timers.filter { it.state is TimerState.Running }
runningTimers.forEach { timer ->
EventBus.getDefault().post(TimerEvent.Start(timer.id!!, (timer.state as TimerState.Running).tick))
}
}
} }
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
fun onMessageEvent(state: TimerState.Idle) { fun onMessageEvent(event: TimerEvent.Reset) {
config.timerState = state Log.w(TAG, "onMessageEvent: $event")
timer?.cancel() updateTimerState(event.timerId, TimerState.Idle)
timers[event.timerId]?.cancel()
} }
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
fun onMessageEvent(state: TimerState.Start) { fun onMessageEvent(event: TimerEvent.Start) {
timer = object : CountDownTimer(state.duration, 1000) { Log.w(TAG, "onMessageEvent: $event")
val countDownTimer = object : CountDownTimer(event.duration, 1000) {
override fun onTick(tick: Long) { override fun onTick(tick: Long) {
lastTick = tick Log.d(TAG, "onMessageEvent--> $tick")
updateTimerState(event.timerId, TimerState.Running(event.duration, tick))
val newState = TimerState.Running(state.duration, tick)
EventBus.getDefault().post(newState)
config.timerState = newState
} }
override fun onFinish() { override fun onFinish() {
EventBus.getDefault().post(TimerState.Finish(state.duration)) EventBus.getDefault().post(TimerEvent.Finish(event.timerId, event.duration))
EventBus.getDefault().post(TimerStopService) EventBus.getDefault().post(TimerStopService)
} }
}.start() }.start()
timers[event.timerId] = countDownTimer
} }
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
fun onMessageEvent(event: TimerState.Finish) { fun onMessageEvent(event: TimerEvent.Finish) {
val pendingIntent = getOpenTimerTabIntent() Log.w(TAG, "onMessageEvent: $event")
val notification = getTimerNotification(pendingIntent, false) timerHelper.getTimer(event.timerId) { timer ->
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val pendingIntent = getOpenTimerTabIntent(event.timerId)
notificationManager.notify(TIMER_NOTIF_ID, notification) val notification = getTimerNotification(timer, pendingIntent, false)
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
EventBus.getDefault().post(TimerState.Finished) notificationManager.notify(TIMER_NOTIF_ID, notification)
updateTimerState(event.timerId, TimerState.Finished)
}
} }
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
fun onMessageEvent(state: TimerState.Finished) { fun onMessageEvent(event: TimerEvent.Pause) {
config.timerState = state Log.w(TAG, "onMessageEvent: $event")
timerHelper.getTimer(event.timerId) { timer ->
updateTimerState(event.timerId, TimerState.Paused(event.duration, (timer.state as TimerState.Running).tick))
timers[event.timerId]?.cancel()
}
} }
@Subscribe(threadMode = ThreadMode.MAIN) private fun updateTimerState(timerId: Long, state: TimerState) {
fun onMessageEvent(event: TimerState.Pause) { timerHelper.getTimer(timerId) { timer ->
EventBus.getDefault().post(TimerState.Paused(event.duration, lastTick)) val newTimer = timer.copy(state = state)
Log.w(TAG, "updateTimerState: $newTimer")
timerHelper.insertOrUpdateTimer(newTimer) {
EventBus.getDefault().post(TimerEvent.Refresh(timerId))
timerHelper.getTimer(timerId) {
Log.e(TAG, "updated timer $it")
}
}
}
} }
@Subscribe(threadMode = ThreadMode.MAIN) companion object {
fun onMessageEvent(state: TimerState.Paused) { private const val TAG = "App"
config.timerState = state
timer?.cancel()
} }
} }

View File

@ -2,6 +2,7 @@ package com.simplemobiletools.clock.adapters
import android.media.AudioManager import android.media.AudioManager
import android.media.RingtoneManager import android.media.RingtoneManager
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -11,7 +12,10 @@ import androidx.recyclerview.widget.RecyclerView
import com.simplemobiletools.clock.R import com.simplemobiletools.clock.R
import com.simplemobiletools.clock.activities.SimpleActivity import com.simplemobiletools.clock.activities.SimpleActivity
import com.simplemobiletools.clock.dialogs.MyTimePickerDialogDialog import com.simplemobiletools.clock.dialogs.MyTimePickerDialogDialog
import com.simplemobiletools.clock.extensions.* import com.simplemobiletools.clock.extensions.checkAlarmsWithDeletedSoundUri
import com.simplemobiletools.clock.extensions.colorLeftDrawable
import com.simplemobiletools.clock.extensions.config
import com.simplemobiletools.clock.extensions.timerHelper
import com.simplemobiletools.clock.helpers.PICK_AUDIO_FILE_INTENT_ID import com.simplemobiletools.clock.helpers.PICK_AUDIO_FILE_INTENT_ID
import com.simplemobiletools.clock.models.Timer import com.simplemobiletools.clock.models.Timer
import com.simplemobiletools.clock.models.TimerState import com.simplemobiletools.clock.models.TimerState
@ -20,16 +24,13 @@ import com.simplemobiletools.commons.extensions.getDefaultAlarmSound
import com.simplemobiletools.commons.extensions.getFormattedDuration import com.simplemobiletools.commons.extensions.getFormattedDuration
import com.simplemobiletools.commons.extensions.onTextChangeListener import com.simplemobiletools.commons.extensions.onTextChangeListener
import com.simplemobiletools.commons.models.AlarmSound import com.simplemobiletools.commons.models.AlarmSound
import kotlin.math.roundToInt
import kotlinx.android.synthetic.main.item_timer.view.* import kotlinx.android.synthetic.main.item_timer.view.*
import org.greenrobot.eventbus.EventBus
class TimerAdapter( class TimerAdapter(
private val activity: SimpleActivity, private val activity: SimpleActivity,
private val onRefresh: () -> Unit, private val onRefresh: () -> Unit,
) : ListAdapter<Timer, TimerAdapter.TimerViewHolder>(diffUtil) {
) : ListAdapter<Timer, TimerAdapter.TimerViewHolder>(diffUtil) {
private val config = activity.config
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TimerViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TimerViewHolder {
return TimerViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_timer, parent, false)) return TimerViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_timer, parent, false))
@ -45,28 +46,32 @@ class TimerAdapter(
inner class TimerViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { inner class TimerViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
init {
itemView.timer_label.onTextChangeListener { text ->
Log.w(TAG, "timer_label")
updateTimer(getItemAt(adapterPosition).copy(label = text), false)
}
itemView.post {
val textColor = activity.config.textColor
itemView.timer_initial_time.colorLeftDrawable(textColor)
itemView.timer_vibrate.colorLeftDrawable(textColor)
itemView.timer_sound.colorLeftDrawable(textColor)
}
}
fun bind(timer: Timer) { fun bind(timer: Timer) {
itemView.apply { itemView.apply {
timer_time.text = timer.seconds.getFormattedDuration() //only update when different to prevent flickering and unnecessary updates
timer_label.setText(timer.label) if (timer_label.text.toString() != timer.label) {
timer_label.setText(timer.label)
timer_time.text = timer.seconds.getFormattedDuration() }
timer_label.setText(timer.label)
val textColor = activity.config.textColor
timer_initial_time.text = timer.seconds.getFormattedDuration() timer_initial_time.text = timer.seconds.getFormattedDuration()
timer_initial_time.colorLeftDrawable(textColor)
timer_vibrate.isChecked = timer.vibrate timer_vibrate.isChecked = timer.vibrate
timer_vibrate.colorLeftDrawable(textColor)
timer_sound.text = timer.soundTitle timer_sound.text = timer.soundTitle
timer_sound.colorLeftDrawable(textColor)
timer_time.setOnClickListener {
stopTimer(timer)
}
timer_time.setOnClickListener { timer_time.setOnClickListener {
changeDuration(timer) changeDuration(timer)
@ -77,12 +82,13 @@ class TimerAdapter(
} }
timer_vibrate_holder.setOnClickListener { timer_vibrate_holder.setOnClickListener {
Log.w(TAG, "toggle")
timer_vibrate.toggle() timer_vibrate.toggle()
updateTimer(timer.copy(vibrate = timer_vibrate.isChecked), false) updateTimer(timer.copy(vibrate = timer_vibrate.isChecked), false)
} }
timer_sound.setOnClickListener { timer_sound.setOnClickListener {
SelectAlarmSoundDialog(activity, config.timerSoundUri, AudioManager.STREAM_ALARM, PICK_AUDIO_FILE_INTENT_ID, SelectAlarmSoundDialog(activity, timer.soundUri, AudioManager.STREAM_ALARM, PICK_AUDIO_FILE_INTENT_ID,
RingtoneManager.TYPE_ALARM, true, RingtoneManager.TYPE_ALARM, true,
onAlarmPicked = { sound -> onAlarmPicked = { sound ->
if (sound != null) { if (sound != null) {
@ -99,39 +105,52 @@ class TimerAdapter(
}) })
} }
timer_label.onTextChangeListener { text ->
updateTimer(timer.copy(label = text), false) when (timer.state) {
is TimerState.Finished -> {
timer_time.text = 0.getFormattedDuration()
}
is TimerState.Idle -> {
timer_time.text = timer.seconds.getFormattedDuration()
}
is TimerState.Paused -> {
timer_time.text = timer.state.tick.div(1000F).roundToInt().getFormattedDuration()
}
is TimerState.Running -> {
timer_time.text = timer.state.tick.div(1000F).roundToInt().getFormattedDuration()
}
} }
} }
} }
} }
private fun stopTimer(timer: Timer) {
EventBus.getDefault().post(TimerState.Idle)
activity.hideTimerNotification()
}
private fun changeDuration(timer: Timer) { private fun changeDuration(timer: Timer) {
MyTimePickerDialogDialog(activity, timer.seconds) { seconds -> MyTimePickerDialogDialog(activity, timer.seconds) { seconds ->
val timerSeconds = if (seconds <= 0) 10 else seconds val timerSeconds = if (seconds <= 0) 10 else seconds
Log.w(TAG, "changeDuration")
updateTimer(timer.copy(seconds = timerSeconds)) updateTimer(timer.copy(seconds = timerSeconds))
} }
} }
fun updateAlarmSound(timer: Timer, alarmSound: AlarmSound) { fun updateAlarmSound(timer: Timer, alarmSound: AlarmSound) {
Log.w(TAG, "updateAlarmSound: $timer")
updateTimer(timer.copy(soundTitle = alarmSound.title, soundUri = alarmSound.uri)) updateTimer(timer.copy(soundTitle = alarmSound.title, soundUri = alarmSound.uri))
} }
private fun updateTimer(timer: Timer, refresh: Boolean = true) { private fun updateTimer(timer: Timer, refresh: Boolean = true) {
Log.w(TAG, "updateTimer: $timer")
activity.timerHelper.insertOrUpdateTimer(timer) activity.timerHelper.insertOrUpdateTimer(timer)
if (refresh) { if (refresh) {
onRefresh.invoke() onRefresh.invoke()
} }
} }
companion object { companion object {
val diffUtil = object : DiffUtil.ItemCallback<Timer>() { private const val TAG = "TimerAdapter"
private val diffUtil = object : DiffUtil.ItemCallback<Timer>() {
override fun areItemsTheSame(oldItem: Timer, newItem: Timer): Boolean { override fun areItemsTheSame(oldItem: Timer, newItem: Timer): Boolean {
return oldItem.id == newItem.id return oldItem.id == newItem.id
} }

View File

@ -11,6 +11,7 @@ import com.simplemobiletools.clock.fragments.TimerFragment
import com.simplemobiletools.clock.helpers.TABS_COUNT import com.simplemobiletools.clock.helpers.TABS_COUNT
import com.simplemobiletools.clock.helpers.TAB_ALARM import com.simplemobiletools.clock.helpers.TAB_ALARM
import com.simplemobiletools.clock.helpers.TAB_CLOCK import com.simplemobiletools.clock.helpers.TAB_CLOCK
import com.simplemobiletools.clock.helpers.TAB_TIMER
import com.simplemobiletools.commons.models.AlarmSound import com.simplemobiletools.commons.models.AlarmSound
class ViewPagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm) { class ViewPagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm) {
@ -50,6 +51,6 @@ class ViewPagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm) {
} }
fun updateTimerTabAlarmSound(alarmSound: AlarmSound) { fun updateTimerTabAlarmSound(alarmSound: AlarmSound) {
// (fragments[TAB_TIMER] as? TimerFragment)?.updateAlarmSound(alarmSound) (fragments[TAB_TIMER] as? TimerFragment)?.updateAlarmSound(alarmSound)
} }
} }

View File

@ -25,6 +25,7 @@ import com.simplemobiletools.clock.helpers.*
import com.simplemobiletools.clock.interfaces.TimerDao import com.simplemobiletools.clock.interfaces.TimerDao
import com.simplemobiletools.clock.models.Alarm import com.simplemobiletools.clock.models.Alarm
import com.simplemobiletools.clock.models.MyTimeZone import com.simplemobiletools.clock.models.MyTimeZone
import com.simplemobiletools.clock.models.Timer
import com.simplemobiletools.clock.receivers.AlarmReceiver import com.simplemobiletools.clock.receivers.AlarmReceiver
import com.simplemobiletools.clock.receivers.DateTimeWidgetUpdateReceiver import com.simplemobiletools.clock.receivers.DateTimeWidgetUpdateReceiver
import com.simplemobiletools.clock.receivers.HideAlarmReceiver import com.simplemobiletools.clock.receivers.HideAlarmReceiver
@ -139,9 +140,10 @@ fun Context.getOpenAlarmTabIntent(): PendingIntent {
return PendingIntent.getActivity(this, OPEN_ALARMS_TAB_INTENT_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT) return PendingIntent.getActivity(this, OPEN_ALARMS_TAB_INTENT_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT)
} }
fun Context.getOpenTimerTabIntent(): PendingIntent { fun Context.getOpenTimerTabIntent(timerId: Long): PendingIntent {
val intent = getLaunchIntent() ?: Intent(this, SplashActivity::class.java) val intent = getLaunchIntent() ?: Intent(this, SplashActivity::class.java)
intent.putExtra(OPEN_TAB, TAB_TIMER) intent.putExtra(OPEN_TAB, TAB_TIMER)
intent.putExtra(TIMER_ID, timerId)
return PendingIntent.getActivity(this, TIMER_NOTIF_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT) return PendingIntent.getActivity(this, TIMER_NOTIF_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT)
} }
@ -260,8 +262,8 @@ fun Context.showAlarmNotification(alarm: Alarm) {
} }
@SuppressLint("NewApi") @SuppressLint("NewApi")
fun Context.getTimerNotification(pendingIntent: PendingIntent, addDeleteIntent: Boolean): Notification { fun Context.getTimerNotification(timer: Timer, pendingIntent: PendingIntent, addDeleteIntent: Boolean): Notification {
var soundUri = config.timerSoundUri var soundUri = timer.soundUri
if (soundUri == SILENT) { if (soundUri == SILENT) {
soundUri = "" soundUri = ""
} else { } else {
@ -292,7 +294,7 @@ fun Context.getTimerNotification(pendingIntent: PendingIntent, addDeleteIntent:
lightColor = getAdjustedPrimaryColor() lightColor = getAdjustedPrimaryColor()
setSound(Uri.parse(soundUri), audioAttributes) setSound(Uri.parse(soundUri), audioAttributes)
if (!config.timerVibrate) { if (!timer.vibrate) {
vibrationPattern = longArrayOf(0L) vibrationPattern = longArrayOf(0L)
} }
@ -321,7 +323,7 @@ fun Context.getTimerNotification(pendingIntent: PendingIntent, addDeleteIntent:
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
if (config.timerVibrate) { if (timer.vibrate) {
val vibrateArray = LongArray(2) { 500 } val vibrateArray = LongArray(2) { 500 }
builder.setVibrate(vibrateArray) builder.setVibrate(vibrateArray)
} }

View File

@ -7,11 +7,8 @@ import com.simplemobiletools.clock.models.TimerState
val timerStates = valueOf<TimerState>() val timerStates = valueOf<TimerState>()
.registerSubtype(TimerState.Idle::class.java) .registerSubtype(TimerState.Idle::class.java)
.registerSubtype(TimerState.Start::class.java)
.registerSubtype(TimerState.Running::class.java) .registerSubtype(TimerState.Running::class.java)
.registerSubtype(TimerState.Pause::class.java)
.registerSubtype(TimerState.Paused::class.java) .registerSubtype(TimerState.Paused::class.java)
.registerSubtype(TimerState.Finish::class.java)
.registerSubtype(TimerState.Finished::class.java) .registerSubtype(TimerState.Finished::class.java)
inline fun <reified T : Any> valueOf(): RuntimeTypeAdapterFactory<T> = RuntimeTypeAdapterFactory.of(T::class.java) inline fun <reified T : Any> valueOf(): RuntimeTypeAdapterFactory<T> = RuntimeTypeAdapterFactory.of(T::class.java)

View File

@ -2,6 +2,7 @@ package com.simplemobiletools.clock.fragments
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -10,13 +11,16 @@ import androidx.viewpager2.widget.ViewPager2
import com.simplemobiletools.clock.R import com.simplemobiletools.clock.R
import com.simplemobiletools.clock.activities.SimpleActivity import com.simplemobiletools.clock.activities.SimpleActivity
import com.simplemobiletools.clock.adapters.TimerAdapter import com.simplemobiletools.clock.adapters.TimerAdapter
import com.simplemobiletools.clock.dialogs.MyTimePickerDialogDialog
import com.simplemobiletools.clock.extensions.config import com.simplemobiletools.clock.extensions.config
import com.simplemobiletools.clock.extensions.hideTimerNotification import com.simplemobiletools.clock.extensions.hideTimerNotification
import com.simplemobiletools.clock.extensions.secondsToMillis
import com.simplemobiletools.clock.extensions.timerHelper import com.simplemobiletools.clock.extensions.timerHelper
import com.simplemobiletools.clock.models.Timer import com.simplemobiletools.clock.models.Timer
import com.simplemobiletools.clock.models.TimerEvent
import com.simplemobiletools.clock.models.TimerState import com.simplemobiletools.clock.models.TimerState
import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.models.AlarmSound
import kotlinx.android.synthetic.main.fragment_timer.timer_view_pager
import kotlinx.android.synthetic.main.fragment_timer.view.* import kotlinx.android.synthetic.main.fragment_timer.view.*
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
@ -24,7 +28,7 @@ import org.greenrobot.eventbus.ThreadMode
class TimerFragment : Fragment() { class TimerFragment : Fragment() {
lateinit var view: ViewGroup private lateinit var view: ViewGroup
private lateinit var timerAdapter: TimerAdapter private lateinit var timerAdapter: TimerAdapter
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -42,7 +46,10 @@ class TimerFragment : Fragment() {
timerAdapter = TimerAdapter(requireActivity() as SimpleActivity) { timerAdapter = TimerAdapter(requireActivity() as SimpleActivity) {
refreshTimers() refreshTimers()
} }
timer_view_pager.adapter = timerAdapter timer_view_pager.adapter = timerAdapter
//set empty page transformer to disable item animations
timer_view_pager.setPageTransformer { _, _ -> }
timer_view_pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { timer_view_pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) { override fun onPageSelected(position: Int) {
updateViews(position) updateViews(position)
@ -64,16 +71,13 @@ class TimerFragment : Fragment() {
timer_play_pause.applyColorFilter(if (activity?.getAdjustedPrimaryColor() == Color.WHITE) Color.BLACK else Color.WHITE) timer_play_pause.applyColorFilter(if (activity?.getAdjustedPrimaryColor() == Color.WHITE) Color.BLACK else Color.WHITE)
timer_reset.applyColorFilter(textColor) timer_reset.applyColorFilter(textColor)
timer_play_pause.setOnClickListener { timer_play_pause.setOnClickListener {
val timer = timerAdapter.getItemAt(timer_view_pager.currentItem) val timer = timerAdapter.getItemAt(timer_view_pager.currentItem)
when (val state = timer.state) { when (val state = timer.state) {
// is TimerState.Idle -> EventBus.getDefault().post(TimerState.Start(timer.seconds.secondsToMillis)) is TimerState.Idle -> EventBus.getDefault().post(TimerEvent.Start(timer.id!!, timer.seconds.secondsToMillis))
// is TimerState.Paused -> EventBus.getDefault().post(TimerState.Start(state.tick)) is TimerState.Paused -> EventBus.getDefault().post(TimerEvent.Start(timer.id!!, state.tick))
// is TimerState.Running -> EventBus.getDefault().post(TimerState.Pause(state.tick)) is TimerState.Running -> EventBus.getDefault().post(TimerEvent.Pause(timer.id!!, state.tick))
// is TimerState.Finished -> EventBus.getDefault().post(TimerState.Start(timer.seconds.secondsToMillis)) is TimerState.Finished -> EventBus.getDefault().post(TimerEvent.Start(timer.id!!, timer.seconds.secondsToMillis))
else -> {
}
} }
} }
@ -95,67 +99,39 @@ class TimerFragment : Fragment() {
} }
private fun updateViews(position: Int) { private fun updateViews(position: Int) {
val timer = timerAdapter.getItemAt(position) activity?.runOnUiThread {
//check if timer is running to update view val timer = timerAdapter.getItemAt(position)
updateViewStates(timer.state)
}
} }
private fun refreshTimers(scrollToLast: Boolean = false) { private fun refreshTimers(scrollToLast: Boolean = false) {
activity?.timerHelper?.getTimers { timers -> activity?.timerHelper?.getTimers { timers ->
timerAdapter.submitList(timers) Log.d(TAG, "refreshTimers: $timers")
activity?.runOnUiThread { timerAdapter.submitList(timers) {
view.timer_delete.beVisibleIf(timers.size > 1)
if (scrollToLast) { if (scrollToLast) {
view.timer_view_pager.currentItem = timers.lastIndex view.timer_view_pager.currentItem = timers.lastIndex
} }
updateViews(timer_view_pager.currentItem)
} }
} }
} }
private fun stopTimer(timer: Timer) { private fun stopTimer(timer: Timer) {
EventBus.getDefault().post(TimerState.Idle) EventBus.getDefault().post(TimerEvent.Reset(timer.id!!, timer.seconds.secondsToMillis))
activity?.hideTimerNotification() activity?.hideTimerNotification()
// view.timer_time.text = activity?.config?.timerSeconds?.getFormattedDuration()
}
private fun changeDuration() {
MyTimePickerDialogDialog(activity as SimpleActivity, requireContext().config.timerSeconds) { seconds ->
val timerSeconds = if (seconds <= 0) 10 else seconds
activity?.config?.timerSeconds = timerSeconds
val duration = timerSeconds.getFormattedDuration()
// view.timer_initial_time.text = duration
// if (view.timer_reset.isGone()) {
// stopTimer()
// }
}
} }
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
fun onMessageEvent(state: TimerState.Idle) { fun onMessageEvent(event: TimerEvent.Refresh) {
// view.timer_time.text = requiredActivity.config.timerSeconds.getFormattedDuration() Log.d(TAG, "onMessageEvent: $event")
updateViewStates(state) refreshTimers()
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onMessageEvent(state: TimerState.Running) {
// view.timer_time.text = state.tick.div(1000F).roundToInt().getFormattedDuration()
updateViewStates(state)
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onMessageEvent(state: TimerState.Paused) {
updateViewStates(state)
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onMessageEvent(state: TimerState.Finished) {
// view.timer_time.text = 0.getFormattedDuration()
updateViewStates(state)
} }
private fun updateViewStates(state: TimerState) { private fun updateViewStates(state: TimerState) {
val resetPossible = state is TimerState.Running || state is TimerState.Paused || state is TimerState.Finished val resetPossible = state is TimerState.Running || state is TimerState.Paused || state is TimerState.Finished
view.timer_reset.beVisibleIf(resetPossible) view.timer_reset.beVisibleIf(resetPossible)
view.timer_delete.beVisibleIf(!resetPossible && timerAdapter.itemCount > 1)
val drawableId = if (state is TimerState.Running) { val drawableId = if (state is TimerState.Running) {
R.drawable.ic_pause_vector R.drawable.ic_pause_vector
@ -171,10 +147,15 @@ class TimerFragment : Fragment() {
view.timer_play_pause.setImageDrawable(resources.getColoredDrawableWithColor(drawableId, iconColor)) view.timer_play_pause.setImageDrawable(resources.getColoredDrawableWithColor(drawableId, iconColor))
} }
//
// fun updateAlarmSound(alarmSound: AlarmSound) { fun updateAlarmSound(alarmSound: AlarmSound) {
// activity?.config?.timerSoundTitle = alarmSound.title val timer = timerAdapter.getItemAt(timer_view_pager.currentItem)
// activity?.config?.timerSoundUri = alarmSound.uri activity?.timerHelper?.insertOrUpdateTimer(timer.copy(soundTitle = alarmSound.title, soundUri = alarmSound.uri)) {
// view.timer_sound.text = alarmSound.title refreshTimers()
// } }
}
companion object {
private const val TAG = "TimerFragment"
}
} }

View File

@ -45,6 +45,7 @@ const val TAB_CLOCK = 0
const val TAB_ALARM = 1 const val TAB_ALARM = 1
const val TAB_STOPWATCH = 2 const val TAB_STOPWATCH = 2
const val TAB_TIMER = 3 const val TAB_TIMER = 3
const val TIMER_ID = "timer_position"
// stopwatch sorting // stopwatch sorting
const val SORT_BY_LAP = 1 const val SORT_BY_LAP = 1

View File

@ -1,12 +1,11 @@
package com.simplemobiletools.clock.helpers package com.simplemobiletools.clock.helpers
import androidx.room.TypeConverter import androidx.room.TypeConverter
import com.google.gson.Gson import com.simplemobiletools.clock.extensions.gson.gson
import com.simplemobiletools.clock.models.StateWrapper import com.simplemobiletools.clock.models.StateWrapper
import com.simplemobiletools.clock.models.TimerState import com.simplemobiletools.clock.models.TimerState
class Converters { class Converters {
private val gson = Gson()
@TypeConverter @TypeConverter
fun jsonToTimerState(value: String): TimerState { fun jsonToTimerState(value: String): TimerState {

View File

@ -2,6 +2,7 @@ package com.simplemobiletools.clock.helpers
import android.content.Context import android.content.Context
import android.media.RingtoneManager import android.media.RingtoneManager
import android.util.Log
import com.simplemobiletools.clock.extensions.timerDb import com.simplemobiletools.clock.extensions.timerDb
import com.simplemobiletools.clock.models.Timer import com.simplemobiletools.clock.models.Timer
import com.simplemobiletools.clock.models.TimerState import com.simplemobiletools.clock.models.TimerState
@ -12,15 +13,22 @@ import com.simplemobiletools.commons.helpers.ensureBackgroundThread
class TimerHelper(val context: Context) { class TimerHelper(val context: Context) {
private val timerDao = context.timerDb private val timerDao = context.timerDb
fun getTimers(callback: (notes: List<Timer>) -> Unit) { fun getTimers(callback: (timers: List<Timer>) -> Unit) {
ensureBackgroundThread { ensureBackgroundThread {
callback.invoke(timerDao.getTimers()) callback.invoke(timerDao.getTimers())
} }
} }
fun getTimer(timerId: Long, callback: (timer: Timer) -> Unit) {
ensureBackgroundThread {
callback.invoke(timerDao.getTimer(timerId))
}
}
fun insertOrUpdateTimer(timer: Timer, callback: () -> Unit = {}) { fun insertOrUpdateTimer(timer: Timer, callback: () -> Unit = {}) {
ensureBackgroundThread { ensureBackgroundThread {
timerDao.insertOrUpdateTimer(timer) val id = timerDao.insertOrUpdateTimer(timer)
Log.d(TAG, "insertOrUpdateTimer: $id")
callback.invoke() callback.invoke()
} }
} }
@ -48,4 +56,8 @@ class TimerHelper(val context: Context) {
callback.invoke() callback.invoke()
} }
} }
companion object {
private const val TAG = "TimerHelper"
}
} }

View File

@ -12,6 +12,9 @@ interface TimerDao {
@Query("SELECT * FROM timers") @Query("SELECT * FROM timers")
fun getTimers(): List<Timer> fun getTimers(): List<Timer>
@Query("SELECT * FROM timers WHERE id=:id")
fun getTimer(id: Long): Timer
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertOrUpdateTimer(timer: Timer): Long fun insertOrUpdateTimer(timer: Timer): Long

View File

@ -0,0 +1,9 @@
package com.simplemobiletools.clock.models
sealed class TimerEvent(open val timerId: Long) {
data class Reset(override val timerId: Long, val duration: Long) : TimerEvent(timerId)
data class Start(override val timerId: Long, val duration: Long) : TimerEvent(timerId)
data class Pause(override val timerId: Long, val duration: Long) : TimerEvent(timerId)
data class Finish(override val timerId: Long, val duration: Long) : TimerEvent(timerId)
data class Refresh(override val timerId: Long) : TimerEvent(timerId)
}

View File

@ -2,10 +2,7 @@ package com.simplemobiletools.clock.models
sealed class TimerState { sealed class TimerState {
object Idle : TimerState() object Idle : TimerState()
data class Start(val duration: Long) : TimerState()
data class Running(val duration: Long, val tick: Long) : TimerState() data class Running(val duration: Long, val tick: Long) : TimerState()
data class Pause(val duration: Long) : TimerState()
data class Paused(val duration: Long, val tick: Long) : TimerState() data class Paused(val duration: Long, val tick: Long) : TimerState()
data class Finish(val duration: Long) : TimerState()
object Finished : TimerState() object Finished : TimerState()
} }

View File

@ -76,7 +76,7 @@ class TimerService : Service() {
.setContentTitle(label) .setContentTitle(label)
.setContentText(formattedDuration) .setContentText(formattedDuration)
.setSmallIcon(R.drawable.ic_timer) .setSmallIcon(R.drawable.ic_timer)
.setContentIntent(this.getOpenTimerTabIntent()) .setContentIntent(this.getOpenTimerTabIntent(0))
.setPriority(Notification.PRIORITY_DEFAULT) .setPriority(Notification.PRIORITY_DEFAULT)
.setSound(null) .setSound(null)
.setOngoing(true) .setOngoing(true)

View File

@ -24,7 +24,7 @@
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:padding="@dimen/normal_margin" android:padding="@dimen/normal_margin"
android:src="@drawable/ic_reset_vector" android:src="@drawable/ic_reset_vector"
android:visibility="gone" android:visibility="visible"
app:layout_constraintBottom_toBottomOf="@+id/timer_play_pause" app:layout_constraintBottom_toBottomOf="@+id/timer_play_pause"
app:layout_constraintEnd_toStartOf="@+id/timer_play_pause" app:layout_constraintEnd_toStartOf="@+id/timer_play_pause"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"

View File

@ -1,111 +1,103 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout <com.simplemobiletools.commons.views.MyTextView
android:id="@+id/timer_time"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:layout_marginTop="@dimen/normal_margin"
android:background="?attr/selectableItemBackground"
android:gravity="center_horizontal"
android:padding="@dimen/small_margin"
android:textSize="@dimen/stopwatch_text_size"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="00:00" />
<com.simplemobiletools.commons.views.MyTextView <com.simplemobiletools.commons.views.MyTextView
android:id="@+id/timer_time" android:id="@+id/timer_initial_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/activity_margin"
android:background="?attr/selectableItemBackground"
android:drawableStart="@drawable/ic_timer"
android:drawablePadding="@dimen/normal_margin"
android:padding="@dimen/activity_margin"
android:textSize="@dimen/bigger_text_size"
app:layout_constraintTop_toBottomOf="@+id/timer_time"
tools:text="05:00" />
<RelativeLayout
android:id="@+id/timer_vibrate_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:padding="@dimen/activity_margin"
app:layout_constraintTop_toBottomOf="@+id/timer_initial_time">
<com.simplemobiletools.commons.views.MySwitchCompat
android:id="@+id/timer_vibrate"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/normal_margin" android:background="@null"
android:background="?attr/selectableItemBackground" android:clickable="false"
android:gravity="center_horizontal" android:drawableStart="@drawable/ic_vibrate_vector"
android:padding="@dimen/small_margin"
android:textSize="@dimen/stopwatch_text_size"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="00:00" />
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/timer_initial_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/activity_margin"
android:background="?attr/selectableItemBackground"
android:drawableStart="@drawable/ic_timer"
android:drawablePadding="@dimen/normal_margin" android:drawablePadding="@dimen/normal_margin"
android:padding="@dimen/activity_margin" android:text="@string/vibrate"
android:textSize="@dimen/bigger_text_size" android:textSize="@dimen/bigger_text_size" />
app:layout_constraintTop_toBottomOf="@+id/timer_time"
tools:text="05:00" />
<RelativeLayout </RelativeLayout>
android:id="@+id/timer_vibrate_holder"
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/timer_sound"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:drawableStart="@drawable/ic_bell_vector"
android:drawablePadding="@dimen/normal_margin"
android:padding="@dimen/activity_margin"
android:textSize="@dimen/bigger_text_size"
app:layout_constraintTop_toBottomOf="@+id/timer_vibrate_holder"
tools:text="Default alarm" />
<LinearLayout
android:id="@+id/timer_label_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="@dimen/activity_margin"
android:paddingTop="@dimen/medium_margin"
android:paddingEnd="@dimen/activity_margin"
android:paddingBottom="@dimen/medium_margin"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/timer_sound"
app:layout_constraintVertical_bias="0"
app:layout_constraintVertical_chainStyle="spread_inside">
<ImageView
android:id="@+id/timer_label_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_label_vector" />
<com.simplemobiletools.commons.views.MyEditText
android:id="@+id/timer_label"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground" android:layout_marginStart="10dp"
android:padding="@dimen/activity_margin" android:hint="@string/label"
app:layout_constraintTop_toBottomOf="@+id/timer_initial_time"> android:maxLines="1"
android:singleLine="true"
<com.simplemobiletools.commons.views.MySwitchCompat android:textCursorDrawable="@null"
android:id="@+id/timer_vibrate" android:textSize="@dimen/normal_text_size" />
android:layout_width="match_parent" </LinearLayout>
android:layout_height="wrap_content"
android:background="@null"
android:clickable="false"
android:drawableStart="@drawable/ic_vibrate_vector"
android:drawablePadding="@dimen/normal_margin"
android:text="@string/vibrate"
android:textSize="@dimen/bigger_text_size" />
</RelativeLayout>
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/timer_sound"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:drawableStart="@drawable/ic_bell_vector"
android:drawablePadding="@dimen/normal_margin"
android:padding="@dimen/activity_margin"
android:textSize="@dimen/bigger_text_size"
app:layout_constraintTop_toBottomOf="@+id/timer_vibrate_holder"
tools:text="Default alarm" />
<LinearLayout
android:id="@+id/timer_label_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="@dimen/activity_margin"
android:paddingTop="@dimen/medium_margin"
android:paddingEnd="@dimen/activity_margin"
android:paddingBottom="@dimen/medium_margin"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/timer_sound"
app:layout_constraintVertical_bias="0"
app:layout_constraintVertical_chainStyle="spread_inside">
<ImageView
android:id="@+id/timer_label_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_label_vector" />
<com.simplemobiletools.commons.views.MyEditText
android:id="@+id/timer_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:hint="@string/label"
android:maxLines="1"
android:singleLine="true"
android:textCursorDrawable="@null"
android:textSize="@dimen/normal_text_size" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>