From 046f0323d112cc3749f03fc47fabfef20b1fc512 Mon Sep 17 00:00:00 2001 From: Pavol Franek <> Date: Wed, 4 Mar 2020 08:42:19 +0100 Subject: [PATCH 1/6] wip --- app/build.gradle | 14 + .../clock/extensions/Context.kt | 4 + .../clock/extensions/Fragment.kt | 10 + .../clock/fragments/TimerFragment.kt | 281 +++++++++--------- .../clock/workers/timerWorker.kt | 41 +++ 5 files changed, 202 insertions(+), 148 deletions(-) create mode 100644 app/src/main/kotlin/com/simplemobiletools/clock/extensions/Fragment.kt create mode 100644 app/src/main/kotlin/com/simplemobiletools/clock/workers/timerWorker.kt diff --git a/app/build.gradle b/app/build.gradle index a16d3c7..dc51efa 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -54,6 +54,16 @@ android { checkReleaseBuilds false abortOnError false } + + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } + + kotlinOptions { + jvmTarget = "1.8" + freeCompilerArgs = ["-XXLanguage:+NewInference"] + } } dependencies { @@ -61,4 +71,8 @@ dependencies { implementation 'com.facebook.stetho:stetho:1.5.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta2' implementation 'com.shawnlin:number-picker:2.4.6' + implementation "androidx.preference:preference:1.1.0" + implementation "androidx.work:work-runtime-ktx:2.3.2" + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0' } diff --git a/app/src/main/kotlin/com/simplemobiletools/clock/extensions/Context.kt b/app/src/main/kotlin/com/simplemobiletools/clock/extensions/Context.kt index b202301..2277ef3 100644 --- a/app/src/main/kotlin/com/simplemobiletools/clock/extensions/Context.kt +++ b/app/src/main/kotlin/com/simplemobiletools/clock/extensions/Context.kt @@ -6,6 +6,7 @@ import android.appwidget.AppWidgetManager import android.content.ComponentName import android.content.Context import android.content.Intent +import android.content.SharedPreferences import android.media.AudioAttributes import android.media.AudioManager import android.net.Uri @@ -15,6 +16,7 @@ import android.text.style.RelativeSizeSpan import android.widget.Toast import androidx.core.app.AlarmManagerCompat import androidx.core.app.NotificationCompat +import androidx.preference.PreferenceManager import com.simplemobiletools.clock.R import com.simplemobiletools.clock.activities.ReminderActivity import com.simplemobiletools.clock.activities.SnoozeReminderActivity @@ -382,3 +384,5 @@ fun Context.checkAlarmsWithDeletedSoundUri(uri: String) { dbHelper.updateAlarm(it) } } + +val Context.preferences: SharedPreferences get() = PreferenceManager.getDefaultSharedPreferences(this) diff --git a/app/src/main/kotlin/com/simplemobiletools/clock/extensions/Fragment.kt b/app/src/main/kotlin/com/simplemobiletools/clock/extensions/Fragment.kt new file mode 100644 index 0000000..6fe261a --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/clock/extensions/Fragment.kt @@ -0,0 +1,10 @@ +package com.simplemobiletools.clock.extensions + +import android.content.SharedPreferences +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.preference.PreferenceManager + +val Fragment.requiredActivity: FragmentActivity get() = this.activity!! + +val Fragment.preferences: SharedPreferences get() = PreferenceManager.getDefaultSharedPreferences(requiredActivity) 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 2709b90..1096b03 100644 --- a/app/src/main/kotlin/com/simplemobiletools/clock/fragments/TimerFragment.kt +++ b/app/src/main/kotlin/com/simplemobiletools/clock/fragments/TimerFragment.kt @@ -4,6 +4,7 @@ import android.annotation.TargetApi import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager +import android.app.NotificationManager.IMPORTANCE_HIGH import android.content.Context import android.content.Intent import android.graphics.Color @@ -12,11 +13,16 @@ import android.os.Build import android.os.Bundle import android.os.Handler import android.os.SystemClock +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.app.NotificationCompat import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.lifecycleScope +import androidx.work.WorkInfo +import androidx.work.WorkManager import com.simplemobiletools.clock.R import com.simplemobiletools.clock.activities.ReminderActivity import com.simplemobiletools.clock.activities.SimpleActivity @@ -24,53 +30,65 @@ 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.helpers.TIMER_NOTIF_ID +import com.simplemobiletools.clock.workers.cancelTimerWorker +import com.simplemobiletools.clock.workers.enqueueTimerWorker +import com.simplemobiletools.clock.workers.timerRequestId import com.simplemobiletools.commons.dialogs.SelectAlarmSoundDialog import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.helpers.ALARM_SOUND_TYPE_ALARM import com.simplemobiletools.commons.helpers.isOreoPlus import com.simplemobiletools.commons.models.AlarmSound +import kotlinx.android.synthetic.main.fragment_timer.* import kotlinx.android.synthetic.main.fragment_timer.view.* +import kotlinx.android.synthetic.main.fragment_timer.view.timer_time +import kotlinx.coroutines.InternalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import java.util.concurrent.TimeUnit class TimerFragment : Fragment() { + private val UPDATE_INTERVAL = 1000L private val WAS_RUNNING = "was_running" private val CURRENT_TICKS = "current_ticks" private val TOTAL_TICKS = "total_ticks" private var isRunning = false - private var uptimeAtStart = 0L private var initialSecs = 0 private var totalTicks = 0 private var currentTicks = 0 - private var updateHandler = Handler() - private var isForegrounded = true lateinit var view: ViewGroup + @InternalCoroutinesApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - val config = context!!.config + val config = requiredActivity.config view = (inflater.inflate(R.layout.fragment_timer, container, false) as ViewGroup).apply { timer_time.setOnClickListener { - togglePlayPause() + val selectedDuration = config.timerSeconds + enqueueTimerWorker(TimeUnit.SECONDS.toMillis(selectedDuration.toLong())) + showNotification(selectedDuration.getFormattedDuration()) } timer_play_pause.setOnClickListener { - togglePlayPause() + val selectedDuration = config.timerSeconds + enqueueTimerWorker(TimeUnit.SECONDS.toMillis(selectedDuration.toLong())) + showNotification(selectedDuration.getFormattedDuration()) } timer_reset.setOnClickListener { - context!!.hideTimerNotification() - resetTimer() + cancelTimerWorker() } timer_initial_time.setOnClickListener { - MyTimePickerDialogDialog(activity as SimpleActivity, config.timerSeconds) { - val seconds = if (it <= 0) 10 else it - config.timerSeconds = seconds - timer_initial_time.text = seconds.getFormattedDuration() - if (!isRunning) { - resetTimer() - } + MyTimePickerDialogDialog(activity as SimpleActivity, config.timerSeconds) { seconds -> + val timerSeconds = if (seconds <= 0) 10 else seconds + config.timerSeconds = timerSeconds + timer_initial_time.text = timerSeconds.getFormattedDuration() } } @@ -81,28 +99,47 @@ class TimerFragment : Fragment() { timer_sound.setOnClickListener { SelectAlarmSoundDialog(activity as SimpleActivity, config.timerSoundUri, AudioManager.STREAM_ALARM, PICK_AUDIO_FILE_INTENT_ID, - ALARM_SOUND_TYPE_ALARM, true, onAlarmPicked = { - if (it != null) { - updateAlarmSound(it) - } - }, onAlarmSoundDeleted = { - if (config.timerSoundUri == it.uri) { - val defaultAlarm = context.getDefaultAlarmSound(ALARM_SOUND_TYPE_ALARM) - updateAlarmSound(defaultAlarm) - } - context.checkAlarmsWithDeletedSoundUri(it.uri) - }) + ALARM_SOUND_TYPE_ALARM, true, + onAlarmPicked = { sound -> + if (sound != null) { + updateAlarmSound(sound) + } + }, + onAlarmSoundDeleted = { sound -> + if (config.timerSoundUri == sound.uri) { + val defaultAlarm = context.getDefaultAlarmSound(ALARM_SOUND_TYPE_ALARM) + updateAlarmSound(defaultAlarm) + } + + context.checkAlarmsWithDeletedSoundUri(sound.uri) + }) } } initialSecs = config.timerSeconds - updateDisplayedText() - return view - } - override fun onStart() { - super.onStart() - isForegrounded = true + WorkManager.getInstance(activity!!).getWorkInfoByIdLiveData(timerRequestId) + .observe(this, Observer { workInfo -> + Log.e("test", workInfo.toString()) + + when (workInfo.state) { + WorkInfo.State.SUCCEEDED -> { + showNotification("-") + } + WorkInfo.State.FAILED -> {} + WorkInfo.State.CANCELLED -> {} + else -> {} + } + }) + + val a = lifecycleScope.launch { + (config.timerSeconds downTo 0).asFlow().onEach { delay(1000) }.collect { + Log.e("test", it.toString()) + timer_time.text = it.getFormattedDuration() + } + } + + return view } override fun onResume() { @@ -110,28 +147,12 @@ class TimerFragment : Fragment() { setupViews() } - override fun onStop() { - super.onStop() - isForegrounded = false - context!!.hideNotification(TIMER_NOTIF_ID) - } - - override fun onDestroy() { - super.onDestroy() - if (isRunning && activity?.isChangingConfigurations == false) { - context?.toast(R.string.timer_stopped) - } - isRunning = false - updateHandler.removeCallbacks(updateRunnable) - } - override fun onSaveInstanceState(outState: Bundle) { - outState.apply { + super.onSaveInstanceState(outState.apply { putBoolean(WAS_RUNNING, isRunning) putInt(TOTAL_TICKS, totalTicks) putInt(CURRENT_TICKS, currentTicks) - } - super.onSaveInstanceState(outState) + }) } override fun onViewStateRestored(savedInstanceState: Bundle?) { @@ -140,25 +161,21 @@ class TimerFragment : Fragment() { isRunning = getBoolean(WAS_RUNNING, false) totalTicks = getInt(TOTAL_TICKS, 0) currentTicks = getInt(CURRENT_TICKS, 0) - - if (isRunning) { - uptimeAtStart = SystemClock.uptimeMillis() - currentTicks * UPDATE_INTERVAL - updateTimerState(false) - } } } fun updateAlarmSound(alarmSound: AlarmSound) { - context!!.config.timerSoundTitle = alarmSound.title - context!!.config.timerSoundUri = alarmSound.uri + requiredActivity.config.timerSoundTitle = alarmSound.title + requiredActivity.config.timerSoundUri = alarmSound.uri view.timer_sound.text = alarmSound.title } private fun setupViews() { - val config = context!!.config + val config = requiredActivity.config val textColor = config.textColor + view.apply { - context!!.updateTextColors(timer_fragment) + requiredActivity.updateTextColors(timer_fragment) timer_play_pause.background = resources.getColoredDrawableWithColor(R.drawable.circle_background_filled, context!!.getAdjustedPrimaryColor()) timer_reset.applyColorFilter(textColor) @@ -173,84 +190,19 @@ class TimerFragment : Fragment() { } updateIcons() - updateDisplayedText() - } - - private fun togglePlayPause() { - isRunning = !isRunning - updateTimerState(true) - } - - private fun updateTimerState(setUptimeAtStart: Boolean) { - updateIcons() - context!!.hideTimerNotification() - - if (isRunning) { - updateHandler.post(updateRunnable) - view.timer_reset.beVisible() - if (setUptimeAtStart) { - uptimeAtStart = SystemClock.uptimeMillis() - } - } else { - updateHandler.removeCallbacksAndMessages(null) - currentTicks = 0 - totalTicks-- - } } private fun updateIcons() { val drawableId = if (isRunning) R.drawable.ic_pause_vector else R.drawable.ic_play_vector - val iconColor = if (context!!.getAdjustedPrimaryColor() == Color.WHITE) Color.BLACK else context!!.config.textColor + val iconColor = if (requiredActivity.getAdjustedPrimaryColor() == Color.WHITE) Color.BLACK else requiredActivity.config.textColor view.timer_play_pause.setImageDrawable(resources.getColoredDrawableWithColor(drawableId, iconColor)) } - private fun resetTimer() { - updateHandler.removeCallbacks(updateRunnable) - isRunning = false - currentTicks = 0 - totalTicks = 0 - initialSecs = context!!.config.timerSeconds - updateDisplayedText() - updateIcons() - view.timer_reset.beGone() - } - - private fun updateDisplayedText(): Boolean { - val diff = initialSecs - totalTicks - var formattedDuration = Math.abs(diff).getFormattedDuration() - - if (diff < 0) { - formattedDuration = "-$formattedDuration" - if (!isForegrounded) { - resetTimer() - return false - } - } - - view.timer_time.text = formattedDuration - if (diff == 0) { - if (context?.isScreenOn() == true) { - context!!.showTimerNotification(false) - Handler().postDelayed({ - context?.hideTimerNotification() - }, context?.config!!.timerMaxReminderSecs * 1000L) - } else { - Intent(context, ReminderActivity::class.java).apply { - activity?.startActivity(this) - } - } - } else if (diff > 0 && !isForegrounded && isRunning) { - showNotification(formattedDuration) - } - - return true - } - @TargetApi(Build.VERSION_CODES.O) private fun showNotification(formattedDuration: String) { val channelId = "simple_alarm_timer" val label = getString(R.string.timer) - val notificationManager = context!!.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val notificationManager = requiredActivity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager if (isOreoPlus()) { val importance = NotificationManager.IMPORTANCE_HIGH NotificationChannel(channelId, label, importance).apply { @@ -260,29 +212,62 @@ class TimerFragment : Fragment() { } val builder = NotificationCompat.Builder(context) - .setContentTitle(label) - .setContentText(formattedDuration) - .setSmallIcon(R.drawable.ic_timer) - .setContentIntent(context!!.getOpenTimerTabIntent()) - .setPriority(Notification.PRIORITY_HIGH) - .setSound(null) - .setOngoing(true) - .setAutoCancel(true) - .setChannelId(channelId) + .setContentTitle(label) + .setContentText(formattedDuration) + .setSmallIcon(R.drawable.ic_timer) + .setContentIntent(context!!.getOpenTimerTabIntent()) + .setPriority(Notification.PRIORITY_HIGH) + .setSound(null) + .setOngoing(true) + .setAutoCancel(true) + .setChannelId(channelId) - builder.setVisibility(Notification.VISIBILITY_PUBLIC) + builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) notificationManager.notify(TIMER_NOTIF_ID, builder.build()) } - - private val updateRunnable = object : Runnable { - override fun run() { - if (isRunning) { - if (updateDisplayedText()) { - currentTicks++ - totalTicks++ - updateHandler.postAtTime(this, uptimeAtStart + currentTicks * UPDATE_INTERVAL) - } - } - } - } } + +// private fun resetTimer() { +// updateHandler.removeCallbacks(updateRunnable) +// isRunning = false +// currentTicks = 0 +// totalTicks = 0 +// initialSecs = context!!.config.timerSeconds +// updateDisplayedText() +// updateIcons() +// view.timer_reset.beGone() +// requiredActivity.hideTimerNotification() +// context!!.hideNotification(TIMER_NOTIF_ID) +// context?.toast(R.string.timer_stopped) +// } + +// private fun updateDisplayedText(): Boolean { +// val diff = initialSecs - totalTicks +// var formattedDuration = Math.abs(diff).getFormattedDuration() +// +// if (diff < 0) { +// formattedDuration = "-$formattedDuration" +// if (!isForegrounded) { +// resetTimer() +// return false +// } +// } +// +// view.timer_time.text = formattedDuration +// if (diff == 0) { +// if (context?.isScreenOn() == true) { +// context!!.showTimerNotification(false) +// Handler().postDelayed({ +// context?.hideTimerNotification() +// }, context?.config!!.timerMaxReminderSecs * 1000L) +// } else { +// Intent(context, ReminderActivity::class.java).apply { +// activity?.startActivity(this) +// } +// } +// } else if (diff > 0 && !isForegrounded && isRunning) { +// showNotification(formattedDuration) +// } +// +// return true +// } diff --git a/app/src/main/kotlin/com/simplemobiletools/clock/workers/timerWorker.kt b/app/src/main/kotlin/com/simplemobiletools/clock/workers/timerWorker.kt new file mode 100644 index 0000000..07bf2b7 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/clock/workers/timerWorker.kt @@ -0,0 +1,41 @@ +package com.simplemobiletools.clock.workers + +import android.content.Context +import androidx.fragment.app.Fragment +import androidx.work.* +import com.simplemobiletools.clock.extensions.preferences +import com.simplemobiletools.clock.extensions.requiredActivity +import java.util.* +import java.util.concurrent.TimeUnit + +private const val TIMER_REQUEST_ID = "TIMER_REQUEST_ID" +private const val TIMER_WORKER_KEY = "TIMER_WORKER_KEY" + +private fun Fragment.saveTimerRequestId(uuid: UUID) = + preferences.edit().putString(TIMER_REQUEST_ID, uuid.toString()).apply() + +val Fragment.timerRequestId: UUID get() = + UUID.fromString(preferences.getString(TIMER_REQUEST_ID, UUID.randomUUID().toString())) + +fun Fragment.cancelTimerWorker() = + WorkManager.getInstance(requiredActivity).apply { + timerRequestId.let(::cancelWorkById) + } + +fun Fragment.enqueueTimerWorker(delay: Long) = + WorkManager.getInstance(requiredActivity).enqueueUniqueWork(TIMER_WORKER_KEY, ExistingWorkPolicy.REPLACE, timerRequest(delay)) + +private fun Fragment.timerRequest(delay: Long) = + OneTimeWorkRequestBuilder().setInitialDelay(delay, TimeUnit.MILLISECONDS).build().also { + saveTimerRequestId(it.id) + } + +class TimerWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) { + override fun doWork(): Result = + try { + Result.success() + } catch (exception: Exception) { + Result.failure() + } +} + From c3ab1f9330ff8827248340af1eaa4afe9cc54aea Mon Sep 17 00:00:00 2001 From: Pavol Franek <> Date: Wed, 4 Mar 2020 16:27:48 +0100 Subject: [PATCH 2/6] wip 2 --- .../clock/extensions/Logs.kt | 7 + .../clock/extensions/Long.kt | 9 + .../clock/fragments/TimerFragment.kt | 194 ++++++------------ .../simplemobiletools/clock/helpers/Config.kt | 5 + .../clock/helpers/Constants.kt | 1 + .../clock/workers/timerWorker.kt | 23 ++- 6 files changed, 99 insertions(+), 140 deletions(-) create mode 100644 app/src/main/kotlin/com/simplemobiletools/clock/extensions/Logs.kt diff --git a/app/src/main/kotlin/com/simplemobiletools/clock/extensions/Logs.kt b/app/src/main/kotlin/com/simplemobiletools/clock/extensions/Logs.kt new file mode 100644 index 0000000..4a66268 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/clock/extensions/Logs.kt @@ -0,0 +1,7 @@ +package com.simplemobiletools.clock.extensions + +import android.util.Log +import com.simplemobiletools.clock.BuildConfig + +fun A.log(tag: String) = apply { if (BuildConfig.DEBUG) Log.wtf(tag, this.toString()) } +fun A.log(first: String, tag: String) = apply { if (BuildConfig.DEBUG) Log.wtf(tag, first) } diff --git a/app/src/main/kotlin/com/simplemobiletools/clock/extensions/Long.kt b/app/src/main/kotlin/com/simplemobiletools/clock/extensions/Long.kt index 2b29f68..e4eac62 100644 --- a/app/src/main/kotlin/com/simplemobiletools/clock/extensions/Long.kt +++ b/app/src/main/kotlin/com/simplemobiletools/clock/extensions/Long.kt @@ -1,5 +1,7 @@ package com.simplemobiletools.clock.extensions +import android.text.format.DateFormat +import java.util.* import java.util.concurrent.TimeUnit fun Long.formatStopwatchTime(useLongerMSFormat: Boolean): String { @@ -27,3 +29,10 @@ fun Long.formatStopwatchTime(useLongerMSFormat: Boolean): String { } } } + +fun Long.timestampFormat(format: String = "dd. MM. yyyy"): String { + val calendar = Calendar.getInstance(Locale.getDefault()) + calendar.timeInMillis = this + + return DateFormat.format(format, calendar).toString() +} 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 1096b03..9447f4f 100644 --- a/app/src/main/kotlin/com/simplemobiletools/clock/fragments/TimerFragment.kt +++ b/app/src/main/kotlin/com/simplemobiletools/clock/fragments/TimerFragment.kt @@ -4,15 +4,12 @@ import android.annotation.TargetApi import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager -import android.app.NotificationManager.IMPORTANCE_HIGH import android.content.Context -import android.content.Intent import android.graphics.Color import android.media.AudioManager import android.os.Build import android.os.Bundle -import android.os.Handler -import android.os.SystemClock +import android.os.CountDownTimer import android.util.Log import android.view.LayoutInflater import android.view.View @@ -20,16 +17,16 @@ import android.view.ViewGroup import androidx.core.app.NotificationCompat import androidx.fragment.app.Fragment import androidx.lifecycle.Observer -import androidx.lifecycle.lifecycleScope import androidx.work.WorkInfo import androidx.work.WorkManager import com.simplemobiletools.clock.R -import com.simplemobiletools.clock.activities.ReminderActivity import com.simplemobiletools.clock.activities.SimpleActivity import com.simplemobiletools.clock.dialogs.MyTimePickerDialogDialog import com.simplemobiletools.clock.extensions.* +import com.simplemobiletools.clock.helpers.Config import com.simplemobiletools.clock.helpers.PICK_AUDIO_FILE_INTENT_ID import com.simplemobiletools.clock.helpers.TIMER_NOTIF_ID +import com.simplemobiletools.clock.workers.TIMER_WORKER_KEY import com.simplemobiletools.clock.workers.cancelTimerWorker import com.simplemobiletools.clock.workers.enqueueTimerWorker import com.simplemobiletools.clock.workers.timerRequestId @@ -38,56 +35,35 @@ import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.helpers.ALARM_SOUND_TYPE_ALARM import com.simplemobiletools.commons.helpers.isOreoPlus import com.simplemobiletools.commons.models.AlarmSound -import kotlinx.android.synthetic.main.fragment_timer.* import kotlinx.android.synthetic.main.fragment_timer.view.* -import kotlinx.android.synthetic.main.fragment_timer.view.timer_time -import kotlinx.coroutines.InternalCoroutinesApi -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.asFlow -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch import java.util.concurrent.TimeUnit class TimerFragment : Fragment() { - private val UPDATE_INTERVAL = 1000L - private val WAS_RUNNING = "was_running" - private val CURRENT_TICKS = "current_ticks" - private val TOTAL_TICKS = "total_ticks" - - private var isRunning = false - private var initialSecs = 0 - private var totalTicks = 0 - private var currentTicks = 0 - lateinit var view: ViewGroup + private var timer: CountDownTimer? = null - @InternalCoroutinesApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { val config = requiredActivity.config view = (inflater.inflate(R.layout.fragment_timer, container, false) as ViewGroup).apply { timer_time.setOnClickListener { - val selectedDuration = config.timerSeconds - enqueueTimerWorker(TimeUnit.SECONDS.toMillis(selectedDuration.toLong())) - showNotification(selectedDuration.getFormattedDuration()) + startTimer(config) } timer_play_pause.setOnClickListener { - val selectedDuration = config.timerSeconds - enqueueTimerWorker(TimeUnit.SECONDS.toMillis(selectedDuration.toLong())) - showNotification(selectedDuration.getFormattedDuration()) + startTimer(config) } timer_reset.setOnClickListener { cancelTimerWorker() + requiredActivity.toast(R.string.timer_stopped) } timer_initial_time.setOnClickListener { MyTimePickerDialogDialog(activity as SimpleActivity, config.timerSeconds) { seconds -> val timerSeconds = if (seconds <= 0) 10 else seconds config.timerSeconds = timerSeconds + config.timerTimeStamp = System.currentTimeMillis() + timerSeconds timer_initial_time.text = timerSeconds.getFormattedDuration() } } @@ -99,69 +75,70 @@ class TimerFragment : Fragment() { timer_sound.setOnClickListener { SelectAlarmSoundDialog(activity as SimpleActivity, config.timerSoundUri, AudioManager.STREAM_ALARM, PICK_AUDIO_FILE_INTENT_ID, - ALARM_SOUND_TYPE_ALARM, true, - onAlarmPicked = { sound -> - if (sound != null) { - updateAlarmSound(sound) - } - }, - onAlarmSoundDeleted = { sound -> - if (config.timerSoundUri == sound.uri) { - val defaultAlarm = context.getDefaultAlarmSound(ALARM_SOUND_TYPE_ALARM) - updateAlarmSound(defaultAlarm) - } + ALARM_SOUND_TYPE_ALARM, true, + onAlarmPicked = { sound -> + if (sound != null) { + updateAlarmSound(sound) + } + }, + onAlarmSoundDeleted = { sound -> + if (config.timerSoundUri == sound.uri) { + val defaultAlarm = context.getDefaultAlarmSound(ALARM_SOUND_TYPE_ALARM) + updateAlarmSound(defaultAlarm) + } - context.checkAlarmsWithDeletedSoundUri(sound.uri) - }) + context.checkAlarmsWithDeletedSoundUri(sound.uri) + }) } - } - initialSecs = config.timerSeconds + WorkManager.getInstance(requiredActivity).getWorkInfosByTagLiveData(TIMER_WORKER_KEY).observe(requiredActivity, Observer { workInfo -> + workInfo.log("log") - WorkManager.getInstance(activity!!).getWorkInfoByIdLiveData(timerRequestId) - .observe(this, Observer { workInfo -> - Log.e("test", workInfo.toString()) + val workerState = workInfo?.firstOrNull()?.state + when (workerState) { + WorkInfo.State.ENQUEUED -> { + timer?.cancel() + timer = object : CountDownTimer(config.timerSeconds.toLong().times(1000), 1000) { + override fun onTick(millisUntilFinished: Long) { + timer_time.text = millisUntilFinished.div(1000).toInt().getFormattedDuration() + } - when (workInfo.state) { - WorkInfo.State.SUCCEEDED -> { - showNotification("-") + override fun onFinish() {} + }.start() } - WorkInfo.State.FAILED -> {} - WorkInfo.State.CANCELLED -> {} - else -> {} - } - }) - val a = lifecycleScope.launch { - (config.timerSeconds downTo 0).asFlow().onEach { delay(1000) }.collect { - Log.e("test", it.toString()) - timer_time.text = it.getFormattedDuration() - } + else -> { + timer_time.text = 0.getFormattedDuration() + } + } + + updateIcons(workerState == WorkInfo.State.ENQUEUED) + timer_reset.beVisibleIf(workerState == WorkInfo.State.ENQUEUED) + }) } return view } + private fun startTimer(config: Config) { + val selectedDuration = config.timerSeconds + val formattedTimestamp = config.timerTimeStamp.timestampFormat("HH:mm:ss") + enqueueTimerWorker(TimeUnit.SECONDS.toMillis(selectedDuration.toLong())) + showNotification("(${selectedDuration.getFormattedDuration()}) $formattedTimestamp") + } + override fun onResume() { super.onResume() setupViews() } override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState.apply { - putBoolean(WAS_RUNNING, isRunning) - putInt(TOTAL_TICKS, totalTicks) - putInt(CURRENT_TICKS, currentTicks) - }) + super.onSaveInstanceState(outState.apply {}) } override fun onViewStateRestored(savedInstanceState: Bundle?) { super.onViewStateRestored(savedInstanceState) - savedInstanceState?.apply { - isRunning = getBoolean(WAS_RUNNING, false) - totalTicks = getInt(TOTAL_TICKS, 0) - currentTicks = getInt(CURRENT_TICKS, 0) - } + savedInstanceState?.apply {} } fun updateAlarmSound(alarmSound: AlarmSound) { @@ -170,6 +147,10 @@ class TimerFragment : Fragment() { view.timer_sound.text = alarmSound.title } +// private val isRunning +// get(): Boolean = +// WorkManager.getInstance(requiredActivity).getWorkInfosByTagLiveData(TIMER_WORKER_KEY).value?.firstOrNull()?.state == WorkInfo.State.ENQUEUED + private fun setupViews() { val config = requiredActivity.config val textColor = config.textColor @@ -188,11 +169,9 @@ class TimerFragment : Fragment() { timer_sound.text = config.timerSoundTitle timer_sound.colorLeftDrawable(textColor) } - - updateIcons() } - private fun updateIcons() { + private fun updateIcons(isRunning: Boolean) { val drawableId = if (isRunning) 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)) @@ -212,62 +191,17 @@ class TimerFragment : Fragment() { } val builder = NotificationCompat.Builder(context) - .setContentTitle(label) - .setContentText(formattedDuration) - .setSmallIcon(R.drawable.ic_timer) - .setContentIntent(context!!.getOpenTimerTabIntent()) - .setPriority(Notification.PRIORITY_HIGH) - .setSound(null) - .setOngoing(true) - .setAutoCancel(true) - .setChannelId(channelId) + .setContentTitle(label) + .setContentText(formattedDuration) + .setSmallIcon(R.drawable.ic_timer) + .setContentIntent(context!!.getOpenTimerTabIntent()) + .setPriority(Notification.PRIORITY_HIGH) + .setSound(null) + .setOngoing(true) + .setAutoCancel(true) + .setChannelId(channelId) builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) notificationManager.notify(TIMER_NOTIF_ID, builder.build()) } } - -// private fun resetTimer() { -// updateHandler.removeCallbacks(updateRunnable) -// isRunning = false -// currentTicks = 0 -// totalTicks = 0 -// initialSecs = context!!.config.timerSeconds -// updateDisplayedText() -// updateIcons() -// view.timer_reset.beGone() -// requiredActivity.hideTimerNotification() -// context!!.hideNotification(TIMER_NOTIF_ID) -// context?.toast(R.string.timer_stopped) -// } - -// private fun updateDisplayedText(): Boolean { -// val diff = initialSecs - totalTicks -// var formattedDuration = Math.abs(diff).getFormattedDuration() -// -// if (diff < 0) { -// formattedDuration = "-$formattedDuration" -// if (!isForegrounded) { -// resetTimer() -// return false -// } -// } -// -// view.timer_time.text = formattedDuration -// if (diff == 0) { -// if (context?.isScreenOn() == true) { -// context!!.showTimerNotification(false) -// Handler().postDelayed({ -// context?.hideTimerNotification() -// }, context?.config!!.timerMaxReminderSecs * 1000L) -// } else { -// Intent(context, ReminderActivity::class.java).apply { -// activity?.startActivity(this) -// } -// } -// } else if (diff > 0 && !isForegrounded && isRunning) { -// showNotification(formattedDuration) -// } -// -// return true -// } diff --git a/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Config.kt b/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Config.kt index e0ebdcd..06f9ab4 100644 --- a/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Config.kt +++ b/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Config.kt @@ -5,6 +5,7 @@ import com.simplemobiletools.commons.extensions.getDefaultAlarmTitle import com.simplemobiletools.commons.extensions.getDefaultAlarmUri import com.simplemobiletools.commons.helpers.ALARM_SOUND_TYPE_ALARM import com.simplemobiletools.commons.helpers.BaseConfig +import java.util.concurrent.TimeUnit class Config(context: Context) : BaseConfig(context) { companion object { @@ -27,6 +28,10 @@ class Config(context: Context) : BaseConfig(context) { get() = prefs.getInt(TIMER_SECONDS, 300) set(lastTimerSeconds) = prefs.edit().putInt(TIMER_SECONDS, lastTimerSeconds).apply() + var timerTimeStamp: Long + get() = prefs.getLong(TIMER_TIMESTAMP, System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(timerSeconds.toLong())) + set(timestamp) = prefs.edit().putLong(TIMER_TIMESTAMP, timestamp).apply() + var timerVibrate: Boolean get() = prefs.getBoolean(TIMER_VIBRATE, false) set(timerVibrate) = prefs.edit().putBoolean(TIMER_VIBRATE, timerVibrate).apply() diff --git a/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Constants.kt b/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Constants.kt index 4f6ce6b..613c422 100644 --- a/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Constants.kt +++ b/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Constants.kt @@ -8,6 +8,7 @@ const val SHOW_SECONDS = "show_seconds" const val SELECTED_TIME_ZONES = "selected_time_zones" const val EDITED_TIME_ZONE_TITLES = "edited_time_zone_titles" const val TIMER_SECONDS = "timer_seconds" +const val TIMER_TIMESTAMP = "timer_timetamp" const val TIMER_VIBRATE = "timer_vibrate" const val TIMER_SOUND_URI = "timer_sound_uri" const val TIMER_SOUND_TITLE = "timer_sound_title" diff --git a/app/src/main/kotlin/com/simplemobiletools/clock/workers/timerWorker.kt b/app/src/main/kotlin/com/simplemobiletools/clock/workers/timerWorker.kt index 07bf2b7..c3bd62f 100644 --- a/app/src/main/kotlin/com/simplemobiletools/clock/workers/timerWorker.kt +++ b/app/src/main/kotlin/com/simplemobiletools/clock/workers/timerWorker.kt @@ -3,39 +3,42 @@ package com.simplemobiletools.clock.workers import android.content.Context import androidx.fragment.app.Fragment import androidx.work.* -import com.simplemobiletools.clock.extensions.preferences -import com.simplemobiletools.clock.extensions.requiredActivity +import com.simplemobiletools.clock.extensions.* import java.util.* import java.util.concurrent.TimeUnit private const val TIMER_REQUEST_ID = "TIMER_REQUEST_ID" -private const val TIMER_WORKER_KEY = "TIMER_WORKER_KEY" +const val TIMER_WORKER_KEY = "TIMER_WORKER_KEY" private fun Fragment.saveTimerRequestId(uuid: UUID) = preferences.edit().putString(TIMER_REQUEST_ID, uuid.toString()).apply() -val Fragment.timerRequestId: UUID get() = - UUID.fromString(preferences.getString(TIMER_REQUEST_ID, UUID.randomUUID().toString())) +val Fragment.timerRequestId: UUID? get() = + preferences.getString(TIMER_REQUEST_ID, UUID.randomUUID().toString())?.let { UUID.fromString(it) } fun Fragment.cancelTimerWorker() = - WorkManager.getInstance(requiredActivity).apply { - timerRequestId.let(::cancelWorkById) - } + WorkManager.getInstance(requiredActivity).cancelAllWorkByTag(TIMER_WORKER_KEY) fun Fragment.enqueueTimerWorker(delay: Long) = WorkManager.getInstance(requiredActivity).enqueueUniqueWork(TIMER_WORKER_KEY, ExistingWorkPolicy.REPLACE, timerRequest(delay)) private fun Fragment.timerRequest(delay: Long) = - OneTimeWorkRequestBuilder().setInitialDelay(delay, TimeUnit.MILLISECONDS).build().also { + OneTimeWorkRequestBuilder().setInitialDelay(delay, TimeUnit.MILLISECONDS).addTag(TIMER_WORKER_KEY).build().also { saveTimerRequestId(it.id) } -class TimerWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) { +class TimerWorker(val context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) { override fun doWork(): Result = try { + context.showTimerNotification(false) Result.success() } catch (exception: Exception) { Result.failure() } + + override fun onStopped() { + super.onStopped() + context.hideTimerNotification() + } } From ed18715b39e9ff4f3f0cebf5bdf72a428ee34c97 Mon Sep 17 00:00:00 2001 From: Pavol Franek <> Date: Sun, 8 Mar 2020 10:47:42 +0100 Subject: [PATCH 3/6] wip3 --- .../clock/fragments/TimerFragment.kt | 127 ++++++++++-------- .../simplemobiletools/clock/helpers/Config.kt | 11 +- .../clock/helpers/Constants.kt | 3 +- .../clock/workers/timerWorker.kt | 37 +++-- 4 files changed, 98 insertions(+), 80 deletions(-) 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 9447f4f..b8fe52c 100644 --- a/app/src/main/kotlin/com/simplemobiletools/clock/fragments/TimerFragment.kt +++ b/app/src/main/kotlin/com/simplemobiletools/clock/fragments/TimerFragment.kt @@ -10,7 +10,6 @@ import android.media.AudioManager import android.os.Build import android.os.Bundle import android.os.CountDownTimer -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -26,10 +25,7 @@ import com.simplemobiletools.clock.extensions.* import com.simplemobiletools.clock.helpers.Config import com.simplemobiletools.clock.helpers.PICK_AUDIO_FILE_INTENT_ID import com.simplemobiletools.clock.helpers.TIMER_NOTIF_ID -import com.simplemobiletools.clock.workers.TIMER_WORKER_KEY -import com.simplemobiletools.clock.workers.cancelTimerWorker -import com.simplemobiletools.clock.workers.enqueueTimerWorker -import com.simplemobiletools.clock.workers.timerRequestId +import com.simplemobiletools.clock.workers.* import com.simplemobiletools.commons.dialogs.SelectAlarmSoundDialog import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.helpers.ALARM_SOUND_TYPE_ALARM @@ -38,24 +34,53 @@ import com.simplemobiletools.commons.models.AlarmSound import kotlinx.android.synthetic.main.fragment_timer.view.* import java.util.concurrent.TimeUnit + class TimerFragment : Fragment() { lateinit var view: ViewGroup private var timer: CountDownTimer? = null + private var isRunning = false override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - val config = requiredActivity.config view = (inflater.inflate(R.layout.fragment_timer, container, false) as ViewGroup).apply { + val config = requiredActivity.config + val textColor = config.textColor + + requiredActivity.updateTextColors(timer_fragment) + timer_play_pause.background = resources.getColoredDrawableWithColor(R.drawable.circle_background_filled, context!!.getAdjustedPrimaryColor()) + timer_reset.applyColorFilter(textColor) + + timer_initial_time.text = config.timerSeconds.getFormattedDuration() + timer_initial_time.colorLeftDrawable(textColor) + + timer_vibrate.isChecked = config.timerVibrate + timer_vibrate.colorLeftDrawable(textColor) + + timer_sound.text = config.timerSoundTitle + timer_sound.colorLeftDrawable(textColor) + timer_time.setOnClickListener { - startTimer(config) + if (isRunning) { + pauseTimer(config) + } else { + startTimer(config) + } } timer_play_pause.setOnClickListener { - startTimer(config) + if (isRunning) { + pauseTimer(config) + } else { + startTimer(config) + } } timer_reset.setOnClickListener { cancelTimerWorker() + requiredActivity.hideTimerNotification() + + config.timerTickStamp = 0L + config.timerStartStamp = 0L requiredActivity.toast(R.string.timer_stopped) } @@ -63,7 +88,6 @@ class TimerFragment : Fragment() { MyTimePickerDialogDialog(activity as SimpleActivity, config.timerSeconds) { seconds -> val timerSeconds = if (seconds <= 0) 10 else seconds config.timerSeconds = timerSeconds - config.timerTimeStamp = System.currentTimeMillis() + timerSeconds timer_initial_time.text = timerSeconds.getFormattedDuration() } } @@ -92,15 +116,20 @@ class TimerFragment : Fragment() { } WorkManager.getInstance(requiredActivity).getWorkInfosByTagLiveData(TIMER_WORKER_KEY).observe(requiredActivity, Observer { workInfo -> - workInfo.log("log") - val workerState = workInfo?.firstOrNull()?.state + isRunning = (workerState == WorkInfo.State.ENQUEUED) + + updateIcons(isRunning) + timer_reset.beVisibleIf(isRunning) + timer?.cancel() + when (workerState) { WorkInfo.State.ENQUEUED -> { - timer?.cancel() - timer = object : CountDownTimer(config.timerSeconds.toLong().times(1000), 1000) { + val duration = config.timerSeconds.toLong() * 1000 //MS + + timer = object : CountDownTimer(duration, 1000) { override fun onTick(millisUntilFinished: Long) { - timer_time.text = millisUntilFinished.div(1000).toInt().getFormattedDuration() + timer_time.text = TimeUnit.MILLISECONDS.toSeconds(millisUntilFinished).toInt().getFormattedDuration() } override fun onFinish() {} @@ -111,34 +140,46 @@ class TimerFragment : Fragment() { timer_time.text = 0.getFormattedDuration() } } - - updateIcons(workerState == WorkInfo.State.ENQUEUED) - timer_reset.beVisibleIf(workerState == WorkInfo.State.ENQUEUED) }) + + cancelTimerWorker() } return view } private fun startTimer(config: Config) { - val selectedDuration = config.timerSeconds - val formattedTimestamp = config.timerTimeStamp.timestampFormat("HH:mm:ss") - enqueueTimerWorker(TimeUnit.SECONDS.toMillis(selectedDuration.toLong())) - showNotification("(${selectedDuration.getFormattedDuration()}) $formattedTimestamp") + val isTimerNoTick = config.timerTickStamp == 0L + + if (isTimerNoTick) { + config.timerStartStamp = System.currentTimeMillis() + + val selectedDuration = config.timerSeconds + val formattedTimestamp = config.timerStartStamp.timestampFormat("HH:mm:ss") + + enqueueTimerWorker(TimeUnit.SECONDS.toMillis(selectedDuration.toLong())) + showNotification("(${selectedDuration.getFormattedDuration()}) $formattedTimestamp") + } else { + val duration = config.timerSeconds.toLong() * 1000 //MS + val selectedDuration = (config.timerStartStamp + duration) - (config.timerTickStamp - config.timerStartStamp) + val formattedTimestamp = config.timerStartStamp.timestampFormat("HH:mm:ss") + + enqueueTimerWorker(TimeUnit.SECONDS.toMillis(selectedDuration.toLong())) + showNotification("(${selectedDuration.toInt().getFormattedDuration()}) $formattedTimestamp") + } } - override fun onResume() { - super.onResume() - setupViews() - } + private fun pauseTimer(config: Config) { + cancelTimerWorker() + requiredActivity.hideTimerNotification() - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState.apply {}) - } + config.timerTickStamp = System.currentTimeMillis() - override fun onViewStateRestored(savedInstanceState: Bundle?) { - super.onViewStateRestored(savedInstanceState) - savedInstanceState?.apply {} + val tick = config.timerTickStamp + val duration = config.timerSeconds.toLong() * 1000 //MS + val startedAt = config.timerStartStamp + val distance = duration - (tick - startedAt) + view.timer_time.text = distance.toInt().getFormattedDuration() } fun updateAlarmSound(alarmSound: AlarmSound) { @@ -147,30 +188,6 @@ class TimerFragment : Fragment() { view.timer_sound.text = alarmSound.title } -// private val isRunning -// get(): Boolean = -// WorkManager.getInstance(requiredActivity).getWorkInfosByTagLiveData(TIMER_WORKER_KEY).value?.firstOrNull()?.state == WorkInfo.State.ENQUEUED - - private fun setupViews() { - val config = requiredActivity.config - val textColor = config.textColor - - view.apply { - requiredActivity.updateTextColors(timer_fragment) - timer_play_pause.background = resources.getColoredDrawableWithColor(R.drawable.circle_background_filled, context!!.getAdjustedPrimaryColor()) - timer_reset.applyColorFilter(textColor) - - timer_initial_time.text = config.timerSeconds.getFormattedDuration() - timer_initial_time.colorLeftDrawable(textColor) - - timer_vibrate.isChecked = config.timerVibrate - timer_vibrate.colorLeftDrawable(textColor) - - timer_sound.text = config.timerSoundTitle - timer_sound.colorLeftDrawable(textColor) - } - } - private fun updateIcons(isRunning: Boolean) { val drawableId = if (isRunning) R.drawable.ic_pause_vector else R.drawable.ic_play_vector val iconColor = if (requiredActivity.getAdjustedPrimaryColor() == Color.WHITE) Color.BLACK else requiredActivity.config.textColor diff --git a/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Config.kt b/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Config.kt index 06f9ab4..9f2f8ab 100644 --- a/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Config.kt +++ b/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Config.kt @@ -5,7 +5,6 @@ import com.simplemobiletools.commons.extensions.getDefaultAlarmTitle import com.simplemobiletools.commons.extensions.getDefaultAlarmUri import com.simplemobiletools.commons.helpers.ALARM_SOUND_TYPE_ALARM import com.simplemobiletools.commons.helpers.BaseConfig -import java.util.concurrent.TimeUnit class Config(context: Context) : BaseConfig(context) { companion object { @@ -28,9 +27,13 @@ class Config(context: Context) : BaseConfig(context) { get() = prefs.getInt(TIMER_SECONDS, 300) set(lastTimerSeconds) = prefs.edit().putInt(TIMER_SECONDS, lastTimerSeconds).apply() - var timerTimeStamp: Long - get() = prefs.getLong(TIMER_TIMESTAMP, System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(timerSeconds.toLong())) - set(timestamp) = prefs.edit().putLong(TIMER_TIMESTAMP, timestamp).apply() + var timerStartStamp: Long + get() = prefs.getLong(TIMER_START_TIMESTAMP, 0L) + set(timestamp) = prefs.edit().putLong(TIMER_START_TIMESTAMP, timestamp).apply() + + var timerTickStamp: Long + get() = prefs.getLong(TIMER_TICK_TIMESTAMP, 0L) + set(timestamp) = prefs.edit().putLong(TIMER_TICK_TIMESTAMP, timestamp).apply() var timerVibrate: Boolean get() = prefs.getBoolean(TIMER_VIBRATE, false) diff --git a/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Constants.kt b/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Constants.kt index 613c422..f383d54 100644 --- a/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Constants.kt +++ b/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Constants.kt @@ -8,7 +8,8 @@ const val SHOW_SECONDS = "show_seconds" const val SELECTED_TIME_ZONES = "selected_time_zones" const val EDITED_TIME_ZONE_TITLES = "edited_time_zone_titles" const val TIMER_SECONDS = "timer_seconds" -const val TIMER_TIMESTAMP = "timer_timetamp" +const val TIMER_START_TIMESTAMP = "timer_timetamp" +const val TIMER_TICK_TIMESTAMP = "timer_tick" const val TIMER_VIBRATE = "timer_vibrate" const val TIMER_SOUND_URI = "timer_sound_uri" const val TIMER_SOUND_TITLE = "timer_sound_title" diff --git a/app/src/main/kotlin/com/simplemobiletools/clock/workers/timerWorker.kt b/app/src/main/kotlin/com/simplemobiletools/clock/workers/timerWorker.kt index c3bd62f..1bb655c 100644 --- a/app/src/main/kotlin/com/simplemobiletools/clock/workers/timerWorker.kt +++ b/app/src/main/kotlin/com/simplemobiletools/clock/workers/timerWorker.kt @@ -11,34 +11,31 @@ private const val TIMER_REQUEST_ID = "TIMER_REQUEST_ID" const val TIMER_WORKER_KEY = "TIMER_WORKER_KEY" private fun Fragment.saveTimerRequestId(uuid: UUID) = - preferences.edit().putString(TIMER_REQUEST_ID, uuid.toString()).apply() + preferences.edit().putString(TIMER_REQUEST_ID, uuid.toString()).apply() -val Fragment.timerRequestId: UUID? get() = - preferences.getString(TIMER_REQUEST_ID, UUID.randomUUID().toString())?.let { UUID.fromString(it) } +val Fragment.timerRequestId: UUID? + get() = + preferences.getString(TIMER_REQUEST_ID, UUID.randomUUID().toString())?.let { UUID.fromString(it) } fun Fragment.cancelTimerWorker() = - WorkManager.getInstance(requiredActivity).cancelAllWorkByTag(TIMER_WORKER_KEY) + WorkManager.getInstance(requiredActivity).cancelAllWorkByTag(TIMER_WORKER_KEY) fun Fragment.enqueueTimerWorker(delay: Long) = - WorkManager.getInstance(requiredActivity).enqueueUniqueWork(TIMER_WORKER_KEY, ExistingWorkPolicy.REPLACE, timerRequest(delay)) + WorkManager.getInstance(requiredActivity).enqueueUniqueWork(TIMER_WORKER_KEY, ExistingWorkPolicy.REPLACE, timerRequest(delay)) -private fun Fragment.timerRequest(delay: Long) = - OneTimeWorkRequestBuilder().setInitialDelay(delay, TimeUnit.MILLISECONDS).addTag(TIMER_WORKER_KEY).build().also { +private fun Fragment.timerRequest(delay: Long): OneTimeWorkRequest = + OneTimeWorkRequestBuilder().setInitialDelay(delay, TimeUnit.MILLISECONDS).addTag(TIMER_WORKER_KEY).build().also { saveTimerRequestId(it.id) - } +} class TimerWorker(val context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) { override fun doWork(): Result = - try { - context.showTimerNotification(false) - Result.success() - } catch (exception: Exception) { - Result.failure() - } - - override fun onStopped() { - super.onStopped() - context.hideTimerNotification() - } + try { + context.showTimerNotification(false) + context.config.timerTickStamp = 0L + context.config.timerStartStamp = 0L + Result.success() + } catch (exception: Exception) { + Result.failure() + } } - From 44ac30ada410e346837c8100e848f9c4783a9afa Mon Sep 17 00:00:00 2001 From: Pavol Franek <> Date: Sun, 8 Mar 2020 21:42:23 +0100 Subject: [PATCH 4/6] Almost done --- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 1 + .../clock/extensions/Context.kt | 7 - .../simplemobiletools/clock/extensions/Int.kt | 6 + .../clock/extensions/Long.kt | 3 + .../gson/RuntimeTypeAdapterFactory.java | 267 ++++++++++++++++++ .../clock/extensions/gson/typeAdapter.kt | 22 ++ .../clock/fragments/TimerFragment.kt | 186 ++++-------- .../simplemobiletools/clock/helpers/Config.kt | 17 +- .../clock/helpers/Constants.kt | 3 +- .../clock/services/timerService.kt | 156 ++++++++++ .../clock/workers/timerWorker.kt | 41 --- 12 files changed, 530 insertions(+), 180 deletions(-) create mode 100644 app/src/main/kotlin/com/simplemobiletools/clock/extensions/Int.kt create mode 100644 app/src/main/kotlin/com/simplemobiletools/clock/extensions/gson/RuntimeTypeAdapterFactory.java create mode 100644 app/src/main/kotlin/com/simplemobiletools/clock/extensions/gson/typeAdapter.kt create mode 100644 app/src/main/kotlin/com/simplemobiletools/clock/services/timerService.kt delete mode 100644 app/src/main/kotlin/com/simplemobiletools/clock/workers/timerWorker.kt diff --git a/app/build.gradle b/app/build.gradle index dc51efa..c33a24d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -75,4 +75,5 @@ dependencies { implementation "androidx.work:work-runtime-ktx:2.3.2" 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' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2f44e93..00fb347 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -79,6 +79,7 @@ + diff --git a/app/src/main/kotlin/com/simplemobiletools/clock/extensions/Context.kt b/app/src/main/kotlin/com/simplemobiletools/clock/extensions/Context.kt index 2277ef3..009b2e1 100644 --- a/app/src/main/kotlin/com/simplemobiletools/clock/extensions/Context.kt +++ b/app/src/main/kotlin/com/simplemobiletools/clock/extensions/Context.kt @@ -226,13 +226,6 @@ fun Context.showAlarmNotification(alarm: Alarm) { scheduleNextAlarm(alarm, false) } -fun Context.showTimerNotification(addDeleteIntent: Boolean) { - val pendingIntent = getOpenTimerTabIntent() - val notification = getTimerNotification(pendingIntent, addDeleteIntent) - val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - notificationManager.notify(TIMER_NOTIF_ID, notification) -} - @SuppressLint("NewApi") fun Context.getTimerNotification(pendingIntent: PendingIntent, addDeleteIntent: Boolean): Notification { var soundUri = config.timerSoundUri diff --git a/app/src/main/kotlin/com/simplemobiletools/clock/extensions/Int.kt b/app/src/main/kotlin/com/simplemobiletools/clock/extensions/Int.kt new file mode 100644 index 0000000..56c7f19 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/clock/extensions/Int.kt @@ -0,0 +1,6 @@ +package com.simplemobiletools.clock.extensions + +import java.util.concurrent.TimeUnit + +val Int.secondsToMillis get() = TimeUnit.SECONDS.toMillis(this.toLong()) +val Int.millisToSeconds get() = TimeUnit.MILLISECONDS.toSeconds(this.toLong()) diff --git a/app/src/main/kotlin/com/simplemobiletools/clock/extensions/Long.kt b/app/src/main/kotlin/com/simplemobiletools/clock/extensions/Long.kt index e4eac62..7500e53 100644 --- a/app/src/main/kotlin/com/simplemobiletools/clock/extensions/Long.kt +++ b/app/src/main/kotlin/com/simplemobiletools/clock/extensions/Long.kt @@ -36,3 +36,6 @@ fun Long.timestampFormat(format: String = "dd. MM. yyyy"): String { return DateFormat.format(format, calendar).toString() } + +val Long.secondsToMillis get() = TimeUnit.SECONDS.toMillis(this) +val Long.millisToSeconds get() = TimeUnit.MILLISECONDS.toSeconds(this) diff --git a/app/src/main/kotlin/com/simplemobiletools/clock/extensions/gson/RuntimeTypeAdapterFactory.java b/app/src/main/kotlin/com/simplemobiletools/clock/extensions/gson/RuntimeTypeAdapterFactory.java new file mode 100644 index 0000000..b04dba4 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/clock/extensions/gson/RuntimeTypeAdapterFactory.java @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.simplemobiletools.clock.extensions.gson; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.internal.Streams; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Adapts values whose runtime type may differ from their declaration type. This + * is necessary when a field's type is not the same type that GSON should create + * when deserializing that field. For example, consider these types: + *
   {@code
+ *   abstract class Shape {
+ *     int x;
+ *     int y;
+ *   }
+ *   class Circle extends Shape {
+ *     int radius;
+ *   }
+ *   class Rectangle extends Shape {
+ *     int width;
+ *     int height;
+ *   }
+ *   class Diamond extends Shape {
+ *     int width;
+ *     int height;
+ *   }
+ *   class Drawing {
+ *     Shape bottomShape;
+ *     Shape topShape;
+ *   }
+ * }
+ *

Without additional type information, the serialized JSON is ambiguous. Is + * the bottom shape in this drawing a rectangle or a diamond?

   {@code
+ *   {
+ *     "bottomShape": {
+ *       "width": 10,
+ *       "height": 5,
+ *       "x": 0,
+ *       "y": 0
+ *     },
+ *     "topShape": {
+ *       "radius": 2,
+ *       "x": 4,
+ *       "y": 1
+ *     }
+ *   }}
+ * This class addresses this problem by adding type information to the + * serialized JSON and honoring that type information when the JSON is + * deserialized:
   {@code
+ *   {
+ *     "bottomShape": {
+ *       "type": "Diamond",
+ *       "width": 10,
+ *       "height": 5,
+ *       "x": 0,
+ *       "y": 0
+ *     },
+ *     "topShape": {
+ *       "type": "Circle",
+ *       "radius": 2,
+ *       "x": 4,
+ *       "y": 1
+ *     }
+ *   }}
+ * Both the type field name ({@code "type"}) and the type labels ({@code + * "Rectangle"}) are configurable. + * + *

Registering Types

+ * Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field + * name to the {@link #of} factory method. If you don't supply an explicit type + * field name, {@code "type"} will be used.
   {@code
+ *   RuntimeTypeAdapterFactory shapeAdapterFactory
+ *       = RuntimeTypeAdapterFactory.of(Shape.class, "type");
+ * }
+ * Next register all of your subtypes. Every subtype must be explicitly + * registered. This protects your application from injection attacks. If you + * don't supply an explicit type label, the type's simple name will be used. + *
   {@code
+ *   shapeAdapterFactory.registerSubtype(Rectangle.class, "Rectangle");
+ *   shapeAdapterFactory.registerSubtype(Circle.class, "Circle");
+ *   shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond");
+ * }
+ * Finally, register the type adapter factory in your application's GSON builder: + *
   {@code
+ *   Gson gson = new GsonBuilder()
+ *       .registerTypeAdapterFactory(shapeAdapterFactory)
+ *       .create();
+ * }
+ * Like {@code GsonBuilder}, this API supports chaining:
   {@code
+ *   RuntimeTypeAdapterFactory shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
+ *       .registerSubtype(Rectangle.class)
+ *       .registerSubtype(Circle.class)
+ *       .registerSubtype(Diamond.class);
+ * }
+ */ +public final class RuntimeTypeAdapterFactory implements TypeAdapterFactory { + private final Class baseType; + private final String typeFieldName; + private final Map> labelToSubtype = new LinkedHashMap>(); + private final Map, String> subtypeToLabel = new LinkedHashMap, String>(); + private final boolean maintainType; + + private RuntimeTypeAdapterFactory(Class baseType, String typeFieldName, boolean maintainType) { + if (typeFieldName == null || baseType == null) { + throw new NullPointerException(); + } + this.baseType = baseType; + this.typeFieldName = typeFieldName; + this.maintainType = maintainType; + } + + /** + * Creates a new runtime type adapter using for {@code baseType} using {@code + * typeFieldName} as the type field name. Type field names are case sensitive. + * {@code maintainType} flag decide if the type will be stored in pojo or not. + */ + public static RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName, boolean maintainType) { + return new RuntimeTypeAdapterFactory(baseType, typeFieldName, maintainType); + } + + /** + * Creates a new runtime type adapter using for {@code baseType} using {@code + * typeFieldName} as the type field name. Type field names are case sensitive. + */ + public static RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName) { + return new RuntimeTypeAdapterFactory(baseType, typeFieldName, false); + } + + /** + * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as + * the type field name. + */ + public static RuntimeTypeAdapterFactory of(Class baseType) { + return new RuntimeTypeAdapterFactory(baseType, "type", false); + } + + /** + * Registers {@code type} identified by {@code label}. Labels are case + * sensitive. + * + * @throws IllegalArgumentException if either {@code type} or {@code label} + * have already been registered on this type adapter. + */ + public RuntimeTypeAdapterFactory registerSubtype(Class type, String label) { + if (type == null || label == null) { + throw new NullPointerException(); + } + if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) { + throw new IllegalArgumentException("types and labels must be unique"); + } + labelToSubtype.put(label, type); + subtypeToLabel.put(type, label); + return this; + } + + /** + * Registers {@code type} identified by its {@link Class#getSimpleName simple + * name}. Labels are case sensitive. + * + * @throws IllegalArgumentException if either {@code type} or its simple name + * have already been registered on this type adapter. + */ + public RuntimeTypeAdapterFactory registerSubtype(Class type) { + return registerSubtype(type, type.getSimpleName()); + } + + public TypeAdapter create(Gson gson, TypeToken type) { + if (type.getRawType() != baseType) { + return null; + } + + final Map> labelToDelegate + = new LinkedHashMap>(); + final Map, TypeAdapter> subtypeToDelegate + = new LinkedHashMap, TypeAdapter>(); + for (Map.Entry> entry : labelToSubtype.entrySet()) { + TypeAdapter delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue())); + labelToDelegate.put(entry.getKey(), delegate); + subtypeToDelegate.put(entry.getValue(), delegate); + } + + return new TypeAdapter() { + @Override + public R read(JsonReader in) throws IOException { + JsonElement jsonElement = Streams.parse(in); + JsonElement labelJsonElement; + if (maintainType) { + labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName); + } else { + labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName); + } + + if (labelJsonElement == null) { + throw new JsonParseException("cannot deserialize " + baseType + + " because it does not define a field named " + typeFieldName); + } + String label = labelJsonElement.getAsString(); + @SuppressWarnings("unchecked") // registration requires that subtype extends T + TypeAdapter delegate = (TypeAdapter) labelToDelegate.get(label); + if (delegate == null) { + throw new JsonParseException("cannot deserialize " + baseType + " subtype named " + + label + "; did you forget to register a subtype?"); + } + return delegate.fromJsonTree(jsonElement); + } + + @Override + public void write(JsonWriter out, R value) throws IOException { + Class srcType = value.getClass(); + String label = subtypeToLabel.get(srcType); + @SuppressWarnings("unchecked") // registration requires that subtype extends T + TypeAdapter delegate = (TypeAdapter) subtypeToDelegate.get(srcType); + if (delegate == null) { + throw new JsonParseException("cannot serialize " + srcType.getName() + + "; did you forget to register a subtype?"); + } + JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject(); + + if (maintainType) { + Streams.write(jsonObject, out); + return; + } + + JsonObject clone = new JsonObject(); + + if (jsonObject.has(typeFieldName)) { + throw new JsonParseException("cannot serialize " + srcType.getName() + + " because it already defines a field named " + typeFieldName); + } + clone.add(typeFieldName, new JsonPrimitive(label)); + + for (Map.Entry e : jsonObject.entrySet()) { + clone.add(e.getKey(), e.getValue()); + } + Streams.write(clone, out); + } + }.nullSafe(); + } +} 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 new file mode 100644 index 0000000..7d9b5af --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/clock/extensions/gson/typeAdapter.kt @@ -0,0 +1,22 @@ +package app.common + +import com.google.gson.GsonBuilder +import com.google.gson.TypeAdapterFactory +import com.simplemobiletools.clock.extensions.gson.RuntimeTypeAdapterFactory +import com.simplemobiletools.clock.services.TimerState + +val timerStates = valueOf() + .registerSubtype(TimerState.Idle::class.java) + .registerSubtype(TimerState.Start::class.java) + .registerSubtype(TimerState.Running::class.java) + .registerSubtype(TimerState.Pause::class.java) + .registerSubtype(TimerState.Paused::class.java) + .registerSubtype(TimerState.Finish::class.java) + +inline fun valueOf(): RuntimeTypeAdapterFactory = RuntimeTypeAdapterFactory.of(T::class.java) + +fun GsonBuilder.registerTypes(vararg types: TypeAdapterFactory) = apply { + types.forEach { registerTypeAdapterFactory(it) } +} + +val gson = GsonBuilder().registerTypes(timerStates).create() 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 b8fe52c..d87dda1 100644 --- a/app/src/main/kotlin/com/simplemobiletools/clock/fragments/TimerFragment.kt +++ b/app/src/main/kotlin/com/simplemobiletools/clock/fragments/TimerFragment.kt @@ -1,51 +1,58 @@ package com.simplemobiletools.clock.fragments -import android.annotation.TargetApi -import android.app.Notification -import android.app.NotificationChannel -import android.app.NotificationManager -import android.content.Context import android.graphics.Color import android.media.AudioManager -import android.os.Build import android.os.Bundle -import android.os.CountDownTimer import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.app.NotificationCompat import androidx.fragment.app.Fragment -import androidx.lifecycle.Observer -import androidx.work.WorkInfo -import androidx.work.WorkManager import com.simplemobiletools.clock.R import com.simplemobiletools.clock.activities.SimpleActivity import com.simplemobiletools.clock.dialogs.MyTimePickerDialogDialog import com.simplemobiletools.clock.extensions.* -import com.simplemobiletools.clock.helpers.Config import com.simplemobiletools.clock.helpers.PICK_AUDIO_FILE_INTENT_ID -import com.simplemobiletools.clock.helpers.TIMER_NOTIF_ID -import com.simplemobiletools.clock.workers.* +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 -import com.simplemobiletools.commons.helpers.isOreoPlus import com.simplemobiletools.commons.models.AlarmSound import kotlinx.android.synthetic.main.fragment_timer.view.* -import java.util.concurrent.TimeUnit - +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode class TimerFragment : Fragment() { lateinit var view: ViewGroup - private var timer: CountDownTimer? = null - private var isRunning = false + private var timerState: TimerState = TimerState.Idle + + override fun onResume() { + super.onResume() + + timerState = requiredActivity.config.timerState + updateViewStates(timerState) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + EventBus.getDefault().register(this) + } + + override fun onDestroy() { + EventBus.getDefault().unregister(this) + super.onDestroy() + } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { view = (inflater.inflate(R.layout.fragment_timer, container, false) as ViewGroup).apply { val config = requiredActivity.config val textColor = config.textColor + timer_time.text = 0.getFormattedDuration() + requiredActivity.updateTextColors(timer_fragment) timer_play_pause.background = resources.getColoredDrawableWithColor(R.drawable.circle_background_filled, context!!.getAdjustedPrimaryColor()) timer_reset.applyColorFilter(textColor) @@ -60,27 +67,18 @@ class TimerFragment : Fragment() { timer_sound.colorLeftDrawable(textColor) timer_time.setOnClickListener { - if (isRunning) { - pauseTimer(config) - } else { - startTimer(config) - } + EventBus.getDefault().post(TimerState.Idle) + requiredActivity.hideTimerNotification() + requiredActivity.toast(R.string.timer_stopped) } timer_play_pause.setOnClickListener { - if (isRunning) { - pauseTimer(config) - } else { - startTimer(config) - } + context.startTimerService() } timer_reset.setOnClickListener { - cancelTimerWorker() + EventBus.getDefault().post(TimerState.Idle) requiredActivity.hideTimerNotification() - - config.timerTickStamp = 0L - config.timerStartStamp = 0L requiredActivity.toast(R.string.timer_stopped) } @@ -114,72 +112,46 @@ class TimerFragment : Fragment() { context.checkAlarmsWithDeletedSoundUri(sound.uri) }) } - - WorkManager.getInstance(requiredActivity).getWorkInfosByTagLiveData(TIMER_WORKER_KEY).observe(requiredActivity, Observer { workInfo -> - val workerState = workInfo?.firstOrNull()?.state - isRunning = (workerState == WorkInfo.State.ENQUEUED) - - updateIcons(isRunning) - timer_reset.beVisibleIf(isRunning) - timer?.cancel() - - when (workerState) { - WorkInfo.State.ENQUEUED -> { - val duration = config.timerSeconds.toLong() * 1000 //MS - - timer = object : CountDownTimer(duration, 1000) { - override fun onTick(millisUntilFinished: Long) { - timer_time.text = TimeUnit.MILLISECONDS.toSeconds(millisUntilFinished).toInt().getFormattedDuration() - } - - override fun onFinish() {} - }.start() - } - - else -> { - timer_time.text = 0.getFormattedDuration() - } - } - }) - - cancelTimerWorker() } return view } - private fun startTimer(config: Config) { - val isTimerNoTick = config.timerTickStamp == 0L - - if (isTimerNoTick) { - config.timerStartStamp = System.currentTimeMillis() - - val selectedDuration = config.timerSeconds - val formattedTimestamp = config.timerStartStamp.timestampFormat("HH:mm:ss") - - enqueueTimerWorker(TimeUnit.SECONDS.toMillis(selectedDuration.toLong())) - showNotification("(${selectedDuration.getFormattedDuration()}) $formattedTimestamp") - } else { - val duration = config.timerSeconds.toLong() * 1000 //MS - val selectedDuration = (config.timerStartStamp + duration) - (config.timerTickStamp - config.timerStartStamp) - val formattedTimestamp = config.timerStartStamp.timestampFormat("HH:mm:ss") - - enqueueTimerWorker(TimeUnit.SECONDS.toMillis(selectedDuration.toLong())) - showNotification("(${selectedDuration.toInt().getFormattedDuration()}) $formattedTimestamp") - } + @Subscribe(threadMode = ThreadMode.MAIN) + fun onMessageEvent(state: TimerState.Idle) { + view.timer_time.text = 0.getFormattedDuration() + updateViewStates(state) } - private fun pauseTimer(config: Config) { - cancelTimerWorker() - requiredActivity.hideTimerNotification() + @Subscribe(threadMode = ThreadMode.MAIN) + fun onMessageEvent(state: TimerState.Running) { + view.timer_time.text = state.tick.div(1000).toInt().getFormattedDuration() + updateViewStates(state) + } - config.timerTickStamp = System.currentTimeMillis() + @Subscribe(threadMode = ThreadMode.MAIN) + fun onMessageEvent(state: TimerState.Paused) { + updateViewStates(state) + } - val tick = config.timerTickStamp - val duration = config.timerSeconds.toLong() * 1000 //MS - val startedAt = config.timerStartStamp - val distance = duration - (tick - startedAt) - view.timer_time.text = distance.toInt().getFormattedDuration() + private fun updateViewStates(timerState: TimerState) { + view.timer_reset.beVisibleIf(timerState is TimerState.Running || timerState is TimerState.Paused) + + val drawableId = + if (timerState 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)) } fun updateAlarmSound(alarmSound: AlarmSound) { @@ -187,38 +159,4 @@ class TimerFragment : Fragment() { requiredActivity.config.timerSoundUri = alarmSound.uri view.timer_sound.text = alarmSound.title } - - private fun updateIcons(isRunning: Boolean) { - val drawableId = if (isRunning) 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)) - } - - @TargetApi(Build.VERSION_CODES.O) - private fun showNotification(formattedDuration: String) { - val channelId = "simple_alarm_timer" - val label = getString(R.string.timer) - val notificationManager = requiredActivity.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(context) - .setContentTitle(label) - .setContentText(formattedDuration) - .setSmallIcon(R.drawable.ic_timer) - .setContentIntent(context!!.getOpenTimerTabIntent()) - .setPriority(Notification.PRIORITY_HIGH) - .setSound(null) - .setOngoing(true) - .setAutoCancel(true) - .setChannelId(channelId) - - builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - notificationManager.notify(TIMER_NOTIF_ID, builder.build()) - } } diff --git a/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Config.kt b/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Config.kt index 9f2f8ab..6590627 100644 --- a/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Config.kt +++ b/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Config.kt @@ -1,10 +1,15 @@ package com.simplemobiletools.clock.helpers import android.content.Context +import app.common.gson +import com.google.gson.Gson +import com.simplemobiletools.clock.services.StateWrapper +import com.simplemobiletools.clock.services.TimerState import com.simplemobiletools.commons.extensions.getDefaultAlarmTitle import com.simplemobiletools.commons.extensions.getDefaultAlarmUri import com.simplemobiletools.commons.helpers.ALARM_SOUND_TYPE_ALARM import com.simplemobiletools.commons.helpers.BaseConfig +import java.sql.Time class Config(context: Context) : BaseConfig(context) { companion object { @@ -27,13 +32,11 @@ class Config(context: Context) : BaseConfig(context) { get() = prefs.getInt(TIMER_SECONDS, 300) set(lastTimerSeconds) = prefs.edit().putInt(TIMER_SECONDS, lastTimerSeconds).apply() - var timerStartStamp: Long - get() = prefs.getLong(TIMER_START_TIMESTAMP, 0L) - set(timestamp) = prefs.edit().putLong(TIMER_START_TIMESTAMP, timestamp).apply() - - var timerTickStamp: Long - get() = prefs.getLong(TIMER_TICK_TIMESTAMP, 0L) - set(timestamp) = prefs.edit().putLong(TIMER_TICK_TIMESTAMP, timestamp).apply() + var timerState: TimerState + get() = prefs.getString(TIMER_STATE, null)?.let { state -> + gson.fromJson(state, StateWrapper::class.java) + }?.state ?: TimerState.Idle + set(state) = prefs.edit().putString(TIMER_STATE, gson.toJson(StateWrapper(state))).apply() var timerVibrate: Boolean get() = prefs.getBoolean(TIMER_VIBRATE, false) diff --git a/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Constants.kt b/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Constants.kt index f383d54..c58f2a5 100644 --- a/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Constants.kt +++ b/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Constants.kt @@ -9,7 +9,7 @@ const val SELECTED_TIME_ZONES = "selected_time_zones" const val EDITED_TIME_ZONE_TITLES = "edited_time_zone_titles" const val TIMER_SECONDS = "timer_seconds" const val TIMER_START_TIMESTAMP = "timer_timetamp" -const val TIMER_TICK_TIMESTAMP = "timer_tick" +const val TIMER_STATE = "timer_state" const val TIMER_VIBRATE = "timer_vibrate" const val TIMER_SOUND_URI = "timer_sound_uri" const val TIMER_SOUND_TITLE = "timer_sound_title" @@ -32,6 +32,7 @@ const val UPDATE_WIDGET_INTENT_ID = 9997 const val OPEN_APP_INTENT_ID = 9998 const val ALARM_NOTIF_ID = 9998 const val TIMER_NOTIF_ID = 9999 +const val TIMER_RUNNING_NOTIF_ID = 10000 const val OPEN_TAB = "open_tab" const val TAB_CLOCK = 0 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..1cac5e4 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/clock/services/timerService.kt @@ -0,0 +1,156 @@ +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.* +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() +} diff --git a/app/src/main/kotlin/com/simplemobiletools/clock/workers/timerWorker.kt b/app/src/main/kotlin/com/simplemobiletools/clock/workers/timerWorker.kt deleted file mode 100644 index 1bb655c..0000000 --- a/app/src/main/kotlin/com/simplemobiletools/clock/workers/timerWorker.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.simplemobiletools.clock.workers - -import android.content.Context -import androidx.fragment.app.Fragment -import androidx.work.* -import com.simplemobiletools.clock.extensions.* -import java.util.* -import java.util.concurrent.TimeUnit - -private const val TIMER_REQUEST_ID = "TIMER_REQUEST_ID" -const val TIMER_WORKER_KEY = "TIMER_WORKER_KEY" - -private fun Fragment.saveTimerRequestId(uuid: UUID) = - preferences.edit().putString(TIMER_REQUEST_ID, uuid.toString()).apply() - -val Fragment.timerRequestId: UUID? - get() = - preferences.getString(TIMER_REQUEST_ID, UUID.randomUUID().toString())?.let { UUID.fromString(it) } - -fun Fragment.cancelTimerWorker() = - WorkManager.getInstance(requiredActivity).cancelAllWorkByTag(TIMER_WORKER_KEY) - -fun Fragment.enqueueTimerWorker(delay: Long) = - WorkManager.getInstance(requiredActivity).enqueueUniqueWork(TIMER_WORKER_KEY, ExistingWorkPolicy.REPLACE, timerRequest(delay)) - -private fun Fragment.timerRequest(delay: Long): OneTimeWorkRequest = - OneTimeWorkRequestBuilder().setInitialDelay(delay, TimeUnit.MILLISECONDS).addTag(TIMER_WORKER_KEY).build().also { - saveTimerRequestId(it.id) -} - -class TimerWorker(val context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) { - override fun doWork(): Result = - try { - context.showTimerNotification(false) - context.config.timerTickStamp = 0L - context.config.timerStartStamp = 0L - Result.success() - } catch (exception: Exception) { - Result.failure() - } -} From f6d9c5aece36d2c69d5217a7b2ade21f64212bb5 Mon Sep 17 00:00:00 2001 From: Pavol Franek <> Date: Mon, 9 Mar 2020 06:49:49 +0100 Subject: [PATCH 5/6] Fixed issue #77 Do not stop the timer at closing the app --- .../clock/extensions/Context.kt | 2 -- .../gson/RuntimeTypeAdapterFactory.java | 4 ++-- .../clock/extensions/gson/typeAdapter.kt | 20 ++++++++-------- .../clock/fragments/TimerFragment.kt | 23 +++++++------------ .../simplemobiletools/clock/helpers/Config.kt | 4 +--- .../clock/services/timerService.kt | 6 +++-- 6 files changed, 25 insertions(+), 34 deletions(-) diff --git a/app/src/main/kotlin/com/simplemobiletools/clock/extensions/Context.kt b/app/src/main/kotlin/com/simplemobiletools/clock/extensions/Context.kt index 009b2e1..574adec 100644 --- a/app/src/main/kotlin/com/simplemobiletools/clock/extensions/Context.kt +++ b/app/src/main/kotlin/com/simplemobiletools/clock/extensions/Context.kt @@ -377,5 +377,3 @@ fun Context.checkAlarmsWithDeletedSoundUri(uri: String) { dbHelper.updateAlarm(it) } } - -val Context.preferences: SharedPreferences get() = PreferenceManager.getDefaultSharedPreferences(this) diff --git a/app/src/main/kotlin/com/simplemobiletools/clock/extensions/gson/RuntimeTypeAdapterFactory.java b/app/src/main/kotlin/com/simplemobiletools/clock/extensions/gson/RuntimeTypeAdapterFactory.java index b04dba4..ad5ce3d 100644 --- a/app/src/main/kotlin/com/simplemobiletools/clock/extensions/gson/RuntimeTypeAdapterFactory.java +++ b/app/src/main/kotlin/com/simplemobiletools/clock/extensions/gson/RuntimeTypeAdapterFactory.java @@ -167,7 +167,7 @@ public final class RuntimeTypeAdapterFactory implements TypeAdapterFactory { * sensitive. * * @throws IllegalArgumentException if either {@code type} or {@code label} - * have already been registered on this type adapter. + * have already been registered on this type adapter. */ public RuntimeTypeAdapterFactory registerSubtype(Class type, String label) { if (type == null || label == null) { @@ -186,7 +186,7 @@ public final class RuntimeTypeAdapterFactory implements TypeAdapterFactory { * name}. Labels are case sensitive. * * @throws IllegalArgumentException if either {@code type} or its simple name - * have already been registered on this type adapter. + * have already been registered on this type adapter. */ public RuntimeTypeAdapterFactory registerSubtype(Class type) { return registerSubtype(type, type.getSimpleName()); 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 index 7d9b5af..550d57c 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 @@ -1,22 +1,22 @@ -package app.common +package com.simplemobiletools.clock.extensions.gson +import com.google.gson.Gson import com.google.gson.GsonBuilder import com.google.gson.TypeAdapterFactory -import com.simplemobiletools.clock.extensions.gson.RuntimeTypeAdapterFactory import com.simplemobiletools.clock.services.TimerState val timerStates = valueOf() - .registerSubtype(TimerState.Idle::class.java) - .registerSubtype(TimerState.Start::class.java) - .registerSubtype(TimerState.Running::class.java) - .registerSubtype(TimerState.Pause::class.java) - .registerSubtype(TimerState.Paused::class.java) - .registerSubtype(TimerState.Finish::class.java) + .registerSubtype(TimerState.Idle::class.java) + .registerSubtype(TimerState.Start::class.java) + .registerSubtype(TimerState.Running::class.java) + .registerSubtype(TimerState.Pause::class.java) + .registerSubtype(TimerState.Paused::class.java) + .registerSubtype(TimerState.Finish::class.java) -inline fun valueOf(): RuntimeTypeAdapterFactory = RuntimeTypeAdapterFactory.of(T::class.java) +inline fun valueOf(): RuntimeTypeAdapterFactory = RuntimeTypeAdapterFactory.of(T::class.java) fun GsonBuilder.registerTypes(vararg types: TypeAdapterFactory) = apply { types.forEach { registerTypeAdapterFactory(it) } } -val gson = GsonBuilder().registerTypes(timerStates).create() +val gson: Gson = GsonBuilder().registerTypes(timerStates).create() 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 d87dda1..65095ac 100644 --- a/app/src/main/kotlin/com/simplemobiletools/clock/fragments/TimerFragment.kt +++ b/app/src/main/kotlin/com/simplemobiletools/clock/fragments/TimerFragment.kt @@ -26,18 +26,9 @@ import org.greenrobot.eventbus.ThreadMode class TimerFragment : Fragment() { lateinit var view: ViewGroup - private var timerState: TimerState = TimerState.Idle - - override fun onResume() { - super.onResume() - - timerState = requiredActivity.config.timerState - updateViewStates(timerState) - } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - EventBus.getDefault().register(this) } @@ -67,9 +58,7 @@ class TimerFragment : Fragment() { timer_sound.colorLeftDrawable(textColor) timer_time.setOnClickListener { - EventBus.getDefault().post(TimerState.Idle) - requiredActivity.hideTimerNotification() - requiredActivity.toast(R.string.timer_stopped) + stopTimer() } timer_play_pause.setOnClickListener { @@ -77,9 +66,7 @@ class TimerFragment : Fragment() { } timer_reset.setOnClickListener { - EventBus.getDefault().post(TimerState.Idle) - requiredActivity.hideTimerNotification() - requiredActivity.toast(R.string.timer_stopped) + stopTimer() } timer_initial_time.setOnClickListener { @@ -117,6 +104,12 @@ class TimerFragment : Fragment() { return view } + private fun stopTimer() { + EventBus.getDefault().post(TimerState.Idle) + requiredActivity.hideTimerNotification() + requiredActivity.toast(R.string.timer_stopped) + } + @Subscribe(threadMode = ThreadMode.MAIN) fun onMessageEvent(state: TimerState.Idle) { view.timer_time.text = 0.getFormattedDuration() diff --git a/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Config.kt b/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Config.kt index 6590627..104b8ba 100644 --- a/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Config.kt +++ b/app/src/main/kotlin/com/simplemobiletools/clock/helpers/Config.kt @@ -1,15 +1,13 @@ package com.simplemobiletools.clock.helpers import android.content.Context -import app.common.gson -import com.google.gson.Gson +import com.simplemobiletools.clock.extensions.gson.gson import com.simplemobiletools.clock.services.StateWrapper import com.simplemobiletools.clock.services.TimerState import com.simplemobiletools.commons.extensions.getDefaultAlarmTitle import com.simplemobiletools.commons.extensions.getDefaultAlarmUri import com.simplemobiletools.commons.helpers.ALARM_SOUND_TYPE_ALARM import com.simplemobiletools.commons.helpers.BaseConfig -import java.sql.Time class Config(context: Context) : BaseConfig(context) { companion object { diff --git a/app/src/main/kotlin/com/simplemobiletools/clock/services/timerService.kt b/app/src/main/kotlin/com/simplemobiletools/clock/services/timerService.kt index 1cac5e4..c84d2f3 100644 --- a/app/src/main/kotlin/com/simplemobiletools/clock/services/timerService.kt +++ b/app/src/main/kotlin/com/simplemobiletools/clock/services/timerService.kt @@ -12,7 +12,10 @@ import android.os.CountDownTimer import android.os.IBinder import androidx.core.app.NotificationCompat import com.simplemobiletools.clock.R -import com.simplemobiletools.clock.extensions.* +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 @@ -87,7 +90,6 @@ class TimerService : Service() { 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) From 060b8c6a23058515666d8fea7bd34c43bfdb37ee Mon Sep 17 00:00:00 2001 From: Pavol Franek <> Date: Tue, 10 Mar 2020 15:20:02 +0100 Subject: [PATCH 6/6] 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() -}