Merge pull request #285 from KryptKode/feat/multiple-timers

Feat/multiple timers
This commit is contained in:
Tibor Kaputa 2021-09-12 17:29:40 +02:00 committed by GitHub
commit 84326846c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
58 changed files with 1251 additions and 360 deletions

View File

@ -1,6 +1,7 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
def keystorePropertiesFile = rootProject.file("keystore.properties")
def keystoreProperties = new Properties()
@ -66,7 +67,7 @@ android {
}
dependencies {
implementation 'com.github.SimpleMobileTools:Simple-Commons:ac45c5e893'
implementation 'com.github.SimpleMobileTools:Simple-Commons:b6e2ffb710'
implementation 'com.facebook.stetho:stetho:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'com.shawnlin:number-picker:2.4.6'
@ -76,4 +77,6 @@ dependencies {
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
implementation 'org.greenrobot:eventbus:3.2.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.room:room-runtime:2.3.0'
kapt 'androidx.room:room-compiler:2.3.0'
}

View File

@ -37,7 +37,8 @@
<activity
android:name=".activities.MainActivity"
android:launchMode="singleTask" />
android:launchMode="singleTask"
android:windowSoftInputMode="adjustPan"/>
<activity
android:name=".activities.ReminderActivity"

View File

@ -11,10 +11,10 @@ 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.extensions.timerHelper
import com.simplemobiletools.clock.models.TimerEvent
import com.simplemobiletools.clock.models.TimerState
import com.simplemobiletools.clock.services.TimerStopService
import com.simplemobiletools.clock.services.startTimerService
@ -25,8 +25,7 @@ import org.greenrobot.eventbus.ThreadMode
class App : Application(), LifecycleObserver {
private var timer: CountDownTimer? = null
private var lastTick = 0L
private var countDownTimers = mutableMapOf<Int, CountDownTimer>()
override fun onCreate() {
super.onCreate()
@ -48,63 +47,80 @@ class App : Application(), LifecycleObserver {
@RequiresApi(Build.VERSION_CODES.O)
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
private fun onAppBackgrounded() {
if (config.timerState is TimerState.Running) {
startTimerService(this)
timerHelper.getTimers { timers ->
if (timers.any { it.state is TimerState.Running }) {
startTimerService(this)
}
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_START)
private fun onAppForegrounded() {
EventBus.getDefault().post(TimerStopService)
timerHelper.getTimers { timers ->
val runningTimers = timers.filter { it.state is TimerState.Running }
runningTimers.forEach { timer ->
if (countDownTimers[timer.id] == null) {
EventBus.getDefault().post(TimerEvent.Start(timer.id!!, (timer.state as TimerState.Running).tick))
}
}
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onMessageEvent(state: TimerState.Idle) {
config.timerState = state
timer?.cancel()
fun onMessageEvent(event: TimerEvent.Reset) {
updateTimerState(event.timerId, TimerState.Idle)
countDownTimers[event.timerId]?.cancel()
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onMessageEvent(state: TimerState.Start) {
timer = object : CountDownTimer(state.duration, 1000) {
fun onMessageEvent(event: TimerEvent.Delete) {
countDownTimers[event.timerId]?.cancel()
timerHelper.deleteTimer(event.timerId){
EventBus.getDefault().post(TimerEvent.Refresh)
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onMessageEvent(event: TimerEvent.Start) {
val countDownTimer = object : CountDownTimer(event.duration, 1000) {
override fun onTick(tick: Long) {
lastTick = tick
val newState = TimerState.Running(state.duration, tick)
EventBus.getDefault().post(newState)
config.timerState = newState
updateTimerState(event.timerId, TimerState.Running(event.duration, tick))
}
override fun onFinish() {
EventBus.getDefault().post(TimerState.Finish(state.duration))
EventBus.getDefault().post(TimerEvent.Finish(event.timerId, event.duration))
EventBus.getDefault().post(TimerStopService)
}
}.start()
countDownTimers[event.timerId] = countDownTimer
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onMessageEvent(event: TimerState.Finish) {
val pendingIntent = getOpenTimerTabIntent()
val notification = getTimerNotification(pendingIntent, false)
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.notify(TIMER_NOTIF_ID, notification)
EventBus.getDefault().post(TimerState.Finished)
fun onMessageEvent(event: TimerEvent.Finish) {
timerHelper.getTimer(event.timerId) { timer ->
val pendingIntent = getOpenTimerTabIntent(event.timerId)
val notification = getTimerNotification(timer, pendingIntent, false)
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.notify(event.timerId, notification)
updateTimerState(event.timerId, TimerState.Finished)
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onMessageEvent(state: TimerState.Finished) {
config.timerState = state
fun onMessageEvent(event: TimerEvent.Pause) {
timerHelper.getTimer(event.timerId) { timer ->
updateTimerState(event.timerId, TimerState.Paused(event.duration, (timer.state as TimerState.Running).tick))
countDownTimers[event.timerId]?.cancel()
}
}
@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()
private fun updateTimerState(timerId: Int, state: TimerState) {
timerHelper.getTimer(timerId) { timer ->
val newTimer = timer.copy(state = state)
timerHelper.insertOrUpdateTimer(newTimer) {
EventBus.getDefault().post(TimerEvent.Refresh)
}
}
}
}

View File

@ -19,7 +19,8 @@ import com.simplemobiletools.commons.helpers.LICENSE_RTL
import com.simplemobiletools.commons.helpers.LICENSE_STETHO
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.commons.models.FAQItem
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.activity_main.main_tabs_holder
import kotlinx.android.synthetic.main.activity_main.view_pager
class MainActivity : SimpleActivity() {
private var storedTextColor = 0
@ -30,7 +31,6 @@ class MainActivity : SimpleActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
appLaunched(BuildConfig.APPLICATION_ID)
storeStateVariables()
initFragments()
@ -101,8 +101,14 @@ class MainActivity : SimpleActivity() {
override fun onNewIntent(intent: Intent) {
if (intent.extras?.containsKey(OPEN_TAB) == true) {
view_pager.setCurrentItem(intent.getIntExtra(OPEN_TAB, TAB_CLOCK), false)
val tabToOpen = intent.getIntExtra(OPEN_TAB, TAB_CLOCK)
view_pager.setCurrentItem(tabToOpen, false)
if (tabToOpen == TAB_TIMER) {
val timerId = intent.getIntExtra(TIMER_ID, INVALID_TIMER_ID)
(view_pager.adapter as ViewPagerAdapter).updateTimerPosition(timerId)
}
}
super.onNewIntent(intent)
}
private fun storeStateVariables() {
@ -136,7 +142,8 @@ class MainActivity : SimpleActivity() {
private fun getViewPagerAdapter() = view_pager.adapter as? ViewPagerAdapter
private fun initFragments() {
view_pager.adapter = ViewPagerAdapter(supportFragmentManager)
val viewPagerAdapter = ViewPagerAdapter(supportFragmentManager)
view_pager.adapter = viewPagerAdapter
view_pager.onPageChangeListener {
main_tabs_holder.getTabAt(it)?.select()
invalidateOptionsMenu()
@ -144,6 +151,10 @@ class MainActivity : SimpleActivity() {
val tabToOpen = intent.getIntExtra(OPEN_TAB, config.lastUsedViewPagerPage)
intent.removeExtra(OPEN_TAB)
if (tabToOpen == TAB_TIMER) {
val timerId = intent.getIntExtra(TIMER_ID, INVALID_TIMER_ID)
viewPagerAdapter.updateTimerPosition(timerId)
}
view_pager.currentItem = tabToOpen
view_pager.offscreenPageLimit = TABS_COUNT - 1
main_tabs_holder.onTabSelectionChanged(

View File

@ -1,9 +1,7 @@
package com.simplemobiletools.clock.activities
import android.content.Intent
import com.simplemobiletools.clock.helpers.OPEN_TAB
import com.simplemobiletools.clock.helpers.TAB_ALARM
import com.simplemobiletools.clock.helpers.TAB_CLOCK
import com.simplemobiletools.clock.helpers.*
import com.simplemobiletools.commons.activities.BaseSplashActivity
class SplashActivity : BaseSplashActivity() {
@ -18,6 +16,7 @@ class SplashActivity : BaseSplashActivity() {
intent.extras?.containsKey(OPEN_TAB) == true -> {
Intent(this, MainActivity::class.java).apply {
putExtra(OPEN_TAB, intent.getIntExtra(OPEN_TAB, TAB_CLOCK))
putExtra(TIMER_ID, intent.getIntExtra(TIMER_ID, INVALID_TIMER_ID))
startActivity(this)
}
}

View File

@ -0,0 +1,146 @@
package com.simplemobiletools.clock.adapters
import android.view.Menu
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import com.simplemobiletools.clock.R
import com.simplemobiletools.clock.activities.SimpleActivity
import com.simplemobiletools.clock.extensions.getFormattedDuration
import com.simplemobiletools.clock.extensions.hideTimerNotification
import com.simplemobiletools.clock.extensions.secondsToMillis
import com.simplemobiletools.clock.models.Timer
import com.simplemobiletools.clock.models.TimerEvent
import com.simplemobiletools.clock.models.TimerState
import com.simplemobiletools.commons.adapters.MyRecyclerViewListAdapter
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.views.MyRecyclerView
import kotlinx.android.synthetic.main.item_timer.view.*
import org.greenrobot.eventbus.EventBus
class TimerAdapter(
private val simpleActivity: SimpleActivity,
recyclerView: MyRecyclerView,
onRefresh: () -> Unit,
onItemClick: (Timer) -> Unit,
) : MyRecyclerViewListAdapter<Timer>(simpleActivity, recyclerView, diffUtil, null, onItemClick, onRefresh) {
companion object {
private val diffUtil = object : DiffUtil.ItemCallback<Timer>() {
override fun areItemsTheSame(oldItem: Timer, newItem: Timer): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Timer, newItem: Timer): Boolean {
return oldItem == newItem
}
}
}
init {
setupDragListener(true)
}
override fun getActionMenuId() = R.menu.cab_alarms
override fun prepareActionMode(menu: Menu) {}
override fun actionItemPressed(id: Int) {
if (selectedKeys.isEmpty()) {
return
}
when (id) {
R.id.cab_delete -> deleteItems()
}
}
override fun getSelectableItemCount() = itemCount
override fun getIsItemSelectable(position: Int) = true
override fun getItemSelectionKey(position: Int) = getItem(position).id
override fun getItemKeyPosition(key: Int): Int {
var position = -1
for (i in 0 until itemCount) {
if (key == getItem(i).id) {
position = i
break
}
}
return position
}
override fun onActionModeCreated() {}
override fun onActionModeDestroyed() {}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = createViewHolder(R.layout.item_timer, parent)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bindView(getItem(position), true, true) { itemView, _ ->
setupView(itemView, getItem(position))
}
bindViewHolder(holder)
}
private fun deleteItems() {
val positions = getSelectedItemPositions()
val timersToRemove = positions.map { position ->
getItem(position)
}
removeSelectedItems(positions)
timersToRemove.forEach(::deleteTimer)
}
private fun setupView(view: View, timer: Timer) {
view.apply {
val isSelected = selectedKeys.contains(timer.id)
timer_frame.isSelected = isSelected
timer_label.setTextColor(textColor)
timer_label.setHintTextColor(textColor.adjustAlpha(0.7f))
timer_label.text = timer.label
timer_time.setTextColor(textColor)
timer_time.text = when (timer.state) {
is TimerState.Finished -> 0.getFormattedDuration()
is TimerState.Idle -> timer.seconds.getFormattedDuration()
is TimerState.Paused -> timer.state.tick.getFormattedDuration()
is TimerState.Running -> timer.state.tick.getFormattedDuration()
}
timer_reset.applyColorFilter(textColor)
timer_reset.setOnClickListener {
resetTimer(timer)
}
timer_play_pause.applyColorFilter(textColor)
timer_play_pause.setOnClickListener {
when (val state = timer.state) {
is TimerState.Idle -> EventBus.getDefault().post(TimerEvent.Start(timer.id!!, timer.seconds.secondsToMillis))
is TimerState.Paused -> EventBus.getDefault().post(TimerEvent.Start(timer.id!!, state.tick))
is TimerState.Running -> EventBus.getDefault().post(TimerEvent.Pause(timer.id!!, state.tick))
is TimerState.Finished -> EventBus.getDefault().post(TimerEvent.Start(timer.id!!, timer.seconds.secondsToMillis))
}
}
val state = timer.state
val resetPossible = state is TimerState.Running || state is TimerState.Paused || state is TimerState.Finished
timer_reset.beInvisibleIf(!resetPossible)
val drawableId = if (state is TimerState.Running) R.drawable.ic_pause_vector else R.drawable.ic_play_vector
timer_play_pause.setImageDrawable(simpleActivity.resources.getColoredDrawableWithColor(drawableId, textColor))
}
}
private fun resetTimer(timer: Timer) {
EventBus.getDefault().post(TimerEvent.Reset(timer.id!!))
simpleActivity.hideTimerNotification(timer.id!!)
}
private fun deleteTimer(timer: Timer) {
EventBus.getDefault().post(TimerEvent.Delete(timer.id!!))
simpleActivity.hideTimerNotification(timer.id!!)
}
}

View File

@ -53,4 +53,8 @@ class ViewPagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm) {
fun updateTimerTabAlarmSound(alarmSound: AlarmSound) {
(fragments[TAB_TIMER] as? TimerFragment)?.updateAlarmSound(alarmSound)
}
fun updateTimerPosition(timerId: Int) {
(fragments[TAB_TIMER] as? TimerFragment)?.updatePosition(timerId)
}
}

View File

@ -0,0 +1,66 @@
package com.simplemobiletools.clock.databases
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.sqlite.db.SupportSQLiteDatabase
import com.simplemobiletools.clock.extensions.config
import com.simplemobiletools.clock.helpers.Converters
import com.simplemobiletools.clock.interfaces.TimerDao
import com.simplemobiletools.clock.models.Timer
import java.util.concurrent.Executors
@Database(entities = [Timer::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun TimerDao(): TimerDao
companion object {
private var db: AppDatabase? = null
fun getInstance(context: Context): AppDatabase {
if (db == null) {
synchronized(AppDatabase::class) {
if (db == null) {
db = Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app.db")
.addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
insertDefaultTimer(context)
}
})
.build()
db!!.openHelper.setWriteAheadLoggingEnabled(true)
}
}
}
return db!!
}
private fun insertDefaultTimer(context: Context) {
Executors.newSingleThreadScheduledExecutor().execute {
val config = context.config
db!!.TimerDao().insertOrUpdateTimer(
Timer(
id = null,
seconds = config.timerSeconds,
state = config.timerState,
vibrate = config.timerVibrate,
soundUri = config.timerSoundUri,
soundTitle = config.timerSoundTitle,
label = config.timerLabel ?: "",
createdAt = System.currentTimeMillis(),
channelId = config.timerChannelId,
)
)
}
}
fun destroyInstance() {
db = null
}
}
}

View File

@ -0,0 +1,116 @@
package com.simplemobiletools.clock.dialogs
import android.media.AudioManager
import android.media.RingtoneManager
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.clock.R
import com.simplemobiletools.clock.activities.SimpleActivity
import com.simplemobiletools.clock.extensions.*
import com.simplemobiletools.clock.helpers.PICK_AUDIO_FILE_INTENT_ID
import com.simplemobiletools.clock.models.Timer
import com.simplemobiletools.commons.dialogs.SelectAlarmSoundDialog
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.models.AlarmSound
import kotlinx.android.synthetic.main.dialog_edit_timer.view.*
class EditTimerDialog(val activity: SimpleActivity, val timer: Timer, val callback: () -> Unit) {
private val view = activity.layoutInflater.inflate(R.layout.dialog_edit_timer, null)
private val textColor = activity.config.textColor
init {
restoreLastAlarm()
updateAlarmTime()
view.apply {
edit_timer_initial_time.colorLeftDrawable(textColor)
edit_timer_initial_time.text = timer.seconds.getFormattedDuration()
edit_timer_initial_time.setTextColor(textColor)
edit_timer_initial_time.setOnClickListener {
changeDuration(timer)
}
edit_timer_vibrate.colorLeftDrawable(textColor)
edit_timer_vibrate.isChecked = timer.vibrate
edit_timer_vibrate.setTextColor(textColor)
edit_timer_vibrate_holder.setOnClickListener {
edit_timer_vibrate.toggle()
timer.vibrate = edit_timer_vibrate.isChecked
timer.channelId = null
}
edit_timer_sound.colorLeftDrawable(textColor)
edit_timer_sound.text = timer.soundTitle
edit_timer_sound.setOnClickListener {
SelectAlarmSoundDialog(activity, timer.soundUri, AudioManager.STREAM_ALARM, PICK_AUDIO_FILE_INTENT_ID,
RingtoneManager.TYPE_ALARM, true,
onAlarmPicked = { sound ->
if (sound != null) {
updateAlarmSound(sound)
}
},
onAlarmSoundDeleted = { sound ->
if (timer.soundUri == sound.uri) {
val defaultAlarm = context.getDefaultAlarmSound(RingtoneManager.TYPE_ALARM)
updateAlarmSound(defaultAlarm)
}
context.checkAlarmsWithDeletedSoundUri(sound.uri)
})
}
edit_timer_label_image.applyColorFilter(textColor)
edit_timer_label.setText(timer.label)
}
AlertDialog.Builder(activity)
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.cancel, null)
.create().apply {
activity.setupDialogStuff(view, this) {
getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
timer.label = view.edit_timer_label.value
activity.timerHelper.insertOrUpdateTimer(timer){
activity.config.timerLastConfig = timer
callback()
dismiss()
}
}
}
}
}
private fun restoreLastAlarm() {
if (timer.id == null) {
activity.config.timerLastConfig?.let { lastConfig ->
timer.label = lastConfig.label
timer.seconds = lastConfig.seconds
timer.soundTitle = lastConfig.soundTitle
timer.soundUri = lastConfig.soundUri
timer.vibrate = lastConfig.vibrate
}
}
}
private fun updateAlarmTime() {
view.edit_timer_initial_time.text = activity.getFormattedTime(timer.seconds * 60, false, true)
}
private fun changeDuration(timer: Timer) {
MyTimePickerDialogDialog(activity, timer.seconds) { seconds ->
val timerSeconds = if (seconds <= 0) 10 else seconds
timer.seconds = timerSeconds
view.edit_timer_initial_time.text = timerSeconds.getFormattedDuration()
}
}
fun updateAlarmSound(alarmSound: AlarmSound) {
timer.soundTitle = alarmSound.title
timer.soundUri = alarmSound.uri
timer.channelId = null
view.edit_timer_sound.text = alarmSound.title
}
}

View File

@ -20,9 +20,12 @@ import com.simplemobiletools.clock.R
import com.simplemobiletools.clock.activities.ReminderActivity
import com.simplemobiletools.clock.activities.SnoozeReminderActivity
import com.simplemobiletools.clock.activities.SplashActivity
import com.simplemobiletools.clock.databases.AppDatabase
import com.simplemobiletools.clock.helpers.*
import com.simplemobiletools.clock.interfaces.TimerDao
import com.simplemobiletools.clock.models.Alarm
import com.simplemobiletools.clock.models.MyTimeZone
import com.simplemobiletools.clock.models.Timer
import com.simplemobiletools.clock.receivers.AlarmReceiver
import com.simplemobiletools.clock.receivers.DateTimeWidgetUpdateReceiver
import com.simplemobiletools.clock.receivers.HideAlarmReceiver
@ -39,6 +42,8 @@ import kotlin.math.pow
val Context.config: Config get() = Config.newInstance(applicationContext)
val Context.dbHelper: DBHelper get() = DBHelper.newInstance(applicationContext)
val Context.timerDb: TimerDao get() = AppDatabase.getInstance(applicationContext).TimerDao()
val Context.timerHelper: TimerHelper get() = TimerHelper(this)
fun Context.getFormattedDate(calendar: Calendar): String {
val dayOfWeek = (calendar.get(Calendar.DAY_OF_WEEK) + 5) % 7 // make sure index 0 means monday
@ -80,6 +85,10 @@ fun Context.createNewAlarm(timeInMinutes: Int, weekDays: Int): Alarm {
return Alarm(0, timeInMinutes, weekDays, false, false, defaultAlarmSound.title, defaultAlarmSound.uri, "")
}
fun Context.createNewTimer(): Timer {
return Timer(null, config.timerSeconds, config.timerState, config.timerVibrate, config.timerSoundUri, config.timerSoundTitle, config.timerLabel ?: "", System.currentTimeMillis(), config.timerChannelId, )
}
fun Context.scheduleNextAlarm(alarm: Alarm, showToast: Boolean) {
val calendar = Calendar.getInstance()
calendar.firstDayOfWeek = Calendar.MONDAY
@ -135,10 +144,11 @@ fun Context.getOpenAlarmTabIntent(): PendingIntent {
return PendingIntent.getActivity(this, OPEN_ALARMS_TAB_INTENT_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
fun Context.getOpenTimerTabIntent(): PendingIntent {
fun Context.getOpenTimerTabIntent(timerId: Int): PendingIntent {
val intent = getLaunchIntent() ?: Intent(this, SplashActivity::class.java)
intent.putExtra(OPEN_TAB, TAB_TIMER)
return PendingIntent.getActivity(this, TIMER_NOTIF_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT)
intent.putExtra(TIMER_ID, timerId)
return PendingIntent.getActivity(this, timerId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
fun Context.getAlarmIntent(alarm: Alarm): PendingIntent {
@ -157,10 +167,11 @@ fun Context.hideNotification(id: Int) {
manager.cancel(id)
}
fun Context.hideTimerNotification() = hideNotification(TIMER_NOTIF_ID)
fun Context.hideTimerNotification(timerId: Int) = hideNotification(timerId)
fun Context.updateWidgets() {
val widgetsCnt = AppWidgetManager.getInstance(applicationContext)?.getAppWidgetIds(ComponentName(applicationContext, MyWidgetDateTimeProvider::class.java)) ?: return
val widgetsCnt =
AppWidgetManager.getInstance(applicationContext)?.getAppWidgetIds(ComponentName(applicationContext, MyWidgetDateTimeProvider::class.java)) ?: return
if (widgetsCnt.isNotEmpty()) {
val ids = intArrayOf(R.xml.widget_date_time_info)
Intent(applicationContext, MyWidgetDateTimeProvider::class.java).apply {
@ -173,7 +184,8 @@ fun Context.updateWidgets() {
@SuppressLint("NewApi")
fun Context.scheduleNextWidgetUpdate() {
val widgetsCnt = AppWidgetManager.getInstance(applicationContext)?.getAppWidgetIds(ComponentName(applicationContext, MyWidgetDateTimeProvider::class.java)) ?: return
val widgetsCnt =
AppWidgetManager.getInstance(applicationContext)?.getAppWidgetIds(ComponentName(applicationContext, MyWidgetDateTimeProvider::class.java)) ?: return
if (widgetsCnt.isEmpty()) {
return
}
@ -256,8 +268,8 @@ fun Context.showAlarmNotification(alarm: Alarm) {
}
@SuppressLint("NewApi")
fun Context.getTimerNotification(pendingIntent: PendingIntent, addDeleteIntent: Boolean): Notification {
var soundUri = config.timerSoundUri
fun Context.getTimerNotification(timer: Timer, pendingIntent: PendingIntent, addDeleteIntent: Boolean): Notification {
var soundUri = timer.soundUri
if (soundUri == SILENT) {
soundUri = ""
} else {
@ -265,8 +277,8 @@ fun Context.getTimerNotification(pendingIntent: PendingIntent, addDeleteIntent:
}
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val channelId = config.timerChannelId ?: "simple_timer_channel_${soundUri}_${System.currentTimeMillis()}"
config.timerChannelId = channelId
val channelId = timer.channelId ?: "simple_timer_channel_${soundUri}_${System.currentTimeMillis()}"
timerHelper.insertOrUpdateTimer(timer.copy(channelId = channelId))
if (isOreoPlus()) {
try {
@ -288,7 +300,7 @@ fun Context.getTimerNotification(pendingIntent: PendingIntent, addDeleteIntent:
lightColor = getAdjustedPrimaryColor()
setSound(Uri.parse(soundUri), audioAttributes)
if (!config.timerVibrate) {
if (!timer.vibrate) {
vibrationPattern = longArrayOf(0L)
}
@ -299,7 +311,7 @@ fun Context.getTimerNotification(pendingIntent: PendingIntent, addDeleteIntent:
val reminderActivityIntent = getReminderActivityIntent()
val builder = NotificationCompat.Builder(this)
.setContentTitle(getString(R.string.timer))
.setContentTitle(if(timer.label.isEmpty()) getString(R.string.timer) else timer.label)
.setContentText(getString(R.string.time_expired))
.setSmallIcon(R.drawable.ic_timer)
.setContentIntent(pendingIntent)
@ -309,7 +321,7 @@ fun Context.getTimerNotification(pendingIntent: PendingIntent, addDeleteIntent:
.setAutoCancel(true)
.setSound(Uri.parse(soundUri), STREAM_ALARM)
.setChannelId(channelId)
.addAction(R.drawable.ic_cross_vector, getString(R.string.dismiss), if (addDeleteIntent) reminderActivityIntent else getHideTimerPendingIntent())
.addAction(R.drawable.ic_cross_vector, getString(R.string.dismiss), if (addDeleteIntent) reminderActivityIntent else getHideTimerPendingIntent(timer.id!!))
if (addDeleteIntent) {
builder.setDeleteIntent(reminderActivityIntent)
@ -317,7 +329,7 @@ fun Context.getTimerNotification(pendingIntent: PendingIntent, addDeleteIntent:
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
if (config.timerVibrate) {
if (timer.vibrate) {
val vibrateArray = LongArray(2) { 500 }
builder.setVibrate(vibrateArray)
}
@ -327,9 +339,10 @@ fun Context.getTimerNotification(pendingIntent: PendingIntent, addDeleteIntent:
return notification
}
fun Context.getHideTimerPendingIntent(): PendingIntent {
fun Context.getHideTimerPendingIntent(timerId: Int): PendingIntent {
val intent = Intent(this, HideTimerReceiver::class.java)
return PendingIntent.getBroadcast(this, TIMER_NOTIF_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT)
intent.putExtra(TIMER_ID, timerId)
return PendingIntent.getBroadcast(this, timerId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
fun Context.getHideAlarmPendingIntent(alarm: Alarm): PendingIntent {

View File

@ -1,8 +1,10 @@
package com.simplemobiletools.clock.extensions
import android.text.format.DateFormat
import com.simplemobiletools.commons.extensions.getFormattedDuration
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt
fun Long.formatStopwatchTime(useLongerMSFormat: Boolean): String {
val MSFormat = if (useLongerMSFormat) "%03d" else "%01d"
@ -37,5 +39,9 @@ fun Long.timestampFormat(format: String = "dd. MM. yyyy"): String {
return DateFormat.format(format, calendar).toString()
}
fun Long.getFormattedDuration(forceShowHours: Boolean = false): String {
return this.div(1000F).roundToInt().getFormattedDuration(forceShowHours)
}
val Long.secondsToMillis get() = TimeUnit.SECONDS.toMillis(this)
val Long.millisToSeconds get() = TimeUnit.MILLISECONDS.toSeconds(this)

View File

@ -7,11 +7,8 @@ import com.simplemobiletools.clock.models.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)
.registerSubtype(TimerState.Finished::class.java)
inline fun <reified T : Any> valueOf(): RuntimeTypeAdapterFactory<T> = RuntimeTypeAdapterFactory.of(T::class.java)

View File

@ -1,8 +1,5 @@
package com.simplemobiletools.clock.fragments
import android.graphics.Color
import android.media.AudioManager
import android.media.RingtoneManager
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@ -10,22 +7,31 @@ import android.view.ViewGroup
import androidx.fragment.app.Fragment
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.PICK_AUDIO_FILE_INTENT_ID
import com.simplemobiletools.clock.models.TimerState
import com.simplemobiletools.commons.dialogs.SelectAlarmSoundDialog
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.clock.adapters.TimerAdapter
import com.simplemobiletools.clock.dialogs.EditTimerDialog
import com.simplemobiletools.clock.extensions.config
import com.simplemobiletools.clock.extensions.createNewTimer
import com.simplemobiletools.clock.extensions.timerHelper
import com.simplemobiletools.clock.helpers.DisabledItemChangeAnimator
import com.simplemobiletools.clock.models.Timer
import com.simplemobiletools.clock.models.TimerEvent
import com.simplemobiletools.commons.extensions.hideKeyboard
import com.simplemobiletools.commons.extensions.updateTextColors
import com.simplemobiletools.commons.models.AlarmSound
import kotlinx.android.synthetic.main.fragment_timer.view.*
import kotlinx.android.synthetic.main.fragment_timer.timer_fragment
import kotlinx.android.synthetic.main.fragment_timer.view.timer_add
import kotlinx.android.synthetic.main.fragment_timer.view.timers_list
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import kotlin.math.roundToInt
class TimerFragment : Fragment() {
lateinit var view: ViewGroup
private val INVALID_POSITION = -1
private lateinit var view: ViewGroup
private lateinit var timerAdapter: TimerAdapter
private var timerPositionToScrollTo = INVALID_POSITION
private var storedTextColor = 0
private var currentEditAlarmDialog: EditTimerDialog? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -39,151 +45,90 @@ class TimerFragment : Fragment() {
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 = config.timerSeconds.getFormattedDuration()
timer_label.setText(config.timerLabel)
activity?.updateTextColors(timer_fragment)
timer_play_pause.background = resources.getColoredDrawableWithColor(R.drawable.circle_background_filled, requireContext().getAdjustedPrimaryColor())
timer_play_pause.applyColorFilter(if (requireContext().getAdjustedPrimaryColor() == Color.WHITE) Color.BLACK else Color.WHITE)
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 {
stopTimer()
}
timer_play_pause.setOnClickListener {
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 -> {
}
storeStateVariables()
timers_list.itemAnimator = DisabledItemChangeAnimator()
timer_add.setOnClickListener {
activity?.run {
hideKeyboard()
openEditTimer(createNewTimer())
}
}
timer_reset.setOnClickListener {
stopTimer()
}
timer_time.setOnClickListener {
changeDuration()
}
timer_initial_time.setOnClickListener {
changeDuration()
}
timer_vibrate_holder.setOnClickListener {
timer_vibrate.toggle()
config.timerVibrate = timer_vibrate.isChecked
config.timerChannelId = null
}
timer_sound.setOnClickListener {
SelectAlarmSoundDialog(activity as SimpleActivity, config.timerSoundUri, AudioManager.STREAM_ALARM, PICK_AUDIO_FILE_INTENT_ID,
RingtoneManager.TYPE_ALARM, true,
onAlarmPicked = { sound ->
if (sound != null) {
updateAlarmSound(sound)
}
},
onAlarmSoundDeleted = { sound ->
if (config.timerSoundUri == sound.uri) {
val defaultAlarm = context.getDefaultAlarmSound(RingtoneManager.TYPE_ALARM)
updateAlarmSound(defaultAlarm)
}
context.checkAlarmsWithDeletedSoundUri(sound.uri)
})
}
timer_label.onTextChangeListener { text ->
config.timerLabel = text
}
}
initAdapter()
refreshTimers()
return view
}
private fun stopTimer() {
EventBus.getDefault().post(TimerState.Idle)
activity?.hideTimerNotification()
view.timer_time.text = activity?.config?.timerSeconds?.getFormattedDuration()
private fun initAdapter() {
timerAdapter = TimerAdapter(requireActivity() as SimpleActivity, view.timers_list, ::refreshTimers, ::openEditTimer)
view.timers_list.adapter = timerAdapter
}
private fun changeDuration() {
MyTimePickerDialogDialog(activity as SimpleActivity, requireContext().config.timerSeconds) { seconds ->
val timerSeconds = if (seconds <= 0) 10 else seconds
activity?.config?.timerSeconds = timerSeconds
val duration = timerSeconds.getFormattedDuration()
view.timer_initial_time.text = duration
override fun onResume() {
super.onResume()
requireContext().updateTextColors(timer_fragment)
val configTextColor = requireContext().config.textColor
if (storedTextColor != configTextColor) {
initAdapter()
timerAdapter.updateTextColor(configTextColor)
refreshTimers()
}
}
if (view.timer_reset.isGone()) {
stopTimer()
override fun onPause() {
super.onPause()
storeStateVariables()
}
private fun refreshTimers(scrollToLatest: Boolean = false) {
activity?.timerHelper?.getTimers { timers ->
timerAdapter.submitList(timers) {
view.timers_list.post {
if (getView() != null) {
if (timerPositionToScrollTo != INVALID_POSITION && timerAdapter.itemCount > timerPositionToScrollTo) {
view.timers_list.scrollToPosition(timerPositionToScrollTo)
timerPositionToScrollTo = INVALID_POSITION
} else if (scrollToLatest) {
view.timers_list.scrollToPosition(timers.lastIndex)
}
}
}
}
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onMessageEvent(state: TimerState.Idle) {
view.timer_time.text = requiredActivity.config.timerSeconds.getFormattedDuration()
updateViewStates(state)
private fun storeStateVariables() {
storedTextColor = requireContext().config.textColor
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onMessageEvent(state: TimerState.Running) {
view.timer_time.text = state.tick.div(1000F).roundToInt().getFormattedDuration()
updateViewStates(state)
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onMessageEvent(state: TimerState.Paused) {
updateViewStates(state)
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onMessageEvent(state: TimerState.Finished) {
view.timer_time.text = 0.getFormattedDuration()
updateViewStates(state)
}
private fun updateViewStates(state: TimerState) {
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 (activity?.getAdjustedPrimaryColor() == Color.WHITE) {
Color.BLACK
} else {
Color.WHITE
}
view.timer_play_pause.setImageDrawable(resources.getColoredDrawableWithColor(drawableId, iconColor))
fun onMessageEvent(event: TimerEvent.Refresh) {
refreshTimers()
}
fun updateAlarmSound(alarmSound: AlarmSound) {
activity?.config?.timerSoundTitle = alarmSound.title
activity?.config?.timerSoundUri = alarmSound.uri
view.timer_sound.text = alarmSound.title
currentEditAlarmDialog?.updateAlarmSound(alarmSound)
}
fun updatePosition(timerId: Int) {
activity?.timerHelper?.getTimers { timers ->
val position = timers.indexOfFirst { it.id == timerId }
if (position != INVALID_POSITION) {
activity?.runOnUiThread {
if (timerAdapter.itemCount > position) {
view.timers_list.scrollToPosition(position)
} else {
timerPositionToScrollTo = position
}
}
}
}
}
private fun openEditTimer(timer: Timer) {
currentEditAlarmDialog = EditTimerDialog(activity as SimpleActivity, timer) {
currentEditAlarmDialog = null
refreshTimers()
}
}
}

View File

@ -5,6 +5,7 @@ import android.media.RingtoneManager
import com.simplemobiletools.clock.extensions.gson.gson
import com.simplemobiletools.clock.models.Alarm
import com.simplemobiletools.clock.models.StateWrapper
import com.simplemobiletools.clock.models.Timer
import com.simplemobiletools.clock.models.TimerState
import com.simplemobiletools.commons.extensions.getDefaultAlarmSound
import com.simplemobiletools.commons.extensions.getDefaultAlarmTitle
@ -79,6 +80,12 @@ class Config(context: Context) : BaseConfig(context) {
}
set(alarm) = prefs.edit().putString(ALARM_LAST_CONFIG, gson.toJson(alarm)).apply()
var timerLastConfig: Timer?
get() = prefs.getString(TIMER_LAST_CONFIG, null)?.let { lastAlarm ->
gson.fromJson(lastAlarm, Timer::class.java)
}
set(alarm) = prefs.edit().putString(TIMER_LAST_CONFIG, gson.toJson(alarm)).apply()
var timerChannelId: String?
get() = prefs.getString(TIMER_CHANNEL_ID, null)
set(id) = prefs.edit().putString(TIMER_CHANNEL_ID, id).apply()

View File

@ -1,7 +1,9 @@
package com.simplemobiletools.clock.helpers
import com.simplemobiletools.clock.models.MyTimeZone
import java.util.*
import java.util.Calendar
import java.util.Date
import java.util.TimeZone
import kotlin.math.pow
// shared preferences
@ -18,6 +20,7 @@ const val TIMER_LABEL = "timer_label"
const val TIMER_MAX_REMINDER_SECS = "timer_max_reminder_secs"
const val ALARM_MAX_REMINDER_SECS = "alarm_max_reminder_secs"
const val ALARM_LAST_CONFIG = "alarm_last_config"
const val TIMER_LAST_CONFIG = "timer_last_config"
const val USE_TEXT_SHADOW = "use_text_shadow"
const val INCREASE_VOLUME_GRADUALLY = "increase_volume_gradually"
const val ALARMS_SORT_BY = "alarms_sort_by"
@ -35,7 +38,6 @@ const val OPEN_ALARMS_TAB_INTENT_ID = 9996
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"
@ -43,6 +45,8 @@ const val TAB_CLOCK = 0
const val TAB_ALARM = 1
const val TAB_STOPWATCH = 2
const val TAB_TIMER = 3
const val TIMER_ID = "timer_id"
const val INVALID_TIMER_ID = -1
// stopwatch sorting
const val SORT_BY_LAP = 1
@ -56,6 +60,8 @@ const val SORT_BY_ALARM_TIME = 1
const val TODAY_BIT = -1
const val TOMORROW_BIT = -2
const val DEFAULT_TIME = 300
fun getDefaultTimeZoneTitle(id: Int) = getAllTimeZones().firstOrNull { it.id == id }?.title ?: ""
fun getMSTillNextMinute(): Long {

View File

@ -0,0 +1,21 @@
package com.simplemobiletools.clock.helpers
import androidx.room.TypeConverter
import com.simplemobiletools.clock.extensions.gson.gson
import com.simplemobiletools.clock.models.StateWrapper
import com.simplemobiletools.clock.models.TimerState
class Converters {
@TypeConverter
fun jsonToTimerState(value: String): TimerState {
return try {
gson.fromJson(value, StateWrapper::class.java).state
} catch (e: Exception) {
TimerState.Idle
}
}
@TypeConverter
fun timerStateToJson(state: TimerState) = gson.toJson(StateWrapper(state))
}

View File

@ -0,0 +1,39 @@
package com.simplemobiletools.clock.helpers
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.RecyclerView
/**
* Simple RecyclerView animator that disable itemChange animations
*/
class DisabledItemChangeAnimator : DefaultItemAnimator() {
override fun animateChange(
oldHolder: RecyclerView.ViewHolder,
newHolder: RecyclerView.ViewHolder,
preInfo: ItemHolderInfo,
postInfo: ItemHolderInfo
): Boolean {
dispatchChangeFinished(oldHolder, false)
return false
}
override fun animateChange(
oldHolder: RecyclerView.ViewHolder?,
newHolder: RecyclerView.ViewHolder?,
fromX: Int,
fromY: Int,
toX: Int,
toY: Int
): Boolean {
dispatchChangeFinished(oldHolder, false)
return false
}
override fun canReuseUpdatedViewHolder(viewHolder: RecyclerView.ViewHolder): Boolean {
return true
}
override fun canReuseUpdatedViewHolder(viewHolder: RecyclerView.ViewHolder, payloads: MutableList<Any>): Boolean {
return true
}
}

View File

@ -0,0 +1,43 @@
package com.simplemobiletools.clock.helpers
import android.content.Context
import com.simplemobiletools.clock.extensions.timerDb
import com.simplemobiletools.clock.models.Timer
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
class TimerHelper(val context: Context) {
private val timerDao = context.timerDb
fun getTimers(callback: (timers: List<Timer>) -> Unit) {
ensureBackgroundThread {
callback.invoke(timerDao.getTimers())
}
}
fun getTimer(timerId: Int, callback: (timer: Timer) -> Unit) {
ensureBackgroundThread {
callback.invoke(timerDao.getTimer(timerId))
}
}
fun insertOrUpdateTimer(timer: Timer, callback: () -> Unit = {}) {
ensureBackgroundThread {
timerDao.insertOrUpdateTimer(timer)
callback.invoke()
}
}
fun deleteTimer(id: Int, callback: () -> Unit = {}) {
ensureBackgroundThread {
timerDao.deleteTimer(id)
callback.invoke()
}
}
fun deleteTimers(timers: List<Timer>, callback: () -> Unit = {}) {
ensureBackgroundThread {
timerDao.deleteTimers(timers)
callback.invoke()
}
}
}

View File

@ -0,0 +1,23 @@
package com.simplemobiletools.clock.interfaces
import androidx.room.*
import com.simplemobiletools.clock.models.Timer
@Dao
interface TimerDao {
@Query("SELECT * FROM timers ORDER BY createdAt ASC")
fun getTimers(): List<Timer>
@Query("SELECT * FROM timers WHERE id=:id")
fun getTimer(id: Int): Timer
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertOrUpdateTimer(timer: Timer): Long
@Query("DELETE FROM timers WHERE id=:id")
fun deleteTimer(id: Int)
@Delete
fun deleteTimers(list: List<Timer>)
}

View File

@ -0,0 +1,17 @@
package com.simplemobiletools.clock.models
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "timers")
data class Timer(
@PrimaryKey(autoGenerate = true) var id: Int?,
var seconds: Int,
val state: TimerState,
var vibrate: Boolean,
var soundUri: String,
var soundTitle: String,
var label: String,
var createdAt: Long,
var channelId: String? = null,
)

View File

@ -0,0 +1,12 @@
package com.simplemobiletools.clock.models
import com.simplemobiletools.clock.helpers.INVALID_TIMER_ID
sealed class TimerEvent(open val timerId: Int) {
data class Delete(override val timerId: Int) : TimerEvent(timerId)
data class Reset(override val timerId: Int) : TimerEvent(timerId)
data class Start(override val timerId: Int, val duration: Long) : TimerEvent(timerId)
data class Pause(override val timerId: Int, val duration: Long) : TimerEvent(timerId)
data class Finish(override val timerId: Int, val duration: Long) : TimerEvent(timerId)
object Refresh : TimerEvent(INVALID_TIMER_ID)
}

View File

@ -2,10 +2,7 @@ package com.simplemobiletools.clock.models
sealed class TimerState {
object Idle : TimerState()
data class Start(val duration: Long) : TimerState()
data class Running(val duration: Long, val tick: Long) : TimerState()
data class Pause(val duration: Long) : TimerState()
data class Paused(val duration: Long, val tick: Long) : TimerState()
data class Finish(val duration: Long) : TimerState()
object Finished : TimerState()
}

View File

@ -4,12 +4,15 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.simplemobiletools.clock.extensions.hideTimerNotification
import com.simplemobiletools.clock.models.TimerState
import com.simplemobiletools.clock.helpers.INVALID_TIMER_ID
import com.simplemobiletools.clock.helpers.TIMER_ID
import com.simplemobiletools.clock.models.TimerEvent
import org.greenrobot.eventbus.EventBus
class HideTimerReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
context.hideTimerNotification()
EventBus.getDefault().post(TimerState.Idle)
val timerId = intent.getIntExtra(TIMER_ID, INVALID_TIMER_ID)
context.hideTimerNotification(timerId)
EventBus.getDefault().post(TimerEvent.Reset(timerId, ))
}
}

View File

@ -12,18 +12,21 @@ 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.getFormattedDuration
import com.simplemobiletools.clock.extensions.getOpenTimerTabIntent
import com.simplemobiletools.clock.extensions.timerHelper
import com.simplemobiletools.clock.helpers.INVALID_TIMER_ID
import com.simplemobiletools.clock.helpers.TIMER_RUNNING_NOTIF_ID
import com.simplemobiletools.commons.extensions.getFormattedDuration
import com.simplemobiletools.clock.models.TimerEvent
import com.simplemobiletools.clock.models.TimerState
import com.simplemobiletools.commons.helpers.isOreoPlus
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
class TimerService : Service() {
private val bus = EventBus.getDefault()
private var isStopping = false
override fun onCreate() {
super.onCreate()
@ -34,19 +37,43 @@ class TimerService : Service() {
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))
isStopping = false
updateNotification()
startForeground(TIMER_RUNNING_NOTIF_ID, notification(getString(R.string.app_name), getString(R.string.timers_notification_msg), INVALID_TIMER_ID))
return START_NOT_STICKY
}
private fun updateNotification() {
timerHelper.getTimers { timers ->
val runningTimers = timers.filter { it.state is TimerState.Running }
if (runningTimers.isNotEmpty()) {
val firstTimer = runningTimers.first()
val formattedDuration = (firstTimer.state as TimerState.Running).tick.getFormattedDuration()
val contextText = when {
firstTimer.label.isNotEmpty() -> getString(R.string.timer_single_notification_label_msg, firstTimer.label)
else -> resources.getQuantityString(R.plurals.timer_notification_msg, runningTimers.size, runningTimers.size)
}
startForeground(TIMER_RUNNING_NOTIF_ID, notification(formattedDuration, contextText, firstTimer.id!!))
} else {
stopService()
}
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onMessageEvent(event: TimerStopService) {
stopService()
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onMessageEvent(event: TimerEvent.Refresh) {
if(!isStopping){
updateNotification()
}
}
private fun stopService() {
isStopping = true
if (isOreoPlus()) {
stopForeground(true)
} else {
@ -60,7 +87,7 @@ class TimerService : Service() {
}
@TargetApi(Build.VERSION_CODES.O)
private fun notification(formattedDuration: String): Notification {
private fun notification(title: String, contentText: String, firstRunningTimerId: Int): Notification {
val channelId = "simple_alarm_timer"
val label = getString(R.string.timer)
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@ -73,15 +100,18 @@ class TimerService : Service() {
}
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)
.setContentTitle(title)
.setContentText(contentText)
.setSmallIcon(R.drawable.ic_timer)
.setPriority(Notification.PRIORITY_DEFAULT)
.setSound(null)
.setOngoing(true)
.setAutoCancel(true)
.setChannelId(channelId)
if (firstRunningTimerId != INVALID_TIMER_ID) {
builder.setContentIntent(this.getOpenTimerTabIntent(firstRunningTimerId))
}
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
return builder.build()

View File

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/edit_timer_scrollview"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/edit_timer_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/activity_margin">
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/edit_timer_initial_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:drawableLeft="@drawable/ic_timer"
android:drawablePadding="@dimen/normal_margin"
android:padding="@dimen/activity_margin"
android:textSize="@dimen/bigger_text_size"
app:layout_constraintTop_toTopOf="parent"
tools:text="05:00" />
<RelativeLayout
android:id="@+id/edit_timer_vibrate_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:padding="@dimen/activity_margin"
app:layout_constraintTop_toBottomOf="@+id/edit_timer_initial_time">
<com.simplemobiletools.commons.views.MySwitchCompat
android:id="@+id/edit_timer_vibrate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@null"
android:clickable="false"
android:drawableLeft="@drawable/ic_vibrate_vector"
android:drawablePadding="@dimen/normal_margin"
android:text="@string/vibrate"
android:textSize="@dimen/bigger_text_size" />
</RelativeLayout>
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/edit_timer_sound"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:drawableLeft="@drawable/ic_bell_vector"
android:drawablePadding="@dimen/normal_margin"
android:padding="@dimen/activity_margin"
android:textSize="@dimen/bigger_text_size"
app:layout_constraintTop_toBottomOf="@+id/edit_timer_vibrate_holder"
tools:text="Default Alarm" />
<LinearLayout
android:id="@+id/edit_timer_label_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="@dimen/activity_margin"
android:paddingTop="@dimen/medium_margin"
android:paddingEnd="@dimen/activity_margin"
android:paddingBottom="@dimen/medium_margin"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/edit_timer_sound"
app:layout_constraintVertical_bias="0"
app:layout_constraintVertical_chainStyle="spread_inside">
<ImageView
android:id="@+id/edit_timer_label_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_label_vector" />
<com.simplemobiletools.commons.views.MyEditText
android:id="@+id/edit_timer_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:hint="@string/label"
android:maxLines="1"
android:singleLine="true"
android:textCursorDrawable="@null"
android:textSize="@dimen/normal_text_size" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -6,141 +6,32 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:id="@+id/timer_scroll"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/timer_play_pause"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/timer_constraint"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/timer_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/normal_margin"
android:background="?attr/selectableItemBackground"
android:gravity="center_horizontal"
android:padding="@dimen/small_margin"
android:textSize="@dimen/stopwatch_text_size"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="00:00" />
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/timer_initial_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/activity_margin"
android:background="?attr/selectableItemBackground"
android:drawableLeft="@drawable/ic_timer"
android:drawablePadding="@dimen/normal_margin"
android:padding="@dimen/activity_margin"
android:textSize="@dimen/bigger_text_size"
app:layout_constraintTop_toBottomOf="@+id/timer_time"
tools:text="05:00" />
<RelativeLayout
android:id="@+id/timer_vibrate_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:padding="@dimen/activity_margin"
app:layout_constraintTop_toBottomOf="@+id/timer_initial_time">
<com.simplemobiletools.commons.views.MySwitchCompat
android:id="@+id/timer_vibrate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@null"
android:clickable="false"
android:drawableLeft="@drawable/ic_vibrate_vector"
android:drawablePadding="@dimen/normal_margin"
android:text="@string/vibrate"
android:textSize="@dimen/bigger_text_size" />
</RelativeLayout>
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/timer_sound"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:drawableLeft="@drawable/ic_bell_vector"
android:drawablePadding="@dimen/normal_margin"
android:padding="@dimen/activity_margin"
android:textSize="@dimen/bigger_text_size"
app:layout_constraintTop_toBottomOf="@+id/timer_vibrate_holder"
tools:text="Default alarm" />
<LinearLayout
android:id="@+id/timer_label_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="@dimen/activity_margin"
android:paddingTop="@dimen/medium_margin"
android:paddingEnd="@dimen/activity_margin"
android:paddingBottom="@dimen/medium_margin"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/timer_sound">
<ImageView
android:id="@+id/timer_label_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_label_vector" />
<com.simplemobiletools.commons.views.MyEditText
android:id="@+id/timer_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:hint="@string/label"
android:maxLines="1"
android:singleLine="true"
android:textCursorDrawable="@null"
android:textSize="@dimen/normal_text_size" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
<ImageView
android:id="@+id/timer_play_pause"
android:layout_width="@dimen/stopwatch_button_size"
android:layout_height="@dimen/stopwatch_button_size"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="@dimen/big_margin"
android:padding="@dimen/activity_margin"
android:src="@drawable/ic_play_vector"
<com.simplemobiletools.commons.views.MyRecyclerView
android:id="@+id/timers_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/fab_list_bottom_padding"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<ImageView
android:id="@+id/timer_reset"
android:layout_width="@dimen/stopwatch_button_small_size"
android:layout_height="@dimen/stopwatch_button_small_size"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="@dimen/normal_margin"
android:src="@drawable/ic_reset_vector"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/timer_play_pause"
app:layout_constraintEnd_toStartOf="@+id/timer_play_pause"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/timer_play_pause"
tools:visibility="visible" />
app:layout_constraintTop_toTopOf="parent"
tools:itemCount="3"
tools:listitem="@layout/item_timer" />
<com.simplemobiletools.commons.views.MyFloatingActionButton
android:id="@+id/timer_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/activity_margin"
android:contentDescription="@string/new_timer"
android:src="@drawable/ic_plus_vector"
app:backgroundTint="@color/color_primary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:rippleColor="@color/pressed_item_foreground" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/timer_frame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:foreground="@drawable/selector">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="@dimen/activity_margin"
android:paddingTop="@dimen/medium_margin"
android:paddingBottom="@dimen/medium_margin">
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/timer_time"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:includeFontPadding="false"
android:textSize="@dimen/alarm_text_size"
app:layout_constraintEnd_toStartOf="@id/timer_reset"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="00:00" />
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/timer_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="1"
android:textSize="@dimen/bigger_text_size"
app:layout_constraintEnd_toEndOf="@id/timer_time"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/timer_time"
tools:text="Cook rice" />
<ImageView
android:id="@+id/timer_reset"
android:layout_width="@dimen/timer_button_small_size"
android:layout_height="@dimen/timer_button_small_size"
android:layout_marginStart="@dimen/bigger_margin"
android:layout_marginEnd="@dimen/bigger_margin"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="@dimen/normal_margin"
android:src="@drawable/ic_reset_vector"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="@+id/timer_play_pause"
app:layout_constraintEnd_toStartOf="@+id/timer_play_pause"
app:layout_constraintStart_toEndOf="@id/timer_time"
app:layout_constraintTop_toTopOf="@id/timer_play_pause" />
<ImageView
android:id="@+id/timer_play_pause"
android:layout_width="@dimen/timer_button_size"
android:layout_height="@dimen/timer_button_size"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginStart="@dimen/bigger_margin"
android:layout_marginEnd="@dimen/bigger_margin"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="@dimen/activity_margin"
android:src="@drawable/ic_play_vector"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/timer_reset"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

View File

@ -17,6 +17,16 @@
<string name="sort_by_creation_order">Creation order</string>
<string name="sort_by_alarm_time">Alarm time</string>
<!--Timer-->
<string name="timers_notification_msg">Timers are running</string>
<string name="timer_single_notification_label_msg">Timer for %s is running</string>
<string name="new_timer">New Timer</string>
<plurals name="timer_notification_msg">
<item quantity="one">%d timer is running</item>
<item quantity="other">%d timers are running</item>
</plurals>
<!-- Settings -->
<string name="clock_tab">Saat bölməsi</string>
<string name="alarm_tab">Siqnal bölməsi</string>

View File

@ -17,6 +17,17 @@
<string name="sort_by_creation_order">Creation order</string>
<string name="sort_by_alarm_time">Alarm time</string>
<!--Timer-->
<string name="timers_notification_msg">Timers are running</string>
<string name="timer_single_notification_label_msg">Timer for %s is running</string>
<string name="new_timer">New Timer</string>
<plurals name="timer_notification_msg">
<item quantity="one">%d timer is running</item>
<item quantity="few">%d timers are running</item>
<item quantity="other">%d timers are running</item>
</plurals>
<!-- Settings -->
<string name="clock_tab">Záložka hodin</string>
<string name="alarm_tab">Záložka budíku</string>

View File

@ -17,6 +17,19 @@
<string name="sort_by_creation_order">Creation order</string>
<string name="sort_by_alarm_time">Alarm time</string>
<!--Timer-->
<string name="timers_notification_msg">Timers are running</string>
<string name="timer_single_notification_label_msg">Timer for %s is running</string>
<string name="new_timer">New Timer</string>
<plurals name="timer_notification_msg">
<item quantity="one">%d timer is running</item>
<item quantity="two">%d timers are running</item>
<item quantity="few">%d timers are running</item>
<item quantity="many">%d timers are running</item>
<item quantity="other">%d timers are running</item>
</plurals>
<!-- Settings -->
<string name="clock_tab">Tab cloc</string>
<string name="alarm_tab">Tab larwm</string>

View File

@ -17,6 +17,16 @@
<string name="sort_by_creation_order">Creation order</string>
<string name="sort_by_alarm_time">Alarm time</string>
<!--Timer-->
<string name="timers_notification_msg">Timers are running</string>
<string name="timer_single_notification_label_msg">Timer for %s is running</string>
<string name="new_timer">New Timer</string>
<plurals name="timer_notification_msg">
<item quantity="one">%d timer is running</item>
<item quantity="other">%d timers are running</item>
</plurals>
<!-- Settings -->
<string name="clock_tab">Ur</string>
<string name="alarm_tab">Alarm</string>

View File

@ -17,6 +17,16 @@
<string name="sort_by_creation_order">Creation order</string>
<string name="sort_by_alarm_time">Alarm time</string>
<!--Timer-->
<string name="timers_notification_msg">Timers are running</string>
<string name="timer_single_notification_label_msg">Timer for %s is running</string>
<string name="new_timer">New Timer</string>
<plurals name="timer_notification_msg">
<item quantity="one">%d timer is running</item>
<item quantity="other">%d timers are running</item>
</plurals>
<!-- Settings -->
<string name="clock_tab">Uhr</string>
<string name="alarm_tab">Wecker</string>

View File

@ -18,6 +18,16 @@
<string name="sort_by_creation_order">Creation order</string>
<string name="sort_by_alarm_time">Alarm time</string>
<!--Timer-->
<string name="timers_notification_msg">Timers are running</string>
<string name="timer_single_notification_label_msg">Timer for %s is running</string>
<string name="new_timer">New Timer</string>
<plurals name="timer_notification_msg">
<item quantity="one">%d timer is running</item>
<item quantity="other">%d timers are running</item>
</plurals>
<!-- Settings -->
<string name="clock_tab">Ετικέτα Ρολογιού</string>
<string name="alarm_tab">Ετικέτα Αφύπνισης</string>

View File

@ -17,6 +17,16 @@
<string name="sort_by_creation_order">Creation order</string>
<string name="sort_by_alarm_time">Alarm time</string>
<!--Timer-->
<string name="timers_notification_msg">Timers are running</string>
<string name="timer_single_notification_label_msg">Timer for %s is running</string>
<string name="new_timer">New Timer</string>
<plurals name="timer_notification_msg">
<item quantity="one">%d timer is running</item>
<item quantity="other">%d timers are running</item>
</plurals>
<!-- Settings -->
<string name="clock_tab">Pestaña de reloj</string>
<string name="alarm_tab">Pestaña de alarma</string>

View File

@ -17,6 +17,16 @@
<string name="sort_by_creation_order">Creation order</string>
<string name="sort_by_alarm_time">Alarm time</string>
<!--Timer-->
<string name="timers_notification_msg">Timers are running</string>
<string name="timer_single_notification_label_msg">Timer for %s is running</string>
<string name="new_timer">New Timer</string>
<plurals name="timer_notification_msg">
<item quantity="one">%d timer is running</item>
<item quantity="other">%d timers are running</item>
</plurals>
<!-- Settings -->
<string name="clock_tab">Erloju fitxa</string>
<string name="alarm_tab">Alarma fitxa</string>

View File

@ -16,6 +16,16 @@
<string name="swipe_right_to_dismiss">Pyyhkäise oikealle sammuttaaksesi tai vasemmalle torkuttaaksesi.</string>
<string name="sort_by_creation_order">Luontijärjestys</string>
<string name="sort_by_alarm_time">Herätysaika</string>
 
<!--Timer-->
<string name="timers_notification_msg">Timers are running</string>
<string name="timer_single_notification_label_msg">Timer for %s is running</string>
<string name="new_timer">New Timer</string>
<plurals name="timer_notification_msg">
<item quantity="one">%d timer is running</item>
<item quantity="other">%d timers are running</item>
</plurals>
 
<!-- Settings -->
<string name="clock_tab">Kello-välilehti</string>

View File

@ -17,6 +17,16 @@
<string name="sort_by_creation_order">Creation order</string>
<string name="sort_by_alarm_time">Alarm time</string>
<!--Timer-->
<string name="timers_notification_msg">Timers are running</string>
<string name="timer_single_notification_label_msg">Timer for %s is running</string>
<string name="new_timer">New Timer</string>
<plurals name="timer_notification_msg">
<item quantity="one">%d timer is running</item>
<item quantity="other">%d timers are running</item>
</plurals>
<!-- Settings -->
<string name="clock_tab">Horloge</string>
<string name="alarm_tab">Réveil</string>

View File

@ -17,6 +17,17 @@
<string name="sort_by_creation_order">Creation order</string>
<string name="sort_by_alarm_time">Alarm time</string>
<!--Timer-->
<string name="timers_notification_msg">Timers are running</string>
<string name="timer_single_notification_label_msg">Timer for %s is running</string>
<string name="new_timer">New Timer</string>
<plurals name="timer_notification_msg">
<item quantity="one">%d timer is running</item>
<item quantity="few">%d timers are running</item>
<item quantity="other">%d timers are running</item>
</plurals>
<!-- Settings -->
<string name="clock_tab">Kartica sata</string>
<string name="alarm_tab">Kartica alarma</string>

View File

@ -17,6 +17,16 @@
<string name="sort_by_creation_order">Creation order</string>
<string name="sort_by_alarm_time">Alarm time</string>
<!--Timer-->
<string name="timers_notification_msg">Timers are running</string>
<string name="timer_single_notification_label_msg">Timer for %s is running</string>
<string name="new_timer">New Timer</string>
<plurals name="timer_notification_msg">
<item quantity="one">%d timer is running</item>
<item quantity="other">%d timers are running</item>
</plurals>
<!-- Settings -->
<string name="clock_tab">Tab jam</string>
<string name="alarm_tab">Tab alarm</string>

View File

@ -17,6 +17,16 @@
<string name="sort_by_creation_order">Creation order</string>
<string name="sort_by_alarm_time">Alarm time</string>
<!--Timer-->
<string name="timers_notification_msg">Timers are running</string>
<string name="timer_single_notification_label_msg">Timer for %s is running</string>
<string name="new_timer">New Timer</string>
<plurals name="timer_notification_msg">
<item quantity="one">%d timer is running</item>
<item quantity="other">%d timers are running</item>
</plurals>
<!-- Settings -->
<string name="clock_tab">Tab jam</string>
<string name="alarm_tab">Tab alarm</string>

View File

@ -17,6 +17,16 @@
<string name="sort_by_creation_order">Creation order</string>
<string name="sort_by_alarm_time">Alarm time</string>
<!--Timer-->
<string name="timers_notification_msg">Timers are running</string>
<string name="timer_single_notification_label_msg">Timer for %s is running</string>
<string name="new_timer">New Timer</string>
<plurals name="timer_notification_msg">
<item quantity="one">%d timer is running</item>
<item quantity="other">%d timers are running</item>
</plurals>
<!-- Settings -->
<string name="clock_tab">Scheda orologio</string>
<string name="alarm_tab">Scheda sveglia</string>

View File

@ -17,6 +17,16 @@
<string name="sort_by_creation_order">Creation order</string>
<string name="sort_by_alarm_time">Alarm time</string>
<!--Timer-->
<string name="timers_notification_msg">Timers are running</string>
<string name="timer_single_notification_label_msg">Timer for %s is running</string>
<string name="new_timer">New Timer</string>
<plurals name="timer_notification_msg">
<item quantity="one">%d timer is running</item>
<item quantity="other">%d timers are running</item>
</plurals>
<!-- Settings -->
<string name="clock_tab">時計</string>
<string name="alarm_tab">アラーム</string>

View File

@ -17,6 +17,17 @@
<string name="sort_by_creation_order">Creation order</string>
<string name="sort_by_alarm_time">Alarm time</string>
<!--Timer-->
<string name="timers_notification_msg">Timers are running</string>
<string name="timer_single_notification_label_msg">Timer for %s is running</string>
<string name="new_timer">New Timer</string>
<plurals name="timer_notification_msg">
<item quantity="one">%d timer is running</item>
<item quantity="few">%d timers are running</item>
<item quantity="other">%d timers are running</item>
</plurals>
<!-- Settings -->
<string name="clock_tab">Laikrodžio skirtukas</string>
<string name="alarm_tab">Žadintuvo skirtukas</string>

View File

@ -17,6 +17,16 @@
<string name="sort_by_creation_order">Creation order</string>
<string name="sort_by_alarm_time">Alarm time</string>
<!--Timer-->
<string name="timers_notification_msg">Timers are running</string>
<string name="timer_single_notification_label_msg">Timer for %s is running</string>
<string name="new_timer">New Timer</string>
<plurals name="timer_notification_msg">
<item quantity="one">%d timer is running</item>
<item quantity="other">%d timers are running</item>
</plurals>
<!-- Settings -->
<string name="clock_tab">ക്ലോക്ക് ടാബ്</string>
<string name="alarm_tab">അലാറം ടാബ്</string>

View File

@ -17,6 +17,16 @@
<string name="sort_by_creation_order">Creation order</string>
<string name="sort_by_alarm_time">Alarm time</string>
<!--Timer-->
<string name="timers_notification_msg">Timers are running</string>
<string name="timer_single_notification_label_msg">Timer for %s is running</string>
<string name="new_timer">New Timer</string>
<plurals name="timer_notification_msg">
<item quantity="one">%d timer is running</item>
<item quantity="other">%d timers are running</item>
</plurals>
<!-- Settings -->
<string name="clock_tab">Klokke</string>
<string name="alarm_tab">Alarm</string>

View File

@ -17,6 +17,16 @@
<string name="sort_by_creation_order">Aanmaakvolgorde</string>
<string name="sort_by_alarm_time">Alarmtijd</string>
<!--Timer-->
<string name="timers_notification_msg">Timers are running</string>
<string name="timer_single_notification_label_msg">Timer for %s is running</string>
<string name="new_timer">New Timer</string>
<plurals name="timer_notification_msg">
<item quantity="one">%d timer is running</item>
<item quantity="other">%d timers are running</item>
</plurals>
<!-- Settings -->
<string name="clock_tab">Tab Klok</string>
<string name="alarm_tab">Tab Alarm</string>

View File

@ -17,6 +17,18 @@
<string name="sort_by_creation_order">Creation order</string>
<string name="sort_by_alarm_time">Alarm time</string>
<!--Timer-->
<string name="timers_notification_msg">Timers are running</string>
<string name="timer_single_notification_label_msg">Timer for %s is running</string>
<string name="new_timer">New Timer</string>
<plurals name="timer_notification_msg">
<item quantity="one">%d timer is running</item>
<item quantity="few">%d timers are running</item>
<item quantity="many">%d timers are running</item>
<item quantity="other">%d timers are running</item>
</plurals>
<!-- Settings -->
<string name="clock_tab">Zegar</string>
<string name="alarm_tab">Alarm</string>

View File

@ -17,6 +17,16 @@
<string name="sort_by_creation_order">Creation order</string>
<string name="sort_by_alarm_time">Alarm time</string>
<!--Timer-->
<string name="timers_notification_msg">Timers are running</string>
<string name="timer_single_notification_label_msg">Timer for %s is running</string>
<string name="new_timer">New Timer</string>
<plurals name="timer_notification_msg">
<item quantity="one">%d timer is running</item>
<item quantity="other">%d timers are running</item>
</plurals>
<!-- Settings -->
<string name="clock_tab">Relógio</string>
<string name="alarm_tab">Alarme</string>

View File

@ -17,6 +17,18 @@
<string name="sort_by_creation_order">Creation order</string>
<string name="sort_by_alarm_time">Alarm time</string>
<!--Timer-->
<string name="timers_notification_msg">Timers are running</string>
<string name="timer_single_notification_label_msg">Timer for %s is running</string>
<string name="new_timer">New Timer</string>
<plurals name="timer_notification_msg">
<item quantity="one">%d timer is running</item>
<item quantity="few">%d timers are running</item>
<item quantity="many">%d timers are running</item>
<item quantity="other">%d timers are running</item>
</plurals>
<!-- Settings -->
<string name="clock_tab">Часы</string>
<string name="alarm_tab">Будильник</string>

View File

@ -17,6 +17,17 @@
<string name="sort_by_creation_order">Poradia vytvorenia</string>
<string name="sort_by_alarm_time">Času budíka</string>
<!--Timer-->
<string name="timers_notification_msg">Sú spustené časovače</string>
<string name="timer_single_notification_label_msg">Beží časovač pre %s</string>
<string name="new_timer">Nový časovač</string>
<plurals name="timer_notification_msg">
<item quantity="one">Beží %d časovač</item>
<item quantity="few">Bežia %d časovače</item>
<item quantity="other">Beží %d časovačov</item>
</plurals>
<!-- Settings -->
<string name="clock_tab">Okno s časom</string>
<string name="alarm_tab">Okno s budíkom</string>

View File

@ -17,6 +17,16 @@
<string name="sort_by_creation_order">Creation order</string>
<string name="sort_by_alarm_time">Alarm time</string>
<!--Timer-->
<string name="timers_notification_msg">Timers are running</string>
<string name="timer_single_notification_label_msg">Timer for %s is running</string>
<string name="new_timer">New Timer</string>
<plurals name="timer_notification_msg">
<item quantity="one">%d timer is running</item>
<item quantity="other">%d timers are running</item>
</plurals>
<!-- Settings -->
<string name="clock_tab">Fliken Klocka</string>
<string name="alarm_tab">Fliken Alarm</string>

View File

@ -17,6 +17,16 @@
<string name="sort_by_creation_order">Creation order</string>
<string name="sort_by_alarm_time">Alarm time</string>
<!--Timer-->
<string name="timers_notification_msg">Timers are running</string>
<string name="timer_single_notification_label_msg">Timer for %s is running</string>
<string name="new_timer">New Timer</string>
<plurals name="timer_notification_msg">
<item quantity="one">%d timer is running</item>
<item quantity="other">%d timers are running</item>
</plurals>
<!-- Settings -->
<string name="clock_tab">Saat sekmesi</string>
<string name="alarm_tab">Alarm sekmesi</string>

View File

@ -17,6 +17,18 @@
<string name="sort_by_creation_order">Creation order</string>
<string name="sort_by_alarm_time">Alarm time</string>
<!--Timer-->
<string name="timers_notification_msg">Timers are running</string>
<string name="timer_single_notification_label_msg">Timer for %s is running</string>
<string name="new_timer">New Timer</string>
<plurals name="timer_notification_msg">
<item quantity="one">%d timer is running</item>
<item quantity="few">%d timers are running</item>
<item quantity="many">%d timers are running</item>
<item quantity="other">%d timers are running</item>
</plurals>
<!-- Settings -->
<string name="clock_tab">Годинник</string>
<string name="alarm_tab">Будильник</string>

View File

@ -17,6 +17,16 @@
<string name="sort_by_creation_order">Creation order</string>
<string name="sort_by_alarm_time">Alarm time</string>
<!--Timer-->
<string name="timers_notification_msg">Timers are running</string>
<string name="timer_single_notification_label_msg">Timer for %s is running</string>
<string name="new_timer">New Timer</string>
<plurals name="timer_notification_msg">
<item quantity="one">%d timer is running</item>
<item quantity="other">%d timers are running</item>
</plurals>
<!-- Settings -->
<string name="clock_tab">时钟页面</string>
<string name="alarm_tab">闹钟页面</string>

View File

@ -17,6 +17,16 @@
<string name="sort_by_creation_order">Creation order</string>
<string name="sort_by_alarm_time">Alarm time</string>
<!--Timer-->
<string name="timers_notification_msg">Timers are running</string>
<string name="timer_single_notification_label_msg">Timer for %s is running</string>
<string name="new_timer">New Timer</string>
<plurals name="timer_notification_msg">
<item quantity="one">%d timer is running</item>
<item quantity="other">%d timers are running</item>
</plurals>
<!-- Settings -->
<string name="clock_tab">時鐘頁面</string>
<string name="alarm_tab">鬧鐘頁面</string>

View File

@ -10,6 +10,8 @@
<dimen name="min_widget_width">180dp</dimen>
<dimen name="min_widget_resize_width">110dp</dimen>
<dimen name="fab_list_bottom_padding">68dp</dimen>
<dimen name="timer_button_small_size">50dp</dimen>
<dimen name="timer_button_size">56dp</dimen>
<dimen name="clock_text_size">70sp</dimen>
<dimen name="clock_text_size_smaller">60sp</dimen>

View File

@ -17,6 +17,16 @@
<string name="sort_by_creation_order">Creation order</string>
<string name="sort_by_alarm_time">Alarm time</string>
<!--Timer-->
<string name="timers_notification_msg">Timers are running</string>
<string name="timer_single_notification_label_msg">Timer for %s is running</string>
<string name="new_timer">New Timer</string>
<plurals name="timer_notification_msg">
<item quantity="one">%d timer is running</item>
<item quantity="other">%d timers are running</item>
</plurals>
<!-- Settings -->
<string name="clock_tab">Clock tab</string>
<string name="alarm_tab">Alarm tab</string>

View File

@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.4.32'
ext.kotlin_version = '1.5.21'
repositories {
google()