From 060b8c6a23058515666d8fea7bd34c43bfdb37ee Mon Sep 17 00:00:00 2001 From: Pavol Franek <> Date: Tue, 10 Mar 2020 15:20:02 +0100 Subject: [PATCH] Reworked logic - show notification only if is timer running iin the background --- app/build.gradle | 1 + .../kotlin/com/simplemobiletools/clock/App.kt | 96 ++++++++++- .../gson/{typeAdapter.kt => TypeAdapter.kt} | 1 + .../clock/fragments/TimerFragment.kt | 60 +++++-- .../clock/receivers/HideTimerReceiver.kt | 3 + .../clock/services/TimerService.kt | 114 +++++++++++++ .../clock/services/timerService.kt | 158 ------------------ 7 files changed, 257 insertions(+), 176 deletions(-) rename app/src/main/kotlin/com/simplemobiletools/clock/extensions/gson/{typeAdapter.kt => TypeAdapter.kt} (93%) create mode 100644 app/src/main/kotlin/com/simplemobiletools/clock/services/TimerService.kt delete mode 100644 app/src/main/kotlin/com/simplemobiletools/clock/services/timerService.kt diff --git a/app/build.gradle b/app/build.gradle index c33a24d..e37f813 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -76,4 +76,5 @@ dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0' implementation 'org.greenrobot:eventbus:3.2.0' + implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' } diff --git a/app/src/main/kotlin/com/simplemobiletools/clock/App.kt b/app/src/main/kotlin/com/simplemobiletools/clock/App.kt index 42965fe..268b0a1 100644 --- a/app/src/main/kotlin/com/simplemobiletools/clock/App.kt +++ b/app/src/main/kotlin/com/simplemobiletools/clock/App.kt @@ -1,16 +1,110 @@ package com.simplemobiletools.clock import android.app.Application +import android.app.NotificationManager +import android.content.Context +import android.os.Build +import android.os.CountDownTimer +import androidx.annotation.RequiresApi +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.OnLifecycleEvent +import androidx.lifecycle.ProcessLifecycleOwner import com.facebook.stetho.Stetho +import com.simplemobiletools.clock.extensions.config +import com.simplemobiletools.clock.extensions.getOpenTimerTabIntent +import com.simplemobiletools.clock.extensions.getTimerNotification +import com.simplemobiletools.clock.helpers.TIMER_NOTIF_ID +import com.simplemobiletools.clock.services.TimerState +import com.simplemobiletools.clock.services.TimerStopService +import com.simplemobiletools.clock.services.startTimerService import com.simplemobiletools.commons.extensions.checkUseEnglish +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode + +class App : Application(), LifecycleObserver { + + private var timer: CountDownTimer? = null + private var lastTick = 0L -class App : Application() { override fun onCreate() { super.onCreate() + ProcessLifecycleOwner.get().lifecycle.addObserver(this) + EventBus.getDefault().register(this) + if (BuildConfig.DEBUG) { Stetho.initializeWithDefaults(this) } checkUseEnglish() } + + override fun onTerminate() { + EventBus.getDefault().unregister(this) + super.onTerminate() + } + + @RequiresApi(Build.VERSION_CODES.O) + @OnLifecycleEvent(Lifecycle.Event.ON_STOP) + private fun onAppBackgrounded() { + if (config.timerState is TimerState.Running) { + startTimerService(this) + } + } + + @OnLifecycleEvent(Lifecycle.Event.ON_START) + private fun onAppForegrounded() { + EventBus.getDefault().post(TimerStopService) + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onMessageEvent(state: TimerState.Idle) { + config.timerState = state + timer?.cancel() + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onMessageEvent(state: TimerState.Start) { + timer = object : CountDownTimer(state.duration, 1000) { + override fun onTick(tick: Long) { + lastTick = tick + + val newState = TimerState.Running(state.duration, tick) + EventBus.getDefault().post(newState) + config.timerState = newState + } + + override fun onFinish() { + EventBus.getDefault().post(TimerState.Finish(state.duration)) + EventBus.getDefault().post(TimerStopService) + } + }.start() + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onMessageEvent(event: TimerState.Finish) { + val pendingIntent = getOpenTimerTabIntent() + val notification = getTimerNotification(pendingIntent, false) //MAYBE IN FUTURE ADD TIME TO NOTIFICATION + val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.notify(TIMER_NOTIF_ID, notification) + + EventBus.getDefault().post(TimerState.Finished) + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onMessageEvent(state: TimerState.Finished) { + config.timerState = state + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onMessageEvent(event: TimerState.Pause) { + EventBus.getDefault().post(TimerState.Paused(event.duration, lastTick)) + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onMessageEvent(state: TimerState.Paused) { + config.timerState = state + timer?.cancel() + } } diff --git a/app/src/main/kotlin/com/simplemobiletools/clock/extensions/gson/typeAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/clock/extensions/gson/TypeAdapter.kt similarity index 93% rename from app/src/main/kotlin/com/simplemobiletools/clock/extensions/gson/typeAdapter.kt rename to app/src/main/kotlin/com/simplemobiletools/clock/extensions/gson/TypeAdapter.kt index 550d57c..20df992 100644 --- a/app/src/main/kotlin/com/simplemobiletools/clock/extensions/gson/typeAdapter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/clock/extensions/gson/TypeAdapter.kt @@ -12,6 +12,7 @@ val timerStates = valueOf() .registerSubtype(TimerState.Pause::class.java) .registerSubtype(TimerState.Paused::class.java) .registerSubtype(TimerState.Finish::class.java) + .registerSubtype(TimerState.Finished::class.java) inline fun valueOf(): RuntimeTypeAdapterFactory = RuntimeTypeAdapterFactory.of(T::class.java) diff --git a/app/src/main/kotlin/com/simplemobiletools/clock/fragments/TimerFragment.kt b/app/src/main/kotlin/com/simplemobiletools/clock/fragments/TimerFragment.kt index 65095ac..e656184 100644 --- a/app/src/main/kotlin/com/simplemobiletools/clock/fragments/TimerFragment.kt +++ b/app/src/main/kotlin/com/simplemobiletools/clock/fragments/TimerFragment.kt @@ -13,7 +13,6 @@ import com.simplemobiletools.clock.dialogs.MyTimePickerDialogDialog import com.simplemobiletools.clock.extensions.* import com.simplemobiletools.clock.helpers.PICK_AUDIO_FILE_INTENT_ID import com.simplemobiletools.clock.services.TimerState -import com.simplemobiletools.clock.services.startTimerService import com.simplemobiletools.commons.dialogs.SelectAlarmSoundDialog import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.helpers.ALARM_SOUND_TYPE_ALARM @@ -22,6 +21,7 @@ import kotlinx.android.synthetic.main.fragment_timer.view.* import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode +import kotlin.math.roundToInt class TimerFragment : Fragment() { @@ -62,7 +62,27 @@ class TimerFragment : Fragment() { } timer_play_pause.setOnClickListener { - context.startTimerService() + val state = config.timerState + + when (state) { + is TimerState.Idle -> { + EventBus.getDefault().post(TimerState.Start(config.timerSeconds.secondsToMillis)) + } + + is TimerState.Paused -> { + EventBus.getDefault().post(TimerState.Start(state.tick)) + } + + is TimerState.Running -> { + EventBus.getDefault().post(TimerState.Pause(state.tick)) + } + + is TimerState.Finished -> { + EventBus.getDefault().post(TimerState.Start(config.timerSeconds.secondsToMillis)) + } + + else -> {} + } } timer_reset.setOnClickListener { @@ -118,7 +138,7 @@ class TimerFragment : Fragment() { @Subscribe(threadMode = ThreadMode.MAIN) fun onMessageEvent(state: TimerState.Running) { - view.timer_time.text = state.tick.div(1000).toInt().getFormattedDuration() + view.timer_time.text = state.tick.div(1000F).roundToInt().getFormattedDuration() updateViewStates(state) } @@ -127,22 +147,28 @@ class TimerFragment : Fragment() { updateViewStates(state) } - private fun updateViewStates(timerState: TimerState) { - view.timer_reset.beVisibleIf(timerState is TimerState.Running || timerState is TimerState.Paused) + @Subscribe(threadMode = ThreadMode.MAIN) + fun onMessageEvent(state: TimerState.Finished) { + view.timer_time.text = 0.getFormattedDuration() + updateViewStates(state) + } - val drawableId = - if (timerState is TimerState.Running) { - R.drawable.ic_pause_vector - } else { - R.drawable.ic_play_vector - } + private fun updateViewStates(state: TimerState) { - val iconColor = - if (requiredActivity.getAdjustedPrimaryColor() == Color.WHITE) { - Color.BLACK - } else { - requiredActivity.config.textColor - } + val resetPossible = state is TimerState.Running || state is TimerState.Paused || state is TimerState.Finished + view.timer_reset.beVisibleIf(resetPossible) + + val drawableId = if (state is TimerState.Running) { + R.drawable.ic_pause_vector + } else { + R.drawable.ic_play_vector + } + + val iconColor = if (requiredActivity.getAdjustedPrimaryColor() == Color.WHITE) { + Color.BLACK + } else { + requiredActivity.config.textColor + } view.timer_play_pause.setImageDrawable(resources.getColoredDrawableWithColor(drawableId, iconColor)) } diff --git a/app/src/main/kotlin/com/simplemobiletools/clock/receivers/HideTimerReceiver.kt b/app/src/main/kotlin/com/simplemobiletools/clock/receivers/HideTimerReceiver.kt index e12a9a2..5d3a739 100644 --- a/app/src/main/kotlin/com/simplemobiletools/clock/receivers/HideTimerReceiver.kt +++ b/app/src/main/kotlin/com/simplemobiletools/clock/receivers/HideTimerReceiver.kt @@ -4,9 +4,12 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import com.simplemobiletools.clock.extensions.hideTimerNotification +import com.simplemobiletools.clock.services.TimerState +import org.greenrobot.eventbus.EventBus class HideTimerReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { context.hideTimerNotification() + EventBus.getDefault().post(TimerState.Idle) } } diff --git a/app/src/main/kotlin/com/simplemobiletools/clock/services/TimerService.kt b/app/src/main/kotlin/com/simplemobiletools/clock/services/TimerService.kt new file mode 100644 index 0000000..04cefac --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/clock/services/TimerService.kt @@ -0,0 +1,114 @@ +package com.simplemobiletools.clock.services + +import android.annotation.TargetApi +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.IBinder +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationCompat +import com.simplemobiletools.clock.R +import com.simplemobiletools.clock.extensions.config +import com.simplemobiletools.clock.extensions.getOpenTimerTabIntent +import com.simplemobiletools.clock.helpers.TIMER_RUNNING_NOTIF_ID +import com.simplemobiletools.commons.extensions.getFormattedDuration +import com.simplemobiletools.commons.helpers.isOreoPlus +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode + +@RequiresApi(Build.VERSION_CODES.O) +fun startTimerService(context: Context) { + if (isOreoPlus()) { + context.startForegroundService(Intent(context, TimerService::class.java)) + } else { + context.startService(Intent(context, TimerService::class.java)) + } +} + +class TimerService : Service() { + + private val bus = EventBus.getDefault() + + override fun onCreate() { + super.onCreate() + bus.register(this) + } + + override fun onBind(intent: Intent?): IBinder? = null + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + super.onStartCommand(intent, flags, startId) + + val formattedDuration = config.timerSeconds.getFormattedDuration() + startForeground(TIMER_RUNNING_NOTIF_ID, notification(formattedDuration)) + + return START_NOT_STICKY + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onMessageEvent(event: TimerStopService) { + stopService() + } + + private fun stopService() { + if (isOreoPlus()) { + stopForeground(true) + } else { + stopSelf() + } + } + + override fun onDestroy() { + super.onDestroy() + bus.unregister(this) + } + + @TargetApi(Build.VERSION_CODES.O) + private fun notification(formattedDuration: String): Notification { + val channelId = "simple_alarm_timer" + val label = getString(R.string.timer) + val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + if (isOreoPlus()) { + val importance = NotificationManager.IMPORTANCE_DEFAULT + NotificationChannel(channelId, label, importance).apply { + setSound(null, null) + notificationManager.createNotificationChannel(this) + } + } + + val builder = NotificationCompat.Builder(this) + .setContentTitle(label) + .setContentText(formattedDuration) + .setSmallIcon(R.drawable.ic_timer) + .setContentIntent(this.getOpenTimerTabIntent()) + .setPriority(Notification.PRIORITY_DEFAULT) + .setSound(null) + .setOngoing(true) + .setAutoCancel(true) + .setChannelId(channelId) + + builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + return builder.build() + } +} + +data class StateWrapper(val state: TimerState) + +object TimerStopService + +sealed class TimerState { + object Idle : TimerState() + data class Start(val duration: 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 Finish(val duration: Long) : TimerState() + object Finished : TimerState() +} + + diff --git a/app/src/main/kotlin/com/simplemobiletools/clock/services/timerService.kt b/app/src/main/kotlin/com/simplemobiletools/clock/services/timerService.kt deleted file mode 100644 index c84d2f3..0000000 --- a/app/src/main/kotlin/com/simplemobiletools/clock/services/timerService.kt +++ /dev/null @@ -1,158 +0,0 @@ -package com.simplemobiletools.clock.services - -import android.annotation.TargetApi -import android.app.Notification -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.Service -import android.content.Context -import android.content.Intent -import android.os.Build -import android.os.CountDownTimer -import android.os.IBinder -import androidx.core.app.NotificationCompat -import com.simplemobiletools.clock.R -import com.simplemobiletools.clock.extensions.config -import com.simplemobiletools.clock.extensions.getOpenTimerTabIntent -import com.simplemobiletools.clock.extensions.getTimerNotification -import com.simplemobiletools.clock.extensions.secondsToMillis -import com.simplemobiletools.clock.helpers.TIMER_NOTIF_ID -import com.simplemobiletools.clock.helpers.TIMER_RUNNING_NOTIF_ID -import com.simplemobiletools.commons.extensions.getFormattedDuration -import com.simplemobiletools.commons.helpers.isOreoPlus -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode - -fun Context.startTimerService() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - startForegroundService(Intent(this, TimerService::class.java)) - } else { - startService(Intent(this, TimerService::class.java)) - } -} - -class TimerService : Service() { - - private var timer: CountDownTimer? = null - private var lastTick = 0L - private val bus = EventBus.getDefault() - - override fun onCreate() { - super.onCreate() - bus.register(this) - } - - override fun onBind(intent: Intent?): IBinder? = null - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - super.onStartCommand(intent, flags, startId) - - val formattedDuration = config.timerSeconds.getFormattedDuration() - startForeground(TIMER_RUNNING_NOTIF_ID, notification(formattedDuration)) - - when (val state = config.timerState) { - is TimerState.Idle -> bus.post(TimerState.Start(config.timerSeconds.secondsToMillis)) - is TimerState.Paused -> bus.post(TimerState.Start(state.tick)) - is TimerState.Running -> bus.post(TimerState.Pause(state.tick)) - else -> {} - } - - return START_NOT_STICKY - } - - @Subscribe(threadMode = ThreadMode.MAIN) - fun onMessageEvent(state: TimerState.Idle) { - config.timerState = state - timer?.cancel() - stopService() - } - - @Subscribe(threadMode = ThreadMode.MAIN) - fun onMessageEvent(state: TimerState.Start) { - timer = object : CountDownTimer(state.duration, 1000) { - override fun onTick(tick: Long) { - lastTick = tick - - val newState = TimerState.Running(state.duration, tick) - bus.post(newState) - config.timerState = newState - } - - override fun onFinish() { - bus.post(TimerState.Finish(state.duration)) - } - }.start() - } - - @Subscribe(threadMode = ThreadMode.MAIN) - fun onMessageEvent(event: TimerState.Finish) { - val pendingIntent = getOpenTimerTabIntent() - val notification = getTimerNotification(pendingIntent, false) //MAYBE IN FUTURE ADD TIME TO NOTIFICATION - val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - notificationManager.notify(TIMER_NOTIF_ID, notification) - - bus.post(TimerState.Idle) - } - - @Subscribe(threadMode = ThreadMode.MAIN) - fun onMessageEvent(event: TimerState.Pause) { - bus.post(TimerState.Paused(event.duration, lastTick)) - } - - @Subscribe(threadMode = ThreadMode.MAIN) - fun onMessageEvent(state: TimerState.Paused) { - config.timerState = state - timer?.cancel() - stopService() - } - - private fun stopService() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) stopForeground(true) - else stopSelf() - } - - override fun onDestroy() { - super.onDestroy() - bus.unregister(this) - } - - @TargetApi(Build.VERSION_CODES.O) - private fun notification(formattedDuration: String): Notification { - val channelId = "simple_alarm_timer" - val label = getString(R.string.timer) - val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - if (isOreoPlus()) { - val importance = NotificationManager.IMPORTANCE_HIGH - NotificationChannel(channelId, label, importance).apply { - setSound(null, null) - notificationManager.createNotificationChannel(this) - } - } - - val builder = NotificationCompat.Builder(this) - .setContentTitle(label) - .setContentText(formattedDuration) - .setSmallIcon(R.drawable.ic_timer) - .setContentIntent(this.getOpenTimerTabIntent()) - .setPriority(Notification.PRIORITY_HIGH) - .setSound(null) - .setOngoing(true) - .setAutoCancel(true) - .setChannelId(channelId) - - builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - return builder.build() - } -} - -data class StateWrapper(val state: TimerState) - -sealed class TimerState { - object Idle: TimerState() - data class Start(val duration: 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 Finish(val duration: Long): TimerState() -}