This commit is contained in:
Pavol Franek 2020-03-04 08:42:19 +01:00
parent afaa3f470c
commit 046f0323d1
5 changed files with 202 additions and 148 deletions

View File

@ -54,6 +54,16 @@ android {
checkReleaseBuilds false checkReleaseBuilds false
abortOnError false abortOnError false
} }
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs = ["-XXLanguage:+NewInference"]
}
} }
dependencies { dependencies {
@ -61,4 +71,8 @@ dependencies {
implementation 'com.facebook.stetho:stetho:1.5.0' implementation 'com.facebook.stetho:stetho:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta2' implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta2'
implementation 'com.shawnlin:number-picker:2.4.6' 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'
} }

View File

@ -6,6 +6,7 @@ import android.appwidget.AppWidgetManager
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences
import android.media.AudioAttributes import android.media.AudioAttributes
import android.media.AudioManager import android.media.AudioManager
import android.net.Uri import android.net.Uri
@ -15,6 +16,7 @@ import android.text.style.RelativeSizeSpan
import android.widget.Toast import android.widget.Toast
import androidx.core.app.AlarmManagerCompat import androidx.core.app.AlarmManagerCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.preference.PreferenceManager
import com.simplemobiletools.clock.R import com.simplemobiletools.clock.R
import com.simplemobiletools.clock.activities.ReminderActivity import com.simplemobiletools.clock.activities.ReminderActivity
import com.simplemobiletools.clock.activities.SnoozeReminderActivity import com.simplemobiletools.clock.activities.SnoozeReminderActivity
@ -382,3 +384,5 @@ fun Context.checkAlarmsWithDeletedSoundUri(uri: String) {
dbHelper.updateAlarm(it) dbHelper.updateAlarm(it)
} }
} }
val Context.preferences: SharedPreferences get() = PreferenceManager.getDefaultSharedPreferences(this)

View File

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

View File

