Reworked logic - show notification only if is timer running iin the background

This commit is contained in:
Pavol Franek 2020-03-10 15:20:02 +01:00
parent f6d9c5aece
commit 060b8c6a23
7 changed files with 257 additions and 176 deletions

View File

@ -76,4 +76,5 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
implementation 'org.greenrobot:eventbus:3.2.0' implementation 'org.greenrobot:eventbus:3.2.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
} }

View File

@ -1,16 +1,110 @@
package com.simplemobiletools.clock package com.simplemobiletools.clock
import android.app.Application 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.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 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() { override fun onCreate() {
super.onCreate() super.onCreate()
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
EventBus.getDefault().register(this)
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Stetho.initializeWithDefaults(this) Stetho.initializeWithDefaults(this)
} }
checkUseEnglish() 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()
}
} }

View File

@ -12,6 +12,7 @@ val timerStates = valueOf<TimerState>()
.registerSubtype(TimerState.Pause::class.java) .registerSubtype(TimerState.Pause::class.java)
.registerSubtype(TimerState.Paused::class.java) .registerSubtype(TimerState.Paused::class.java)
.registerSubtype(TimerState.Finish::class.java) .registerSubtype(TimerState.Finish::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

@ -13,7 +13,6 @@ import com.simplemobiletools.clock.dialogs.MyTimePickerDialogDialog
import com.simplemobiletools.clock.extensions.* import com.simplemobiletools.clock.extensions.*
import com.simplemobiletools.clock.helpers.PICK_AUDIO_FILE_INTENT_ID import com.simplemobiletools.clock.helpers.PICK_AUDIO_FILE_INTENT_ID
import com.simplemobiletools.clock.services.TimerState import com.simplemobiletools.clock.services.TimerState
import com.simplemobiletools.clock.services.startTimerService
import com.simplemobiletools.commons.dialogs.SelectAlarmSoundDialog import com.simplemobiletools.commons.dialogs.SelectAlarmSoundDialog
import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.ALARM_SOUND_TYPE_ALARM 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.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
import kotlin.math.roundToInt
class TimerFragment : Fragment() { class TimerFragment : Fragment() {
@ -62,7 +62,27 @@ class TimerFragment : Fragment() {
} }
timer_play_pause.setOnClickListener { 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 { timer_reset.setOnClickListener {
@ -118,7 +138,7 @@ class TimerFragment : Fragment() {
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
fun onMessageEvent(state: TimerState.Running) { 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) updateViewStates(state)
} }
@ -127,18 +147,24 @@ class TimerFragment : Fragment() {
updateViewStates(state) updateViewStates(state)
} }
private fun updateViewStates(timerState: TimerState) { @Subscribe(threadMode = ThreadMode.MAIN)
view.timer_reset.beVisibleIf(timerState is TimerState.Running || timerState is TimerState.Paused) fun onMessageEvent(state: TimerState.Finished) {
view.timer_time.text = 0.getFormattedDuration()
updateViewStates(state)
}
val drawableId = private fun updateViewStates(state: TimerState) {
if (timerState is TimerState.Running) {
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 R.drawable.ic_pause_vector
} else { } else {
R.drawable.ic_play_vector R.drawable.ic_play_vector
} }
val iconColor = val iconColor = if (requiredActivity.getAdjustedPrimaryColor() == Color.WHITE) {
if (requiredActivity.getAdjustedPrimaryColor() == Color.WHITE) {
Color.BLACK Color.BLACK
} else { } else {
requiredActivity.config.textColor requiredActivity.config.textColor

View File

@ -4,9 +4,12 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import com.simplemobiletools.clock.extensions.hideTimerNotification import com.simplemobiletools.clock.extensions.hideTimerNotification
import com.simplemobiletools.clock.services.TimerState
import org.greenrobot.eventbus.EventBus
class HideTimerReceiver : BroadcastReceiver() { class HideTimerReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
context.hideTimerNotification() context.hideTimerNotification()
EventBus.getDefault().post(TimerState.Idle)
} }
} }

View File

@ -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()
}

View File

@ -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()
}