Almost done

This commit is contained in:
Pavol Franek 2020-03-08 21:42:23 +01:00
parent ed18715b39
commit 44ac30ada4
12 changed files with 530 additions and 180 deletions

View file

@ -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'
}

View file

@ -79,6 +79,7 @@
</activity>
<service android:name=".services.SnoozeService"/>
<service android:name=".services.TimerService" />
<receiver android:name=".receivers.AlarmReceiver"/>

View file

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

View file

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

View file

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

View file

@ -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:
* <pre> {@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;
* }
* }</pre>
* <p>Without additional type information, the serialized JSON is ambiguous. Is
* the bottom shape in this drawing a rectangle or a diamond? <pre> {@code
* {
* "bottomShape": {
* "width": 10,
* "height": 5,
* "x": 0,
* "y": 0
* },
* "topShape": {
* "radius": 2,
* "x": 4,
* "y": 1
* }
* }}</pre>
* This class addresses this problem by adding type information to the
* serialized JSON and honoring that type information when the JSON is
* deserialized: <pre> {@code
* {
* "bottomShape": {
* "type": "Diamond",
* "width": 10,
* "height": 5,
* "x": 0,
* "y": 0
* },
* "topShape": {
* "type": "Circle",
* "radius": 2,
* "x": 4,
* "y": 1
* }
* }}</pre>
* Both the type field name ({@code "type"}) and the type labels ({@code
* "Rectangle"}) are configurable.
*
* <h3>Registering Types</h3>
* 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. <pre> {@code
* RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory
* = RuntimeTypeAdapterFactory.of(Shape.class, "type");
* }</pre>
* 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.
* <pre> {@code
* shapeAdapterFactory.registerSubtype(Rectangle.class, "Rectangle");
* shapeAdapterFactory.registerSubtype(Circle.class, "Circle");
* shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond");
* }</pre>
* Finally, register the type adapter factory in your application's GSON builder:
* <pre> {@code
* Gson gson = new GsonBuilder()
* .registerTypeAdapterFactory(shapeAdapterFactory)
* .create();
* }</pre>
* Like {@code GsonBuilder}, this API supports chaining: <pre> {@code
* RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
* .registerSubtype(Rectangle.class)
* .registerSubtype(Circle.class)
* .registerSubtype(Diamond.class);
* }</pre>
*/
public final class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory {
private final Class<?> baseType;
private final String typeFieldName;
private final Map<String, Class<?>> labelToSubtype = new LinkedHashMap<String, Class<?>>();
private final Map<Class<?>, String> subtypeToLabel = new LinkedHashMap<Class<?>, 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 <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName, boolean maintainType) {
return new RuntimeTypeAdapterFactory<T>(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 <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName) {
return new RuntimeTypeAdapterFactory<T>(baseType, typeFieldName, false);
}
/**
* Creates a new runtime type adapter for {@code baseType} using {@code "type"} as
* the type field name.
*/
public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType) {
return new RuntimeTypeAdapterFactory<T>(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<T> registerSubtype(Class<? extends T> 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<T> registerSubtype(Class<? extends T> type) {
return registerSubtype(type, type.getSimpleName());
}
public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) {
if (type.getRawType() != baseType) {
return null;
}
final Map<String, TypeAdapter<?>> labelToDelegate
= new LinkedHashMap<String, TypeAdapter<?>>();
final Map<Class<?>, TypeAdapter<?>> subtypeToDelegate
= new LinkedHashMap<Class<?>, TypeAdapter<?>>();
for (Map.Entry<String, Class<?>> 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<R>() {
@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<R> delegate = (TypeAdapter<R>) 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<R> delegate = (TypeAdapter<R>) 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<String, JsonElement> e : jsonObject.entrySet()) {
clone.add(e.getKey(), e.getValue());
}
Streams.write(clone, out);
}
}.nullSafe();
}
}

View file

@ -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<TimerState>()
.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 <reified T: Any> valueOf(): RuntimeTypeAdapterFactory<T> = RuntimeTypeAdapterFactory.of(T::class.java)
fun GsonBuilder.registerTypes(vararg types: TypeAdapterFactory) = apply {
types.forEach { registerTypeAdapterFactory(it) }
}
val gson = GsonBuilder().registerTypes(timerStates).create()

View file

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

View file

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

View file

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

View file

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

View file

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