@ -4,6 +4,7 @@ import android.annotation.TargetApi
import android.app.Notification import android.app.Notification
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.NotificationManager.IMPORTANCE_HIGH
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
@ -12,11 +13,16 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.SystemClock import android.os.SystemClock
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.fragment.app.Fragment 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.R
import com.simplemobiletools.clock.activities.ReminderActivity import com.simplemobiletools.clock.activities.ReminderActivity
import com.simplemobiletools.clock.activities.SimpleActivity 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.extensions.*
import com.simplemobiletools.clock.helpers.PICK_AUDIO_FILE_INTENT_ID import com.simplemobiletools.clock.helpers.PICK_AUDIO_FILE_INTENT_ID
import com.simplemobiletools.clock.helpers.TIMER_NOTIF_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.dialogs.SelectAlarmSoundDialog
import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.ALARM_SOUND_TYPE_ALARM import com.simplemobiletools.commons.helpers.ALARM_SOUND_TYPE_ALARM
import com.simplemobiletools.commons.helpers.isOreoPlus import com.simplemobiletools.commons.helpers.isOreoPlus
import com.simplemobiletools.commons.models.AlarmSound 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.*
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() { class TimerFragment : Fragment() {
private val UPDATE_INTERVAL = 1000L private val UPDATE_INTERVAL = 1000L
private val WAS_RUNNING = "was_running" private val WAS_RUNNING = "was_running"
private val CURRENT_TICKS = "current_ticks" private val CURRENT_TICKS = "current_ticks"
private val TOTAL_TICKS = "total_ticks" private val TOTAL_TICKS = "total_ticks"
private var isRunning = false private var isRunning = false
private var uptimeAtStart = 0L
private var initialSecs = 0 private var initialSecs = 0
private var totalTicks = 0 private var totalTicks = 0
private var currentTicks = 0 private var currentTicks = 0
private var updateHandler = Handler()
private var isForegrounded = true
lateinit var view: ViewGroup lateinit var view: ViewGroup
@InternalCoroutinesApi
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { 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 { view = (inflater.inflate(R.layout.fragment_timer, container, false) as ViewGroup).apply {
timer_time.setOnClickListener { timer_time.setOnClickListener {
togglePlayPause() val selectedDuration = config.timerSeconds
enqueueTimerWorker(TimeUnit.SECONDS.toMillis(selectedDuration.toLong()))
showNotification(selectedDuration.getFormattedDuration())
} }
timer_play_pause.setOnClickListener { timer_play_pause.setOnClickListener {
togglePlayPause() val selectedDuration = config.timerSeconds
enqueueTimerWorker(TimeUnit.SECONDS.toMillis(selectedDuration.toLong()))
showNotification(selectedDuration.getFormattedDuration())
} }
timer_reset.setOnClickListener { timer_reset.setOnClickListener {
context!!.hideTimerNotification() cancelTimerWorker()
resetTimer()
} }
timer_initial_time.setOnClickListener { timer_initial_time.setOnClickListener {
MyTimePickerDialogDialog(activity as SimpleActivity, config.timerSeconds) { MyTimePickerDialogDialog(activity as SimpleActivity, config.timerSeconds) { seconds ->
val seconds = if (it <= 0) 10 else it val timerSeconds = if (seconds <= 0) 10 else seconds
config.timerSeconds = seconds config.timerSeconds = timerSeconds
timer_initial_time.text = seconds.getFormattedDuration() timer_initial_time.text = timerSeconds.getFormattedDuration()
if (!isRunning) {
resetTimer()
}
} }
} }
@ -81,28 +99,47 @@ class TimerFragment : Fragment() {
timer_sound.setOnClickListener { timer_sound.setOnClickListener {
SelectAlarmSoundDialog(activity as SimpleActivity, config.timerSoundUri, AudioManager.STREAM_ALARM, PICK_AUDIO_FILE_INTENT_ID, SelectAlarmSoundDialog(activity as SimpleActivity, config.timerSoundUri, AudioManager.STREAM_ALARM, PICK_AUDIO_FILE_INTENT_ID,
ALARM_SOUND_TYPE_ALARM, true, onAlarmPicked = { ALARM_SOUND_TYPE_ALARM, true,
if (it != null) { onAlarmPicked = { sound ->
updateAlarmSound(it) if (sound != null) {
} updateAlarmSound(sound)
}, onAlarmSoundDeleted = { }
if (config.timerSoundUri == it.uri) { },
val defaultAlarm = context.getDefaultAlarmSound(ALARM_SOUND_TYPE_ALARM) onAlarmSoundDeleted = { sound ->
updateAlarmSound(defaultAlarm) if (config.timerSoundUri == sound.uri) {
} val defaultAlarm = context.getDefaultAlarmSound(ALARM_SOUND_TYPE_ALARM)
context.checkAlarmsWithDeletedSoundUri(it.uri) updateAlarmSound(defaultAlarm)
}) }
context.checkAlarmsWithDeletedSoundUri(sound.uri)
})
} }
} }
initialSecs = config.timerSeconds initialSecs = config.timerSeconds
updateDisplayedText()
return view
}
override fun onStart() { WorkManager.getInstance(activity!!).getWorkInfoByIdLiveData(timerRequestId)
super.onStart() .observe(this, Observer { workInfo ->
isForegrounded = true 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() { override fun onResume() {
@ -110,28 +147,12 @@ class TimerFragment : Fragment() {
setupViews() 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) { override fun onSaveInstanceState(outState: Bundle) {
outState.apply { super.onSaveInstanceState(outState.apply {
putBoolean(WAS_RUNNING, isRunning) putBoolean(WAS_RUNNING, isRunning)
putInt(TOTAL_TICKS, totalTicks) putInt(TOTAL_TICKS, totalTicks)
putInt(CURRENT_TICKS, currentTicks) putInt(CURRENT_TICKS, currentTicks)
} })
super.onSaveInstanceState(outState)
} }
override fun onViewStateRestored(savedInstanceState: Bundle?) { override fun onViewStateRestored(savedInstanceState: Bundle?) {
@ -140,25 +161,21 @@ class TimerFragment : Fragment() {
isRunning = getBoolean(WAS_RUNNING, false) isRunning = getBoolean(WAS_RUNNING, false)
totalTicks = getInt(TOTAL_TICKS, 0) totalTicks = getInt(TOTAL_TICKS, 0)
currentTicks = getInt(CURRENT_TICKS, 0) currentTicks = getInt(CURRENT_TICKS, 0)
if (isRunning) {
uptimeAtStart = SystemClock.uptimeMillis() - currentTicks * UPDATE_INTERVAL
updateTimerState(false)
}
} }
} }
fun updateAlarmSound(alarmSound: AlarmSound) { fun updateAlarmSound(alarmSound: AlarmSound) {
context!!.config.timerSoundTitle = alarmSound.title requiredActivity.config.timerSoundTitle = alarmSound.title
context!!.config.timerSoundUri = alarmSound.uri requiredActivity.config.timerSoundUri = alarmSound.uri
view.timer_sound.text = alarmSound.title view.timer_sound.text = alarmSound.title
} }
private fun setupViews() { private fun setupViews() {
val config = context!!.config val config = requiredActivity.config
val textColor = config.textColor val textColor = config.textColor
view.apply { view.apply {
context!!.updateTextColors(timer_fragment) requiredActivity.updateTextColors(timer_fragment)
timer_play_pause.background = resources.getColoredDrawableWithColor(R.drawable.circle_background_filled, context!!.getAdjustedPrimaryColor()) timer_play_pause.background = resources.getColoredDrawableWithColor(R.drawable.circle_background_filled, context!!.getAdjustedPrimaryColor())
timer_reset.applyColorFilter(textColor) timer_reset.applyColorFilter(textColor)
@ -173,84 +190,19 @@ class TimerFragment : Fragment() {
} }
updateIcons() 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() { private fun updateIcons() {
val drawableId = if (isRunning) R.drawable.ic_pause_vector else R.drawable.ic_play_vector 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)) 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) @TargetApi(Build.VERSION_CODES.O)
private fun showNotification(formattedDuration: String) { private fun showNotification(formattedDuration: String) {
val channelId = "simple_alarm_timer" val channelId = "simple_alarm_timer"
val label = getString(R.string.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()) { if (isOreoPlus()) {
val importance = NotificationManager.IMPORTANCE_HIGH val importance = NotificationManager.IMPORTANCE_HIGH
NotificationChannel(channelId, label, importance).apply { NotificationChannel(channelId, label, importance).apply {
@ -260,29 +212,62 @@ class TimerFragment : Fragment() {
} }
val builder = NotificationCompat.Builder(context) val builder = NotificationCompat.Builder(context)
.setContentTitle(label) .setContentTitle(label)
.setContentText(formattedDuration) .setContentText(formattedDuration)
.setSmallIcon(R.drawable.ic_timer) .setSmallIcon(R.drawable.ic_timer)
.setContentIntent(context!!.getOpenTimerTabIntent()) .setContentIntent(context!!.getOpenTimerTabIntent())
.setPriority(Notification.PRIORITY_HIGH) .setPriority(Notification.PRIORITY_HIGH)
.setSound(null) .setSound(null)
.setOngoing(true) .setOngoing(true)
.setAutoCancel(true) .setAutoCancel(true)
.setChannelId(channelId) .setChannelId(channelId)
builder.setVisibility(Notification.VISIBILITY_PUBLIC) builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
notificationManager.notify(TIMER_NOTIF_ID, builder.build()) 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
// }

View File

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