diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 905e3c79..325ea717 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -74,7 +74,7 @@
android:windowSoftInputMode="adjustPan" />
+
+
+
+
-
+
-
+
-
+
if (countDownTimers[timer.id] == null) {
- EventBus.getDefault().post(TimerEvent.Start(timer.id!!, (timer.state as TimerState.Running).tick))
+ EventBus.getDefault().post(
+ TimerEvent.Start(
+ timerId = timer.id!!,
+ duration = (timer.state as TimerState.Running).tick
+ )
+ )
}
}
}
@@ -108,8 +116,7 @@ class App : Application(), LifecycleObserver {
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
+ val notification = getTimerNotification(timer, pendingIntent)
try {
notificationManager.notify(event.timerId, notification)
@@ -127,7 +134,10 @@ class App : Application(), LifecycleObserver {
@Subscribe(threadMode = ThreadMode.MAIN)
fun onMessageEvent(event: TimerEvent.Pause) {
timerHelper.getTimer(event.timerId) { timer ->
- updateTimerState(event.timerId, TimerState.Paused(event.duration, (timer.state as TimerState.Running).tick))
+ updateTimerState(
+ event.timerId,
+ TimerState.Paused(event.duration, (timer.state as TimerState.Running).tick)
+ )
countDownTimers[event.timerId]?.cancel()
}
}
diff --git a/app/src/main/kotlin/org/fossify/clock/activities/AlarmActivity.kt b/app/src/main/kotlin/org/fossify/clock/activities/AlarmActivity.kt
new file mode 100644
index 00000000..0bf4a4ac
--- /dev/null
+++ b/app/src/main/kotlin/org/fossify/clock/activities/AlarmActivity.kt
@@ -0,0 +1,236 @@
+package org.fossify.clock.activities
+
+import android.annotation.SuppressLint
+import android.content.Intent
+import android.content.res.Configuration
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.provider.AlarmClock
+import android.view.MotionEvent
+import android.view.WindowManager
+import android.view.animation.AnimationUtils
+import org.fossify.clock.R
+import org.fossify.clock.databinding.ActivityAlarmBinding
+import org.fossify.clock.extensions.alarmController
+import org.fossify.clock.extensions.config
+import org.fossify.clock.extensions.dbHelper
+import org.fossify.clock.extensions.getFormattedTime
+import org.fossify.clock.helpers.ALARM_ID
+import org.fossify.clock.helpers.getPassedSeconds
+import org.fossify.clock.models.Alarm
+import org.fossify.clock.models.AlarmEvent
+import org.fossify.commons.extensions.applyColorFilter
+import org.fossify.commons.extensions.getProperBackgroundColor
+import org.fossify.commons.extensions.getProperPrimaryColor
+import org.fossify.commons.extensions.getProperTextColor
+import org.fossify.commons.extensions.onGlobalLayout
+import org.fossify.commons.extensions.performHapticFeedback
+import org.fossify.commons.extensions.showPickSecondsDialog
+import org.fossify.commons.extensions.updateTextColors
+import org.fossify.commons.extensions.viewBinding
+import org.fossify.commons.helpers.MINUTE_SECONDS
+import org.fossify.commons.helpers.isOreoMr1Plus
+import org.greenrobot.eventbus.EventBus
+import org.greenrobot.eventbus.Subscribe
+import org.greenrobot.eventbus.ThreadMode
+import kotlin.math.max
+import kotlin.math.min
+
+class AlarmActivity : SimpleActivity() {
+
+ private val swipeGuideFadeHandler = Handler(Looper.getMainLooper())
+ private var alarm: Alarm? = null
+ private var didVibrate = false
+ private var dragDownX = 0f
+
+ private val binding by viewBinding(ActivityAlarmBinding::inflate)
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ isMaterialActivity = true
+ super.onCreate(savedInstanceState)
+ setContentView(binding.root)
+ showOverLockscreen()
+ updateTextColors(binding.root)
+ updateStatusbarColor(getProperBackgroundColor())
+
+ val id = intent.getIntExtra(ALARM_ID, -1)
+ alarm = dbHelper.getAlarmWithId(id)
+ if (alarm == null) {
+ finish()
+ return
+ }
+
+ val label = alarm!!.label.ifEmpty {
+ getString(org.fossify.commons.R.string.alarm)
+ }
+
+ binding.reminderTitle.text = label
+ binding.reminderText.text = getFormattedTime(
+ passedSeconds = getPassedSeconds(),
+ showSeconds = false,
+ makeAmPmSmaller = false
+ )
+
+ setupAlarmButtons()
+ EventBus.getDefault().register(this)
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ private fun setupAlarmButtons() {
+ binding.reminderDraggableBackground.startAnimation(
+ AnimationUtils.loadAnimation(this, R.anim.pulsing_animation)
+ )
+ binding.reminderDraggableBackground.applyColorFilter(getProperPrimaryColor())
+
+ val textColor = getProperTextColor()
+ binding.reminderDismiss.applyColorFilter(textColor)
+ binding.reminderDraggable.applyColorFilter(textColor)
+ binding.reminderSnooze.applyColorFilter(textColor)
+
+ var minDragX = 0f
+ var maxDragX = 0f
+ var initialDraggableX = 0f
+
+ binding.reminderDismiss.onGlobalLayout {
+ minDragX = binding.reminderSnooze.left.toFloat()
+ maxDragX = binding.reminderDismiss.left.toFloat()
+ initialDraggableX = binding.reminderDraggable.left.toFloat()
+ }
+
+ binding.reminderDraggable.setOnTouchListener { _, event ->
+ when (event.action) {
+ MotionEvent.ACTION_DOWN -> {
+ dragDownX = event.x
+ binding.reminderDraggableBackground.animate().alpha(0f)
+ }
+
+ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
+ dragDownX = 0f
+ if (!didVibrate) {
+ binding.reminderDraggable.animate().x(initialDraggableX).withEndAction {
+ binding.reminderDraggableBackground.animate().alpha(0.2f)
+ }
+
+ binding.reminderGuide.animate().alpha(1f).start()
+ swipeGuideFadeHandler.removeCallbacksAndMessages(null)
+ swipeGuideFadeHandler.postDelayed({
+ binding.reminderGuide.animate().alpha(0f).start()
+ }, 2000L)
+ }
+ }
+
+ MotionEvent.ACTION_MOVE -> {
+ binding.reminderDraggable.x = min(
+ a = maxDragX,
+ b = max(minDragX, event.rawX - dragDownX)
+ )
+
+ if (binding.reminderDraggable.x >= maxDragX - 50f) {
+ if (!didVibrate) {
+ binding.reminderDraggable.performHapticFeedback()
+ didVibrate = true
+ dismissAlarmAndFinish()
+ }
+ } else if (binding.reminderDraggable.x <= minDragX + 50f) {
+ if (!didVibrate) {
+ binding.reminderDraggable.performHapticFeedback()
+ didVibrate = true
+ snoozeAlarm()
+ }
+ }
+ }
+ }
+ true
+ }
+ }
+
+ override fun onConfigurationChanged(newConfig: Configuration) {
+ super.onConfigurationChanged(newConfig)
+ setupAlarmButtons()
+ }
+
+ override fun onNewIntent(intent: Intent?) {
+ super.onNewIntent(intent)
+ when (intent?.action) {
+ AlarmClock.ACTION_DISMISS_ALARM -> dismissAlarmAndFinish()
+ AlarmClock.ACTION_SNOOZE_ALARM -> {
+ val durationMinutes = intent.getIntExtra(AlarmClock.EXTRA_ALARM_SNOOZE_DURATION, -1)
+ if (durationMinutes == -1) {
+ snoozeAlarm()
+ } else {
+ snoozeAlarm(durationMinutes)
+ }
+ }
+
+ else -> {
+ // no-op. user probably clicked the notification
+ }
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ swipeGuideFadeHandler.removeCallbacksAndMessages(null)
+ EventBus.getDefault().unregister(this)
+ }
+
+ private fun snoozeAlarm(overrideSnoozeDuration: Int? = null) {
+ if (overrideSnoozeDuration != null) {
+ dismissAlarmAndFinish(overrideSnoozeDuration)
+ } else if (config.useSameSnooze) {
+ dismissAlarmAndFinish(config.snoozeTime)
+ } else {
+ alarmController.stopAlarm(alarmId = alarm!!.id, disable = false)
+ showPickSecondsDialog(
+ curSeconds = config.snoozeTime * MINUTE_SECONDS,
+ isSnoozePicker = true,
+ cancelCallback = {
+ dismissAlarmAndFinish()
+ },
+ callback = {
+ config.snoozeTime = it / MINUTE_SECONDS
+ dismissAlarmAndFinish(config.snoozeTime)
+ }
+ )
+ }
+ }
+
+ private fun dismissAlarmAndFinish(snoozeMinutes: Int = -1) {
+ if (alarm != null) {
+ if (snoozeMinutes != -1) {
+ alarmController.snoozeAlarm(alarm!!.id, snoozeMinutes)
+ } else {
+ alarmController.stopAlarm(alarm!!.id)
+ }
+ }
+
+ finishActivity()
+ }
+
+ private fun showOverLockscreen() {
+ window.addFlags(
+ WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or
+ WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD or
+ WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
+ WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
+ )
+
+ if (isOreoMr1Plus()) {
+ setShowWhenLocked(true)
+ setTurnScreenOn(true)
+ }
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ fun onAlarmStoppedEvent(event: AlarmEvent.Stopped) {
+ if (event.alarmId == alarm?.id && !isFinishing) {
+ finishActivity()
+ }
+ }
+
+ private fun finishActivity() {
+ finish()
+ overridePendingTransition(0, 0)
+ }
+}
diff --git a/app/src/main/kotlin/org/fossify/clock/activities/IntentHandlerActivity.kt b/app/src/main/kotlin/org/fossify/clock/activities/IntentHandlerActivity.kt
index b8bbb8a8..a3a42c77 100644
--- a/app/src/main/kotlin/org/fossify/clock/activities/IntentHandlerActivity.kt
+++ b/app/src/main/kotlin/org/fossify/clock/activities/IntentHandlerActivity.kt
@@ -1,20 +1,39 @@
package org.fossify.clock.activities
import android.annotation.SuppressLint
-import android.app.AlarmManager
-import android.content.Context
import android.content.Intent
import android.media.RingtoneManager
-import android.net.Uri
import android.os.Bundle
import android.provider.AlarmClock
+import androidx.core.net.toUri
import org.fossify.clock.R
import org.fossify.clock.dialogs.EditAlarmDialog
import org.fossify.clock.dialogs.EditTimerDialog
import org.fossify.clock.dialogs.SelectAlarmDialog
-import org.fossify.clock.extensions.*
-import org.fossify.clock.helpers.*
-import org.fossify.clock.models.*
+import org.fossify.clock.extensions.alarmController
+import org.fossify.clock.extensions.alarmManager
+import org.fossify.clock.extensions.config
+import org.fossify.clock.extensions.createNewAlarm
+import org.fossify.clock.extensions.createNewTimer
+import org.fossify.clock.extensions.dbHelper
+import org.fossify.clock.extensions.getHideTimerPendingIntent
+import org.fossify.clock.extensions.getSkipUpcomingAlarmPendingIntent
+import org.fossify.clock.extensions.isBitSet
+import org.fossify.clock.extensions.secondsToMillis
+import org.fossify.clock.extensions.timerHelper
+import org.fossify.clock.helpers.DEFAULT_ALARM_MINUTES
+import org.fossify.clock.helpers.TODAY_BIT
+import org.fossify.clock.helpers.TOMORROW_BIT
+import org.fossify.clock.helpers.UPCOMING_ALARM_NOTIFICATION_ID
+import org.fossify.clock.helpers.getBitForCalendarDay
+import org.fossify.clock.helpers.getCurrentDayMinutes
+import org.fossify.clock.helpers.getTodayBit
+import org.fossify.clock.helpers.getTomorrowBit
+import org.fossify.clock.models.Alarm
+import org.fossify.clock.models.AlarmEvent
+import org.fossify.clock.models.Timer
+import org.fossify.clock.models.TimerEvent
+import org.fossify.clock.models.TimerState
import org.fossify.commons.dialogs.PermissionRequiredDialog
import org.fossify.commons.extensions.getDefaultAlarmSound
import org.fossify.commons.extensions.getFilenameFromUri
@@ -65,7 +84,8 @@ class IntentHandlerActivity : SimpleActivity() {
private fun Intent.setNewAlarm() {
val hour = getIntExtra(AlarmClock.EXTRA_HOUR, 0).coerceIn(0, 23)
val minute = getIntExtra(AlarmClock.EXTRA_MINUTES, 0).coerceIn(0, 59)
- val days = getIntegerArrayListExtra(AlarmClock.EXTRA_DAYS) ?: getIntArrayExtra(AlarmClock.EXTRA_DAYS)?.toList()
+ val days = getIntegerArrayListExtra(AlarmClock.EXTRA_DAYS)
+ ?: getIntArrayExtra(AlarmClock.EXTRA_DAYS)?.toList()
val message = getStringExtra(AlarmClock.EXTRA_MESSAGE)
val ringtone = getStringExtra(AlarmClock.EXTRA_RINGTONE)
val vibrate = getBooleanExtra(AlarmClock.EXTRA_VIBRATE, true)
@@ -81,7 +101,7 @@ class IntentHandlerActivity : SimpleActivity() {
AlarmSound(0, getString(org.fossify.commons.R.string.no_sound), SILENT)
} else {
try {
- val uri = Uri.parse(it)
+ val uri = it.toUri()
var filename = getFilenameFromUri(uri)
if (filename.isEmpty()) {
filename = getString(org.fossify.commons.R.string.alarm)
@@ -106,11 +126,11 @@ class IntentHandlerActivity : SimpleActivity() {
}
val existingAlarm = dbHelper.getAlarms().firstOrNull {
it.days == daysToCompare
- && it.vibrate == vibrate
- && it.soundTitle == soundToUse.title
- && it.soundUri == soundToUse.uri
- && it.label == (message ?: "")
- && it.timeInMinutes == timeInMinutes
+ && it.vibrate == vibrate
+ && it.soundTitle == soundToUse.title
+ && it.soundUri == soundToUse.uri
+ && it.label == (message ?: "")
+ && it.timeInMinutes == timeInMinutes
}
if (existingAlarm != null && !existingAlarm.isEnabled) {
@@ -209,7 +229,10 @@ class IntentHandlerActivity : SimpleActivity() {
if (id != null) {
val alarm = dbHelper.getAlarmWithId(id)
if (alarm != null) {
- getDismissAlarmPendingIntent(alarm.id, EARLY_ALARM_NOTIF_ID).send()
+ getSkipUpcomingAlarmPendingIntent(
+ alarmId = alarm.id,
+ notificationId = UPCOMING_ALARM_NOTIFICATION_ID
+ ).send()
EventBus.getDefault().post(AlarmEvent.Refresh)
finish()
}
@@ -224,7 +247,9 @@ class IntentHandlerActivity : SimpleActivity() {
AlarmClock.ALARM_SEARCH_MODE_TIME -> {
if (hasExtra(AlarmClock.EXTRA_HOUR)) {
val hour = getIntExtra(AlarmClock.EXTRA_HOUR, -1).coerceIn(0, 23)
- alarms = alarms.filter { it.timeInMinutes / 60 == hour || it.timeInMinutes / 60 == hour + 12 }
+ alarms = alarms.filter {
+ it.timeInMinutes / 60 == hour || it.timeInMinutes / 60 == hour + 12
+ }
}
if (hasExtra(AlarmClock.EXTRA_MINUTES)) {
val minute = getIntExtra(AlarmClock.EXTRA_MINUTES, -1).coerceIn(0, 59)
@@ -244,9 +269,9 @@ class IntentHandlerActivity : SimpleActivity() {
}
AlarmClock.ALARM_SEARCH_MODE_NEXT -> {
- val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
val next = alarmManager.nextAlarmClock
- val timeInMinutes = TimeUnit.MILLISECONDS.toMinutes(next.triggerTime).toInt()
+ val timeInMinutes =
+ TimeUnit.MILLISECONDS.toMinutes(next.triggerTime).toInt()
val dayBitToLookFor = if (timeInMinutes <= getCurrentDayMinutes()) {
getTomorrowBit()
} else {
@@ -265,7 +290,12 @@ class IntentHandlerActivity : SimpleActivity() {
AlarmClock.ALARM_SEARCH_MODE_LABEL -> {
val messageToSearchFor = getStringExtra(AlarmClock.EXTRA_MESSAGE)
if (messageToSearchFor != null) {
- alarms = alarms.filter { it.label.contains(messageToSearchFor, ignoreCase = true) }
+ alarms = alarms.filter {
+ it.label.contains(
+ other = messageToSearchFor,
+ ignoreCase = true
+ )
+ }
}
}
@@ -276,13 +306,23 @@ class IntentHandlerActivity : SimpleActivity() {
}
if (alarms.count() == 1) {
- getDismissAlarmPendingIntent(alarms.first().id, EARLY_ALARM_NOTIF_ID).send()
+ getSkipUpcomingAlarmPendingIntent(
+ alarmId = alarms.first().id,
+ notificationId = UPCOMING_ALARM_NOTIFICATION_ID
+ ).send()
EventBus.getDefault().post(AlarmEvent.Refresh)
finish()
} else if (alarms.count() > 1) {
- SelectAlarmDialog(this@IntentHandlerActivity, alarms, R.string.select_alarm_to_dismiss) {
+ SelectAlarmDialog(
+ activity = this@IntentHandlerActivity,
+ alarms = alarms,
+ titleResId = R.string.select_alarm_to_dismiss
+ ) {
if (it != null) {
- getDismissAlarmPendingIntent(it.id, EARLY_ALARM_NOTIF_ID).send()
+ getSkipUpcomingAlarmPendingIntent(
+ alarmId = it.id,
+ notificationId = UPCOMING_ALARM_NOTIFICATION_ID
+ ).send()
}
EventBus.getDefault().post(AlarmEvent.Refresh)
finish()
@@ -338,16 +378,24 @@ class IntentHandlerActivity : SimpleActivity() {
}
private fun startAlarm(alarm: Alarm) {
- scheduleNextAlarm(alarm, true)
+ alarmController.scheduleNextOccurrence(alarm, true)
EventBus.getDefault().post(AlarmEvent.Refresh)
}
private fun startTimer(timer: Timer) {
handleNotificationPermission { granted ->
- val newState = TimerState.Running(timer.seconds.secondsToMillis, timer.seconds.secondsToMillis)
+ val newState = TimerState.Running(
+ duration = timer.seconds.secondsToMillis,
+ tick = timer.seconds.secondsToMillis
+ )
val newTimer = timer.copy(state = newState)
fun notifyAndStartTimer() {
- EventBus.getDefault().post(TimerEvent.Start(newTimer.id!!, newTimer.seconds.secondsToMillis))
+ EventBus.getDefault().post(
+ TimerEvent.Start(
+ timerId = newTimer.id!!,
+ duration = newTimer.seconds.secondsToMillis
+ )
+ )
EventBus.getDefault().post(TimerEvent.Refresh)
}
@@ -358,8 +406,8 @@ class IntentHandlerActivity : SimpleActivity() {
}
} else {
PermissionRequiredDialog(
- this,
- org.fossify.commons.R.string.allow_notifications_reminders,
+ activity = this,
+ textId = org.fossify.commons.R.string.allow_notifications_reminders,
positiveActionCallback = {
openNotificationSettings()
timerHelper.insertOrUpdateTimer(newTimer) {
@@ -369,7 +417,8 @@ class IntentHandlerActivity : SimpleActivity() {
},
negativeActionCallback = {
finish()
- })
+ }
+ )
}
}
}
diff --git a/app/src/main/kotlin/org/fossify/clock/activities/MainActivity.kt b/app/src/main/kotlin/org/fossify/clock/activities/MainActivity.kt
index b3b326d8..c698c5b4 100644
--- a/app/src/main/kotlin/org/fossify/clock/activities/MainActivity.kt
+++ b/app/src/main/kotlin/org/fossify/clock/activities/MainActivity.kt
@@ -3,25 +3,59 @@ package org.fossify.clock.activities
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.ShortcutInfo
-import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Icon
import android.graphics.drawable.LayerDrawable
import android.os.Bundle
import android.view.WindowManager
+import androidx.core.graphics.drawable.toDrawable
import me.grantland.widget.AutofitHelper
import org.fossify.clock.BuildConfig
import org.fossify.clock.R
import org.fossify.clock.adapters.ViewPagerAdapter
import org.fossify.clock.databinding.ActivityMainBinding
+import org.fossify.clock.extensions.alarmController
import org.fossify.clock.extensions.config
import org.fossify.clock.extensions.getEnabledAlarms
import org.fossify.clock.extensions.handleFullScreenNotificationsPermission
-import org.fossify.clock.extensions.rescheduleEnabledAlarms
import org.fossify.clock.extensions.updateWidgets
-import org.fossify.clock.helpers.*
+import org.fossify.clock.helpers.INVALID_TIMER_ID
+import org.fossify.clock.helpers.OPEN_TAB
+import org.fossify.clock.helpers.PICK_AUDIO_FILE_INTENT_ID
+import org.fossify.clock.helpers.STOPWATCH_SHORTCUT_ID
+import org.fossify.clock.helpers.STOPWATCH_TOGGLE_ACTION
+import org.fossify.clock.helpers.TABS_COUNT
+import org.fossify.clock.helpers.TAB_ALARM
+import org.fossify.clock.helpers.TAB_ALARM_INDEX
+import org.fossify.clock.helpers.TAB_CLOCK
+import org.fossify.clock.helpers.TAB_CLOCK_INDEX
+import org.fossify.clock.helpers.TAB_STOPWATCH
+import org.fossify.clock.helpers.TAB_STOPWATCH_INDEX
+import org.fossify.clock.helpers.TAB_TIMER
+import org.fossify.clock.helpers.TAB_TIMER_INDEX
+import org.fossify.clock.helpers.TIMER_ID
+import org.fossify.clock.helpers.TOGGLE_STOPWATCH
import org.fossify.commons.databinding.BottomTablayoutItemBinding
-import org.fossify.commons.extensions.*
-import org.fossify.commons.helpers.*
+import org.fossify.commons.extensions.appLaunched
+import org.fossify.commons.extensions.applyColorFilter
+import org.fossify.commons.extensions.convertToBitmap
+import org.fossify.commons.extensions.getBottomNavigationBackgroundColor
+import org.fossify.commons.extensions.getProperBackgroundColor
+import org.fossify.commons.extensions.getProperPrimaryColor
+import org.fossify.commons.extensions.getProperTextColor
+import org.fossify.commons.extensions.launchMoreAppsFromUsIntent
+import org.fossify.commons.extensions.onPageChangeListener
+import org.fossify.commons.extensions.onTabSelectionChanged
+import org.fossify.commons.extensions.shortcutManager
+import org.fossify.commons.extensions.storeNewYourAlarmSound
+import org.fossify.commons.extensions.toast
+import org.fossify.commons.extensions.updateBottomTabItemColors
+import org.fossify.commons.extensions.viewBinding
+import org.fossify.commons.helpers.LICENSE_AUTOFITTEXTVIEW
+import org.fossify.commons.helpers.LICENSE_NUMBER_PICKER
+import org.fossify.commons.helpers.LICENSE_RTL
+import org.fossify.commons.helpers.LICENSE_STETHO
+import org.fossify.commons.helpers.ensureBackgroundThread
+import org.fossify.commons.helpers.isNougatMR1Plus
import org.fossify.commons.models.FAQItem
class MainActivity : SimpleActivity() {
@@ -38,7 +72,12 @@ class MainActivity : SimpleActivity() {
setupOptionsMenu()
refreshMenuItems()
- updateMaterialActivityViews(binding.mainCoordinator, binding.mainHolder, useTransparentNavigation = false, useTopSearchMenu = false)
+ updateMaterialActivityViews(
+ mainCoordinatorLayout = binding.mainCoordinator,
+ nestedView = binding.mainHolder,
+ useTransparentNavigation = false,
+ useTopSearchMenu = false
+ )
storeStateVariables()
initFragments()
@@ -46,7 +85,7 @@ class MainActivity : SimpleActivity() {
updateWidgets()
migrateFirstDayOfWeek()
ensureBackgroundThread {
- rescheduleEnabledAlarms()
+ alarmController.rescheduleEnabledAlarms()
}
getEnabledAlarms { enabledAlarms ->
@@ -72,13 +111,14 @@ class MainActivity : SimpleActivity() {
val configBackgroundColor = getProperBackgroundColor()
if (storedBackgroundColor != configBackgroundColor) {
- binding.mainTabsHolder.background = ColorDrawable(configBackgroundColor)
+ binding.mainTabsHolder.background = configBackgroundColor.toDrawable()
}
val configPrimaryColor = getProperPrimaryColor()
if (storedPrimaryColor != configPrimaryColor) {
binding.mainTabsHolder.setSelectedTabIndicatorColor(getProperPrimaryColor())
- binding.mainTabsHolder.getTabAt(binding.viewPager.currentItem)?.icon?.applyColorFilter(getProperPrimaryColor())
+ binding.mainTabsHolder.getTabAt(binding.viewPager.currentItem)?.icon
+ ?.applyColorFilter(getProperPrimaryColor())
}
if (config.preventPhoneFromSleeping) {
@@ -107,7 +147,9 @@ class MainActivity : SimpleActivity() {
private fun getLaunchStopwatchShortcut(appIconColor: Int): ShortcutInfo {
val newEvent = getString(R.string.start_stopwatch)
val drawable = resources.getDrawable(R.drawable.shortcut_stopwatch)
- (drawable as LayerDrawable).findDrawableByLayerId(R.id.shortcut_stopwatch_background).applyColorFilter(appIconColor)
+ (drawable as LayerDrawable)
+ .findDrawableByLayerId(R.id.shortcut_stopwatch_background)
+ .applyColorFilter(appIconColor)
val bmp = drawable.convertToBitmap()
val intent = Intent(this, SplashActivity::class.java).apply {
@@ -152,8 +194,10 @@ class MainActivity : SimpleActivity() {
private fun refreshMenuItems() {
binding.mainToolbar.menu.apply {
- findItem(R.id.sort).isVisible = binding.viewPager.currentItem == getTabIndex(TAB_ALARM) || binding.viewPager.currentItem == getTabIndex(TAB_TIMER)
- findItem(R.id.more_apps_from_us).isVisible = !resources.getBoolean(org.fossify.commons.R.bool.hide_google_relations)
+ findItem(R.id.sort).isVisible = binding.viewPager.currentItem == getTabIndex(TAB_ALARM)
+ || binding.viewPager.currentItem == getTabIndex(TAB_TIMER)
+ findItem(R.id.more_apps_from_us).isVisible =
+ !resources.getBoolean(org.fossify.commons.R.bool.hide_google_relations)
}
}
@@ -235,33 +279,52 @@ class MainActivity : SimpleActivity() {
R.drawable.ic_stopwatch_vector,
R.drawable.ic_hourglass_vector
)
- val tabLabels = arrayOf(R.string.clock, org.fossify.commons.R.string.alarm, R.string.stopwatch, R.string.timer)
+ val tabLabels = arrayOf(
+ R.string.clock,
+ org.fossify.commons.R.string.alarm,
+ R.string.stopwatch,
+ R.string.timer
+ )
tabDrawables.forEachIndexed { i, drawableId ->
- binding.mainTabsHolder.newTab().setCustomView(org.fossify.commons.R.layout.bottom_tablayout_item).apply tab@{
- customView?.let { BottomTablayoutItemBinding.bind(it) }?.apply {
- tabItemIcon.setImageDrawable(getDrawable(drawableId))
- tabItemLabel.setText(tabLabels[i])
- AutofitHelper.create(tabItemLabel)
- binding.mainTabsHolder.addTab(this@tab)
+ binding.mainTabsHolder.newTab()
+ .setCustomView(org.fossify.commons.R.layout.bottom_tablayout_item)
+ .apply tab@{
+ customView?.let { BottomTablayoutItemBinding.bind(it) }?.apply {
+ tabItemIcon.setImageDrawable(getDrawable(drawableId))
+ tabItemLabel.setText(tabLabels[i])
+ AutofitHelper.create(tabItemLabel)
+ binding.mainTabsHolder.addTab(this@tab)
+ }
}
- }
}
binding.mainTabsHolder.onTabSelectionChanged(
tabUnselectedAction = {
- updateBottomTabItemColors(it.customView, false, getDeselectedTabDrawableIds()[it.position])
+ updateBottomTabItemColors(
+ view = it.customView,
+ isActive = false,
+ drawableId = getDeselectedTabDrawableIds()[it.position]
+ )
},
tabSelectedAction = {
binding.viewPager.currentItem = it.position
- updateBottomTabItemColors(it.customView, true, getSelectedTabDrawableIds()[it.position])
+ updateBottomTabItemColors(
+ view = it.customView,
+ isActive = true,
+ drawableId = getSelectedTabDrawableIds()[it.position]
+ )
}
)
}
private fun setupTabColors() {
val activeView = binding.mainTabsHolder.getTabAt(binding.viewPager.currentItem)?.customView
- updateBottomTabItemColors(activeView, true, getSelectedTabDrawableIds()[binding.viewPager.currentItem])
+ updateBottomTabItemColors(
+ view = activeView,
+ isActive = true,
+ drawableId = getSelectedTabDrawableIds()[binding.viewPager.currentItem]
+ )
getInactiveTabIndexes(binding.viewPager.currentItem).forEach { index ->
val inactiveView = binding.mainTabsHolder.getTabAt(index)?.customView
@@ -274,7 +337,9 @@ class MainActivity : SimpleActivity() {
updateNavigationBarColor(bottomBarColor)
}
- private fun getInactiveTabIndexes(activeIndex: Int) = arrayListOf(0, 1, 2, 3).filter { it != activeIndex }
+ private fun getInactiveTabIndexes(activeIndex: Int): List {
+ return arrayListOf(0, 1, 2, 3).filter { it != activeIndex }
+ }
private fun getSelectedTabDrawableIds() = arrayOf(
org.fossify.commons.R.drawable.ic_clock_filled_vector,
@@ -295,21 +360,50 @@ class MainActivity : SimpleActivity() {
}
private fun launchAbout() {
- val licenses = LICENSE_STETHO or LICENSE_NUMBER_PICKER or LICENSE_RTL or LICENSE_AUTOFITTEXTVIEW
+ val licenses =
+ LICENSE_STETHO or LICENSE_NUMBER_PICKER or LICENSE_RTL or LICENSE_AUTOFITTEXTVIEW
val faqItems = arrayListOf(
- FAQItem(R.string.faq_1_title, R.string.faq_1_text),
- FAQItem(org.fossify.commons.R.string.faq_1_title_commons, org.fossify.commons.R.string.faq_1_text_commons),
- FAQItem(org.fossify.commons.R.string.faq_4_title_commons, org.fossify.commons.R.string.faq_4_text_commons),
- FAQItem(org.fossify.commons.R.string.faq_9_title_commons, org.fossify.commons.R.string.faq_9_text_commons)
+ FAQItem(
+ title = R.string.faq_1_title,
+ text = R.string.faq_1_text
+ ),
+ FAQItem(
+ title = org.fossify.commons.R.string.faq_1_title_commons,
+ text = org.fossify.commons.R.string.faq_1_text_commons
+ ),
+ FAQItem(
+ title = org.fossify.commons.R.string.faq_4_title_commons,
+ text = org.fossify.commons.R.string.faq_4_text_commons
+ ),
+ FAQItem(
+ title = org.fossify.commons.R.string.faq_9_title_commons,
+ text = org.fossify.commons.R.string.faq_9_text_commons
+ )
)
if (!resources.getBoolean(org.fossify.commons.R.bool.hide_google_relations)) {
- faqItems.add(FAQItem(org.fossify.commons.R.string.faq_2_title_commons, org.fossify.commons.R.string.faq_2_text_commons))
- faqItems.add(FAQItem(org.fossify.commons.R.string.faq_6_title_commons, org.fossify.commons.R.string.faq_6_text_commons))
+ faqItems.add(
+ FAQItem(
+ title = org.fossify.commons.R.string.faq_2_title_commons,
+ text = org.fossify.commons.R.string.faq_2_text_commons
+ )
+ )
+ faqItems.add(
+ FAQItem(
+ title = org.fossify.commons.R.string.faq_6_title_commons,
+ text = org.fossify.commons.R.string.faq_6_text_commons
+ )
+ )
}
- startAboutActivity(R.string.app_name, licenses, BuildConfig.VERSION_NAME, faqItems, true)
+ startAboutActivity(
+ appNameId = R.string.app_name,
+ licenseMask = licenses,
+ versionName = BuildConfig.VERSION_NAME,
+ faqItems = faqItems,
+ showFAQBeforeMail = true
+ )
}
@Deprecated("Remove this method in future releases")
diff --git a/app/src/main/kotlin/org/fossify/clock/activities/ReminderActivity.kt b/app/src/main/kotlin/org/fossify/clock/activities/ReminderActivity.kt
deleted file mode 100644
index f0d7d015..00000000
--- a/app/src/main/kotlin/org/fossify/clock/activities/ReminderActivity.kt
+++ /dev/null
@@ -1,393 +0,0 @@
-package org.fossify.clock.activities
-
-import android.annotation.SuppressLint
-import android.content.Intent
-import android.content.res.Configuration
-import android.media.AudioManager
-import android.media.MediaPlayer
-import android.os.Bundle
-import android.os.Handler
-import android.os.Looper
-import android.os.VibrationEffect
-import android.os.Vibrator
-import android.provider.AlarmClock
-import android.view.MotionEvent
-import android.view.WindowManager
-import android.view.animation.AnimationUtils
-import androidx.core.net.toUri
-import org.fossify.clock.R
-import org.fossify.clock.databinding.ActivityReminderBinding
-import org.fossify.clock.extensions.cancelAlarmClock
-import org.fossify.clock.extensions.config
-import org.fossify.clock.extensions.dbHelper
-import org.fossify.clock.extensions.disableExpiredAlarm
-import org.fossify.clock.extensions.getFormattedTime
-import org.fossify.clock.extensions.scheduleNextAlarm
-import org.fossify.clock.extensions.setupAlarmClock
-import org.fossify.clock.helpers.ALARM_ID
-import org.fossify.clock.helpers.ALARM_NOTIF_ID
-import org.fossify.clock.helpers.getPassedSeconds
-import org.fossify.clock.models.Alarm
-import org.fossify.commons.extensions.applyColorFilter
-import org.fossify.commons.extensions.beGone
-import org.fossify.commons.extensions.getColoredDrawableWithColor
-import org.fossify.commons.extensions.getProperBackgroundColor
-import org.fossify.commons.extensions.getProperPrimaryColor
-import org.fossify.commons.extensions.getProperTextColor
-import org.fossify.commons.extensions.notificationManager
-import org.fossify.commons.extensions.onGlobalLayout
-import org.fossify.commons.extensions.performHapticFeedback
-import org.fossify.commons.extensions.showPickSecondsDialog
-import org.fossify.commons.extensions.updateTextColors
-import org.fossify.commons.extensions.viewBinding
-import org.fossify.commons.helpers.MINUTE_SECONDS
-import org.fossify.commons.helpers.SILENT
-import org.fossify.commons.helpers.isOreoMr1Plus
-import org.fossify.commons.helpers.isOreoPlus
-import java.util.Calendar
-import kotlin.math.max
-import kotlin.math.min
-
-class ReminderActivity : SimpleActivity() {
- companion object {
- private const val MIN_ALARM_VOLUME_FOR_INCREASING_ALARMS = 1
- private const val INCREASE_VOLUME_DELAY = 300L
- }
-
- private val increaseVolumeHandler = Handler(Looper.getMainLooper())
- private val maxReminderDurationHandler = Handler(Looper.getMainLooper())
- private val swipeGuideFadeHandler = Handler()
- private val vibrationHandler = Handler(Looper.getMainLooper())
- private var isAlarmReminder = false
- private var didVibrate = false
- private var wasAlarmSnoozed = false
- private var alarm: Alarm? = null
- private var audioManager: AudioManager? = null
- private var mediaPlayer: MediaPlayer? = null
- private var vibrator: Vibrator? = null
- private var initialAlarmVolume: Int? = null
- private var dragDownX = 0f
- private val binding: ActivityReminderBinding by viewBinding(ActivityReminderBinding::inflate)
- private var finished = false
-
- override fun onCreate(savedInstanceState: Bundle?) {
- isMaterialActivity = true
- super.onCreate(savedInstanceState)
- setContentView(binding.root)
- showOverLockscreen()
- updateTextColors(binding.root)
- updateStatusbarColor(getProperBackgroundColor())
-
- val id = intent.getIntExtra(ALARM_ID, -1)
- isAlarmReminder = id != -1
- if (id != -1) {
- alarm = dbHelper.getAlarmWithId(id) ?: return
- }
-
- val label = if (isAlarmReminder) {
- if (alarm!!.label.isEmpty()) {
- getString(org.fossify.commons.R.string.alarm)
- } else {
- alarm!!.label
- }
- } else {
- getString(R.string.timer)
- }
-
- binding.reminderTitle.text = label
- binding.reminderText.text = if (isAlarmReminder) {
- getFormattedTime(
- passedSeconds = getPassedSeconds(),
- showSeconds = false,
- makeAmPmSmaller = false
- )
- } else {
- getString(R.string.time_expired)
- }
-
- val maxDuration = if (isAlarmReminder) {
- config.alarmMaxReminderSecs
- } else {
- config.timerMaxReminderSecs
- }
-
- maxReminderDurationHandler.postDelayed({
- finishActivity()
- cancelNotification()
- }, maxDuration * 1000L)
-
- setupButtons()
- setupEffects()
- }
-
- private fun setupButtons() {
- if (isAlarmReminder) {
- setupAlarmButtons()
- } else {
- setupTimerButtons()
- }
- }
-
- @SuppressLint("ClickableViewAccessibility")
- private fun setupAlarmButtons() {
- binding.reminderStop.beGone()
- binding.reminderDraggableBackground.startAnimation(
- AnimationUtils.loadAnimation(
- this,
- R.anim.pulsing_animation
- )
- )
- binding.reminderDraggableBackground.applyColorFilter(getProperPrimaryColor())
-
- val textColor = getProperTextColor()
- binding.reminderDismiss.applyColorFilter(textColor)
- binding.reminderDraggable.applyColorFilter(textColor)
- binding.reminderSnooze.applyColorFilter(textColor)
-
- var minDragX = 0f
- var maxDragX = 0f
- var initialDraggableX = 0f
-
- binding.reminderDismiss.onGlobalLayout {
- minDragX = binding.reminderSnooze.left.toFloat()
- maxDragX = binding.reminderDismiss.left.toFloat()
- initialDraggableX = binding.reminderDraggable.left.toFloat()
- }
-
- binding.reminderDraggable.setOnTouchListener { v, event ->
- when (event.action) {
- MotionEvent.ACTION_DOWN -> {
- dragDownX = event.x
- binding.reminderDraggableBackground.animate().alpha(0f)
- }
-
- MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
- dragDownX = 0f
- if (!didVibrate) {
- binding.reminderDraggable.animate().x(initialDraggableX).withEndAction {
- binding.reminderDraggableBackground.animate().alpha(0.2f)
- }
-
- binding.reminderGuide.animate().alpha(1f).start()
- swipeGuideFadeHandler.removeCallbacksAndMessages(null)
- swipeGuideFadeHandler.postDelayed({
- binding.reminderGuide.animate().alpha(0f).start()
- }, 2000L)
- }
- }
-
- MotionEvent.ACTION_MOVE -> {
- binding.reminderDraggable.x = min(
- a = maxDragX,
- b = max(minDragX, event.rawX - dragDownX)
- )
-
- if (binding.reminderDraggable.x >= maxDragX - 50f) {
- if (!didVibrate) {
- binding.reminderDraggable.performHapticFeedback()
- didVibrate = true
- finishActivity()
- }
-
- cancelNotification()
- } else if (binding.reminderDraggable.x <= minDragX + 50f) {
- if (!didVibrate) {
- binding.reminderDraggable.performHapticFeedback()
- didVibrate = true
- snoozeAlarm()
- }
-
- cancelNotification()
- }
- }
- }
- true
- }
- }
-
- private fun setupTimerButtons() {
- binding.reminderStop.background = resources.getColoredDrawableWithColor(
- drawableId = R.drawable.circle_background_filled,
- color = getProperPrimaryColor()
- )
- arrayOf(
- binding.reminderSnooze,
- binding.reminderDraggableBackground,
- binding.reminderDraggable,
- binding.reminderDismiss
- ).forEach {
- it.beGone()
- }
-
- binding.reminderStop.setOnClickListener {
- finishActivity()
- }
- }
-
- private fun setupEffects() {
- audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
- initialAlarmVolume = audioManager?.getStreamVolume(AudioManager.STREAM_ALARM) ?: 7
-
- val doVibrate = alarm?.vibrate ?: config.timerVibrate
- if (doVibrate && isOreoPlus()) {
- val pattern = LongArray(2) { 500 }
- vibrationHandler.postDelayed({
- vibrator = getSystemService(VIBRATOR_SERVICE) as Vibrator
- vibrator?.vibrate(VibrationEffect.createWaveform(pattern, 0))
- }, 500)
- }
-
- val soundUri = if (alarm != null) {
- alarm!!.soundUri
- } else {
- config.timerSoundUri
- }
-
- if (soundUri != SILENT) {
- try {
- mediaPlayer = MediaPlayer().apply {
- setAudioStreamType(AudioManager.STREAM_ALARM)
- setDataSource(this@ReminderActivity, soundUri.toUri())
- isLooping = true
- prepare()
- start()
- }
-
- if (config.increaseVolumeGradually) {
- scheduleVolumeIncrease(
- lastVolume = MIN_ALARM_VOLUME_FOR_INCREASING_ALARMS.toFloat(),
- maxVolume = initialAlarmVolume!!.toFloat(),
- delay = 0
- )
- }
- } catch (e: Exception) {
- }
- }
- }
-
- private fun scheduleVolumeIncrease(lastVolume: Float, maxVolume: Float, delay: Long) {
- increaseVolumeHandler.postDelayed({
- val newLastVolume = (lastVolume + 0.1f).coerceAtMost(maxVolume)
- audioManager?.setStreamVolume(AudioManager.STREAM_ALARM, newLastVolume.toInt(), 0)
- scheduleVolumeIncrease(newLastVolume, maxVolume, INCREASE_VOLUME_DELAY)
- }, delay)
- }
-
- private fun resetVolumeToInitialValue() {
- initialAlarmVolume?.apply {
- audioManager?.setStreamVolume(AudioManager.STREAM_ALARM, this, 0)
- }
- }
-
- override fun onConfigurationChanged(newConfig: Configuration) {
- super.onConfigurationChanged(newConfig)
- setupAlarmButtons()
- }
-
- override fun onNewIntent(intent: Intent?) {
- super.onNewIntent(intent)
- if (intent?.action == AlarmClock.ACTION_SNOOZE_ALARM) {
- val durationMinutes = intent.getIntExtra(AlarmClock.EXTRA_ALARM_SNOOZE_DURATION, -1)
- if (durationMinutes == -1) {
- snoozeAlarm()
- } else {
- snoozeAlarm(durationMinutes)
- }
- } else {
- finishActivity()
- }
- }
-
- override fun onDestroy() {
- super.onDestroy()
- increaseVolumeHandler.removeCallbacksAndMessages(null)
- maxReminderDurationHandler.removeCallbacksAndMessages(null)
- swipeGuideFadeHandler.removeCallbacksAndMessages(null)
- vibrationHandler.removeCallbacksAndMessages(null)
- if (!finished) {
- finishActivity()
- cancelNotification()
- } else {
- destroyEffects()
- }
- }
-
- private fun destroyEffects() {
- if (config.increaseVolumeGradually) {
- resetVolumeToInitialValue()
- }
-
- mediaPlayer?.stop()
- mediaPlayer?.release()
- mediaPlayer = null
- vibrator?.cancel()
- vibrator = null
- }
-
- private fun snoozeAlarm(overrideSnoozeDuration: Int? = null) {
- destroyEffects()
- if (overrideSnoozeDuration != null) {
- scheduleSnoozedAlarm(overrideSnoozeDuration)
- } else if (config.useSameSnooze) {
- scheduleSnoozedAlarm(config.snoozeTime)
- } else {
- showPickSecondsDialog(
- curSeconds = config.snoozeTime * MINUTE_SECONDS,
- isSnoozePicker = true,
- cancelCallback = {
- finishActivity()
- },
- callback = {
- config.snoozeTime = it / MINUTE_SECONDS
- scheduleSnoozedAlarm(config.snoozeTime)
- }
- )
- }
- }
-
- private fun scheduleSnoozedAlarm(snoozeMinutes: Int) {
- setupAlarmClock(
- alarm = alarm!!,
- triggerTimeMillis = Calendar.getInstance()
- .apply { add(Calendar.MINUTE, snoozeMinutes) }
- .timeInMillis
- )
-
- wasAlarmSnoozed = true
- finishActivity()
- }
-
- private fun finishActivity() {
- if (!wasAlarmSnoozed && alarm != null) {
- cancelAlarmClock(alarm!!)
- if (alarm!!.days > 0) {
- scheduleNextAlarm(alarm!!, false)
- }
-
- disableExpiredAlarm(alarm!!)
- }
-
- finished = true
- destroyEffects()
- finish()
- overridePendingTransition(0, 0)
- }
-
- private fun showOverLockscreen() {
- window.addFlags(
- WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or
- WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD or
- WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
- WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
- )
-
- if (isOreoMr1Plus()) {
- setShowWhenLocked(true)
- setTurnScreenOn(true)
- }
- }
-
- private fun cancelNotification() {
- notificationManager.cancel(ALARM_NOTIF_ID)
- }
-}
diff --git a/app/src/main/kotlin/org/fossify/clock/activities/SettingsActivity.kt b/app/src/main/kotlin/org/fossify/clock/activities/SettingsActivity.kt
index 2ce673f1..1b34b658 100644
--- a/app/src/main/kotlin/org/fossify/clock/activities/SettingsActivity.kt
+++ b/app/src/main/kotlin/org/fossify/clock/activities/SettingsActivity.kt
@@ -10,10 +10,9 @@ import org.fossify.clock.R
import org.fossify.clock.databinding.ActivitySettingsBinding
import org.fossify.clock.dialogs.ExportDataDialog
import org.fossify.clock.extensions.config
-import org.fossify.clock.extensions.updateWidgets
import org.fossify.clock.extensions.dbHelper
import org.fossify.clock.extensions.timerDb
-import org.fossify.clock.helpers.DBHelper
+import org.fossify.clock.extensions.updateWidgets
import org.fossify.clock.helpers.DEFAULT_MAX_ALARM_REMINDER_SECS
import org.fossify.clock.helpers.DEFAULT_MAX_TIMER_REMINDER_SECS
import org.fossify.clock.helpers.EXPORT_BACKUP_MIME_TYPE
@@ -50,7 +49,6 @@ import org.fossify.commons.helpers.TAB_LAST_USED
import org.fossify.commons.helpers.ensureBackgroundThread
import org.fossify.commons.helpers.isTiramisuPlus
import org.fossify.commons.models.RadioItem
-import java.io.IOException
import java.util.Locale
import kotlin.system.exitProcess
@@ -60,11 +58,7 @@ class SettingsActivity : SimpleActivity() {
registerForActivityResult(ActivityResultContracts.CreateDocument(EXPORT_BACKUP_MIME_TYPE)) { uri ->
if (uri == null) return@registerForActivityResult
ensureBackgroundThread {
- try {
- exportDataTo(uri)
- } catch (e: IOException) {
- showErrorToast(e)
- }
+ exportData(uri)
}
}
@@ -72,11 +66,7 @@ class SettingsActivity : SimpleActivity() {
registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
if (uri == null) return@registerForActivityResult
ensureBackgroundThread {
- try {
- importData(uri)
- } catch (e: Exception) {
- showErrorToast(e)
- }
+ importData(uri)
}
}
@@ -218,12 +208,14 @@ class SettingsActivity : SimpleActivity() {
RadioItem(5, getString(org.fossify.commons.R.string.saturday)),
)
- binding.settingsStartWeekOn.text = resources.getStringArray(org.fossify.commons.R.array.week_days)[config.firstDayOfWeek]
+ binding.settingsStartWeekOn.text =
+ resources.getStringArray(org.fossify.commons.R.array.week_days)[config.firstDayOfWeek]
binding.settingsStartWeekOnHolder.setOnClickListener {
RadioGroupDialog(this@SettingsActivity, items, config.firstDayOfWeek) { day ->
val firstDayOfWeek = day as Int
config.firstDayOfWeek = firstDayOfWeek
- binding.settingsStartWeekOn.text = resources.getStringArray(org.fossify.commons.R.array.week_days)[config.firstDayOfWeek]
+ binding.settingsStartWeekOn.text =
+ resources.getStringArray(org.fossify.commons.R.array.week_days)[config.firstDayOfWeek]
}
}
}
@@ -231,7 +223,11 @@ class SettingsActivity : SimpleActivity() {
private fun setupAlarmMaxReminder() {
updateAlarmMaxReminderText()
binding.settingsAlarmMaxReminderHolder.setOnClickListener {
- showPickSecondsDialog(config.alarmMaxReminderSecs, true, true) {
+ showPickSecondsDialog(
+ curSeconds = config.alarmMaxReminderSecs,
+ isSnoozePicker = true,
+ showSecondsAtCustomDialog = true
+ ) {
config.alarmMaxReminderSecs = if (it != 0) it else DEFAULT_MAX_ALARM_REMINDER_SECS
updateAlarmMaxReminderText()
}
@@ -251,7 +247,10 @@ class SettingsActivity : SimpleActivity() {
private fun setupSnoozeTime() {
updateSnoozeText()
binding.settingsSnoozeTimeHolder.setOnClickListener {
- showPickSecondsDialog(config.snoozeTime * MINUTE_SECONDS, true) {
+ showPickSecondsDialog(
+ curSeconds = config.snoozeTime * MINUTE_SECONDS,
+ isSnoozePicker = true
+ ) {
config.snoozeTime = it / MINUTE_SECONDS
updateSnoozeText()
}
@@ -261,7 +260,11 @@ class SettingsActivity : SimpleActivity() {
private fun setupTimerMaxReminder() {
updateTimerMaxReminderText()
binding.settingsTimerMaxReminderHolder.setOnClickListener {
- showPickSecondsDialog(config.timerMaxReminderSecs, true, true) {
+ showPickSecondsDialog(
+ curSeconds = config.timerMaxReminderSecs,
+ isSnoozePicker = true,
+ showSecondsAtCustomDialog = true
+ ) {
config.timerMaxReminderSecs = if (it != 0) it else DEFAULT_MAX_TIMER_REMINDER_SECS
updateTimerMaxReminderText()
}
@@ -311,7 +314,7 @@ class SettingsActivity : SimpleActivity() {
}
}
- private fun exportDataTo(outputUri: Uri) {
+ private fun exportData(outputUri: Uri) {
val alarms = dbHelper.getAlarms()
val timers = timerDb.getTimers()
if (alarms.isEmpty() && timers.isEmpty()) {
@@ -335,7 +338,7 @@ class SettingsActivity : SimpleActivity() {
ExportDataDialog(this, config.lastDataExportPath) { file ->
try {
exportActivityResultLauncher.launch(file.name)
- } catch (e: ActivityNotFoundException) {
+ } catch (@Suppress("SwallowedException") e: ActivityNotFoundException) {
toast(
id = org.fossify.commons.R.string.system_service_disabled,
length = Toast.LENGTH_LONG
@@ -349,7 +352,7 @@ class SettingsActivity : SimpleActivity() {
private fun tryImportData() {
try {
importActivityResultLauncher.launch(IMPORT_BACKUP_MIME_TYPES.toTypedArray())
- } catch (e: ActivityNotFoundException) {
+ } catch (@Suppress("SwallowedException") e: ActivityNotFoundException) {
toast(org.fossify.commons.R.string.system_service_disabled, Toast.LENGTH_LONG)
} catch (e: Exception) {
showErrorToast(e)
@@ -359,7 +362,7 @@ class SettingsActivity : SimpleActivity() {
private fun importData(uri: Uri) {
val result = ImportHelper(
context = this,
- dbHelper = DBHelper.dbInstance!!,
+ dbHelper = dbHelper,
timerHelper = TimerHelper(this)
).importData(uri)
diff --git a/app/src/main/kotlin/org/fossify/clock/activities/SnoozeReminderActivity.kt b/app/src/main/kotlin/org/fossify/clock/activities/SnoozeReminderActivity.kt
index 7f80b392..7439e73c 100644
--- a/app/src/main/kotlin/org/fossify/clock/activities/SnoozeReminderActivity.kt
+++ b/app/src/main/kotlin/org/fossify/clock/activities/SnoozeReminderActivity.kt
@@ -2,29 +2,27 @@ package org.fossify.clock.activities
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
+import org.fossify.clock.extensions.alarmController
import org.fossify.clock.extensions.config
-import org.fossify.clock.extensions.dbHelper
-import org.fossify.clock.extensions.hideNotification
-import org.fossify.clock.extensions.setupAlarmClock
import org.fossify.clock.helpers.ALARM_ID
import org.fossify.commons.extensions.showPickSecondsDialog
import org.fossify.commons.helpers.MINUTE_SECONDS
-import java.util.Calendar
class SnoozeReminderActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- val id = intent.getIntExtra(ALARM_ID, -1)
- val alarm = dbHelper.getAlarmWithId(id) ?: return
- hideNotification(id)
- showPickSecondsDialog(config.snoozeTime * MINUTE_SECONDS, true, cancelCallback = { dialogCancelled() }) {
+ val alarmId = intent.getIntExtra(ALARM_ID, -1)
+ alarmController.stopAlarm(alarmId = alarmId, disable = false)
+ showPickSecondsDialog(
+ curSeconds = config.snoozeTime * MINUTE_SECONDS,
+ isSnoozePicker = true,
+ cancelCallback = {
+ alarmController.stopAlarm(alarmId)
+ dialogCancelled()
+ }
+ ) {
config.snoozeTime = it / MINUTE_SECONDS
- setupAlarmClock(
- alarm = alarm,
- triggerTimeMillis = Calendar.getInstance()
- .apply { add(Calendar.SECOND, it) }
- .timeInMillis
- )
+ alarmController.snoozeAlarm(alarmId, config.snoozeTime)
finishActivity()
}
}
diff --git a/app/src/main/kotlin/org/fossify/clock/adapters/AlarmsAdapter.kt b/app/src/main/kotlin/org/fossify/clock/adapters/AlarmsAdapter.kt
index 407a7f60..bccbe0c9 100644
--- a/app/src/main/kotlin/org/fossify/clock/adapters/AlarmsAdapter.kt
+++ b/app/src/main/kotlin/org/fossify/clock/adapters/AlarmsAdapter.kt
@@ -15,7 +15,6 @@ import org.fossify.clock.extensions.dbHelper
import org.fossify.clock.extensions.getAlarmSelectedDaysString
import org.fossify.clock.extensions.getFormattedTime
import org.fossify.clock.extensions.swap
-import org.fossify.clock.helpers.TODAY_BIT
import org.fossify.clock.helpers.TOMORROW_BIT
import org.fossify.clock.helpers.getCurrentDayMinutes
import org.fossify.clock.interfaces.ToggleAlarmInterface
@@ -95,7 +94,11 @@ class AlarmsAdapter(
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val alarm = alarms[position]
- holder.bindView(alarm, true, true) { itemView, _ ->
+ holder.bindView(
+ any = alarm,
+ allowSingleClick = true,
+ allowLongClick = true
+ ) { itemView, _ ->
setupView(itemView, alarm, holder)
}
bindViewHolder(holder)
@@ -139,7 +142,11 @@ class AlarmsAdapter(
}
false
}
- alarmTime.text = activity.getFormattedTime(alarm.timeInMinutes * 60, false, true)
+ alarmTime.text = activity.getFormattedTime(
+ passedSeconds = alarm.timeInMinutes * 60,
+ showSeconds = false,
+ makeAmPmSmaller = true
+ )
alarmTime.setTextColor(textColor)
alarmDays.text = activity.getAlarmSelectedDaysString(alarm.days)
@@ -152,47 +159,51 @@ class AlarmsAdapter(
alarmSwitch.isChecked = alarm.isEnabled
alarmSwitch.setColors(textColor, properPrimaryColor, backgroundColor)
alarmSwitch.setOnClickListener {
- when {
- alarm.days > 0 -> {
- if (activity.config.wasAlarmWarningShown) {
- toggleAlarmInterface.alarmToggled(alarm.id, alarmSwitch.isChecked)
- } else {
- ConfirmationDialog(
- activity = activity,
- messageId = org.fossify.commons.R.string.alarm_warning,
- positive = org.fossify.commons.R.string.ok,
- negative = 0
- ) {
- activity.config.wasAlarmWarningShown = true
- toggleAlarmInterface.alarmToggled(alarm.id, alarmSwitch.isChecked)
- }
- }
- }
+ toggleAlarm(binding = this, alarm = alarm)
+ }
+ }
+ }
- alarm.days == TODAY_BIT -> {
- if (alarm.timeInMinutes <= getCurrentDayMinutes()) {
- alarm.days = TOMORROW_BIT
- alarmDays.text =
- resources.getString(org.fossify.commons.R.string.tomorrow)
- }
- activity.dbHelper.updateAlarm(alarm)
- toggleAlarmInterface.alarmToggled(alarm.id, alarmSwitch.isChecked)
+ private fun toggleAlarm(binding: ItemAlarmBinding, alarm: Alarm) {
+ when {
+ alarm.isRecurring() -> {
+ if (activity.config.wasAlarmWarningShown) {
+ toggleAlarmInterface.alarmToggled(alarm.id, binding.alarmSwitch.isChecked)
+ } else {
+ ConfirmationDialog(
+ activity = activity,
+ messageId = org.fossify.commons.R.string.alarm_warning,
+ positive = org.fossify.commons.R.string.ok,
+ negative = 0
+ ) {
+ activity.config.wasAlarmWarningShown = true
+ toggleAlarmInterface.alarmToggled(alarm.id, binding.alarmSwitch.isChecked)
}
+ }
+ }
- alarm.days == TOMORROW_BIT -> {
- toggleAlarmInterface.alarmToggled(alarm.id, alarmSwitch.isChecked)
- }
+ alarm.isToday() -> {
+ if (alarm.timeInMinutes <= getCurrentDayMinutes()) {
+ alarm.days = TOMORROW_BIT
+ binding.alarmDays.text =
+ resources.getString(org.fossify.commons.R.string.tomorrow)
+ }
+ activity.dbHelper.updateAlarm(alarm)
+ toggleAlarmInterface.alarmToggled(alarm.id, binding.alarmSwitch.isChecked)
+ }
- // Unreachable zombie branch. Days are always set to a non-zero value.
- alarmSwitch.isChecked -> {
- activity.toast(R.string.no_days_selected)
- alarmSwitch.isChecked = false
- }
+ alarm.isTomorrow() -> {
+ toggleAlarmInterface.alarmToggled(alarm.id, binding.alarmSwitch.isChecked)
+ }
- else -> {
- toggleAlarmInterface.alarmToggled(alarm.id, alarmSwitch.isChecked)
- }
- }
+ // Unreachable zombie branch. Days are always set to a non-zero value.
+ binding.alarmSwitch.isChecked -> {
+ activity.toast(R.string.no_days_selected)
+ binding.alarmSwitch.isChecked = false
+ }
+
+ else -> {
+ toggleAlarmInterface.alarmToggled(alarm.id, binding.alarmSwitch.isChecked)
}
}
}
diff --git a/app/src/main/kotlin/org/fossify/clock/dialogs/EditAlarmDialog.kt b/app/src/main/kotlin/org/fossify/clock/dialogs/EditAlarmDialog.kt
index 4f8f1eb0..adac6d4f 100644
--- a/app/src/main/kotlin/org/fossify/clock/dialogs/EditAlarmDialog.kt
+++ b/app/src/main/kotlin/org/fossify/clock/dialogs/EditAlarmDialog.kt
@@ -4,7 +4,6 @@ import android.app.TimePickerDialog
import android.graphics.drawable.Drawable
import android.media.AudioManager
import android.media.RingtoneManager
-import android.text.format.DateFormat
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import com.google.android.material.timepicker.MaterialTimePicker
@@ -12,7 +11,13 @@ import com.google.android.material.timepicker.TimeFormat
import org.fossify.clock.R
import org.fossify.clock.activities.SimpleActivity
import org.fossify.clock.databinding.DialogEditAlarmBinding
-import org.fossify.clock.extensions.*
+import org.fossify.clock.extensions.checkAlarmsWithDeletedSoundUri
+import org.fossify.clock.extensions.colorCompoundDrawable
+import org.fossify.clock.extensions.config
+import org.fossify.clock.extensions.dbHelper
+import org.fossify.clock.extensions.getFormattedTime
+import org.fossify.clock.extensions.handleFullScreenNotificationsPermission
+import org.fossify.clock.extensions.orderDaysList
import org.fossify.clock.helpers.PICK_AUDIO_FILE_INTENT_ID
import org.fossify.clock.helpers.TODAY_BIT
import org.fossify.clock.helpers.TOMORROW_BIT
@@ -20,10 +25,26 @@ import org.fossify.clock.helpers.getCurrentDayMinutes
import org.fossify.clock.models.Alarm
import org.fossify.commons.dialogs.ConfirmationDialog
import org.fossify.commons.dialogs.SelectAlarmSoundDialog
-import org.fossify.commons.extensions.*
+import org.fossify.commons.extensions.addBit
+import org.fossify.commons.extensions.applyColorFilter
+import org.fossify.commons.extensions.beVisibleIf
+import org.fossify.commons.extensions.getAlertDialogBuilder
+import org.fossify.commons.extensions.getDefaultAlarmSound
+import org.fossify.commons.extensions.getProperBackgroundColor
+import org.fossify.commons.extensions.getProperTextColor
+import org.fossify.commons.extensions.getTimePickerDialogTheme
+import org.fossify.commons.extensions.removeBit
+import org.fossify.commons.extensions.setupDialogStuff
+import org.fossify.commons.extensions.toast
+import org.fossify.commons.extensions.value
import org.fossify.commons.models.AlarmSound
-class EditAlarmDialog(val activity: SimpleActivity, val alarm: Alarm, val onDismiss: () -> Unit = {}, val callback: (alarmId: Int) -> Unit) {
+class EditAlarmDialog(
+ val activity: SimpleActivity,
+ val alarm: Alarm,
+ val onDismiss: () -> Unit = {},
+ val callback: (alarmId: Int) -> Unit,
+) {
private val binding = DialogEditAlarmBinding.inflate(activity.layoutInflater)
private val textColor = activity.getProperTextColor()
@@ -67,14 +88,22 @@ class EditAlarmDialog(val activity: SimpleActivity, val alarm: Alarm, val onDism
editAlarmSound.colorCompoundDrawable(textColor)
editAlarmSound.text = alarm.soundTitle
editAlarmSound.setOnClickListener {
- SelectAlarmSoundDialog(activity, alarm.soundUri, AudioManager.STREAM_ALARM, PICK_AUDIO_FILE_INTENT_ID, RingtoneManager.TYPE_ALARM, true,
+ SelectAlarmSoundDialog(
+ activity = activity,
+ currentUri = alarm.soundUri,
+ audioStream = AudioManager.STREAM_ALARM,
+ pickAudioIntentId = PICK_AUDIO_FILE_INTENT_ID,
+ type = RingtoneManager.TYPE_ALARM,
+ loopAudio = true,
onAlarmPicked = {
if (it != null) {
updateSelectedAlarmSound(it)
}
- }, onAlarmSoundDeleted = {
+ },
+ onAlarmSoundDeleted = {
if (alarm.soundUri == it.uri) {
- val defaultAlarm = root.context.getDefaultAlarmSound(RingtoneManager.TYPE_ALARM)
+ val defaultAlarm =
+ root.context.getDefaultAlarmSound(RingtoneManager.TYPE_ALARM)
updateSelectedAlarmSound(defaultAlarm)
}
activity.checkAlarmsWithDeletedSoundUri(it.uri)
@@ -91,28 +120,32 @@ class EditAlarmDialog(val activity: SimpleActivity, val alarm: Alarm, val onDism
editAlarmLabelImage.applyColorFilter(textColor)
editAlarm.setText(alarm.label)
- val dayLetters = activity.resources.getStringArray(org.fossify.commons.R.array.week_day_letters).toList() as ArrayList
+ val dayLetters =
+ activity.resources.getStringArray(org.fossify.commons.R.array.week_day_letters)
+ .toList() as ArrayList
val dayIndexes = activity.orderDaysList(arrayListOf(0, 1, 2, 3, 4, 5, 6))
dayIndexes.forEach {
- val pow = Math.pow(2.0, it.toDouble()).toInt()
- val day = activity.layoutInflater.inflate(R.layout.alarm_day, editAlarmDaysHolder, false) as TextView
+ val bitmask = 1 shl it
+ val day = activity.layoutInflater.inflate(
+ R.layout.alarm_day, editAlarmDaysHolder, false
+ ) as TextView
day.text = dayLetters[it]
- val isDayChecked = alarm.days > 0 && alarm.days and pow != 0
+ val isDayChecked = alarm.isRecurring() && alarm.days and bitmask != 0
day.background = getProperDayDrawable(isDayChecked)
day.setTextColor(if (isDayChecked) root.context.getProperBackgroundColor() else textColor)
day.setOnClickListener {
- if (alarm.days < 0) {
+ if (!alarm.isRecurring()) {
alarm.days = 0
}
- val selectDay = alarm.days and pow == 0
+ val selectDay = alarm.days and bitmask == 0
if (selectDay) {
- alarm.days = alarm.days.addBit(pow)
+ alarm.days = alarm.days.addBit(bitmask)
} else {
- alarm.days = alarm.days.removeBit(pow)
+ alarm.days = alarm.days.removeBit(bitmask)
}
day.background = getProperDayDrawable(selectDay)
day.setTextColor(if (selectDay) root.context.getProperBackgroundColor() else textColor)
@@ -132,7 +165,7 @@ class EditAlarmDialog(val activity: SimpleActivity, val alarm: Alarm, val onDism
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
if (!activity.config.wasAlarmWarningShown) {
ConfirmationDialog(
- activity,
+ activity = activity,
messageId = org.fossify.commons.R.string.alarm_warning,
positive = org.fossify.commons.R.string.ok,
negative = 0
@@ -193,7 +226,7 @@ class EditAlarmDialog(val activity: SimpleActivity, val alarm: Alarm, val onDism
}
}
- private val timeSetListener = TimePickerDialog.OnTimeSetListener { view, hourOfDay, minute ->
+ private val timeSetListener = TimePickerDialog.OnTimeSetListener { _, hourOfDay, minute ->
timePicked(hourOfDay, minute)
}
@@ -203,7 +236,11 @@ class EditAlarmDialog(val activity: SimpleActivity, val alarm: Alarm, val onDism
}
private fun updateAlarmTime() {
- binding.editAlarmTime.text = activity.getFormattedTime(alarm.timeInMinutes * 60, false, true)
+ binding.editAlarmTime.text = activity.getFormattedTime(
+ passedSeconds = alarm.timeInMinutes * 60,
+ showSeconds = false,
+ makeAmPmSmaller = true
+ )
checkDaylessAlarm()
}
@@ -221,7 +258,12 @@ class EditAlarmDialog(val activity: SimpleActivity, val alarm: Alarm, val onDism
}
private fun getProperDayDrawable(selected: Boolean): Drawable {
- val drawableId = if (selected) R.drawable.circle_background_filled else R.drawable.circle_background_stroke
+ val drawableId = if (selected) {
+ R.drawable.circle_background_filled
+ } else {
+ R.drawable.circle_background_stroke
+ }
+
val drawable = activity.resources.getDrawable(drawableId)
drawable.applyColorFilter(textColor)
return drawable
diff --git a/app/src/main/kotlin/org/fossify/clock/extensions/BroadcastReceiver.kt b/app/src/main/kotlin/org/fossify/clock/extensions/BroadcastReceiver.kt
new file mode 100644
index 00000000..213dc303
--- /dev/null
+++ b/app/src/main/kotlin/org/fossify/clock/extensions/BroadcastReceiver.kt
@@ -0,0 +1,20 @@
+package org.fossify.clock.extensions
+
+import android.content.BroadcastReceiver
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.launch
+
+private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
+
+fun BroadcastReceiver.goAsync(callback: suspend () -> Unit) {
+ val pendingResult = goAsync()
+ coroutineScope.launch {
+ try {
+ callback()
+ } finally {
+ pendingResult.finish()
+ }
+ }
+}
diff --git a/app/src/main/kotlin/org/fossify/clock/extensions/Context.kt b/app/src/main/kotlin/org/fossify/clock/extensions/Context.kt
index c3e1ab46..ffd03b69 100644
--- a/app/src/main/kotlin/org/fossify/clock/extensions/Context.kt
+++ b/app/src/main/kotlin/org/fossify/clock/extensions/Context.kt
@@ -14,7 +14,6 @@ import android.media.AudioManager.STREAM_ALARM
import android.media.RingtoneManager
import android.os.Handler
import android.os.Looper
-import android.os.PowerManager
import android.text.SpannableString
import android.text.style.RelativeSizeSpan
import android.widget.Toast
@@ -22,12 +21,11 @@ import androidx.core.app.AlarmManagerCompat
import androidx.core.app.NotificationCompat
import androidx.core.net.toUri
import org.fossify.clock.R
-import org.fossify.clock.activities.ReminderActivity
import org.fossify.clock.activities.SnoozeReminderActivity
import org.fossify.clock.activities.SplashActivity
import org.fossify.clock.databases.AppDatabase
import org.fossify.clock.helpers.ALARM_ID
-import org.fossify.clock.helpers.ALARM_NOTIFICATION_CHANNEL_ID
+import org.fossify.clock.helpers.AlarmController
import org.fossify.clock.helpers.Config
import org.fossify.clock.helpers.DBHelper
import org.fossify.clock.helpers.EARLY_ALARM_DISMISSAL_INTENT_ID
@@ -40,7 +38,6 @@ import org.fossify.clock.helpers.NOTIFICATION_ID
import org.fossify.clock.helpers.OPEN_ALARMS_TAB_INTENT_ID
import org.fossify.clock.helpers.OPEN_STOPWATCH_TAB_INTENT_ID
import org.fossify.clock.helpers.OPEN_TAB
-import org.fossify.clock.helpers.REMINDER_ACTIVITY_INTENT_ID
import org.fossify.clock.helpers.TAB_ALARM
import org.fossify.clock.helpers.TAB_STOPWATCH
import org.fossify.clock.helpers.TAB_TIMER
@@ -50,21 +47,19 @@ import org.fossify.clock.helpers.TOMORROW_BIT
import org.fossify.clock.helpers.TimerHelper
import org.fossify.clock.helpers.formatTime
import org.fossify.clock.helpers.getAllTimeZones
-import org.fossify.clock.helpers.getCurrentDayMinutes
import org.fossify.clock.helpers.getDefaultTimeZoneTitle
-import org.fossify.clock.helpers.getPassedSeconds
import org.fossify.clock.helpers.getTimeOfNextAlarm
import org.fossify.clock.interfaces.TimerDao
import org.fossify.clock.models.Alarm
-import org.fossify.clock.models.AlarmEvent
import org.fossify.clock.models.MyTimeZone
import org.fossify.clock.models.Timer
import org.fossify.clock.models.TimerState
import org.fossify.clock.receivers.AlarmReceiver
-import org.fossify.clock.receivers.DismissAlarmReceiver
-import org.fossify.clock.receivers.EarlyAlarmDismissalReceiver
-import org.fossify.clock.receivers.HideAlarmReceiver
import org.fossify.clock.receivers.HideTimerReceiver
+import org.fossify.clock.receivers.SkipUpcomingAlarmReceiver
+import org.fossify.clock.receivers.StopAlarmReceiver
+import org.fossify.clock.receivers.UpcomingAlarmReceiver
+import org.fossify.clock.services.AlarmService
import org.fossify.clock.services.SnoozeService
import org.fossify.commons.extensions.formatMinutesToTimeString
import org.fossify.commons.extensions.formatSecondsToTimeString
@@ -73,6 +68,7 @@ import org.fossify.commons.extensions.getLaunchIntent
import org.fossify.commons.extensions.getProperPrimaryColor
import org.fossify.commons.extensions.getSelectedDaysString
import org.fossify.commons.extensions.grantReadUriPermission
+import org.fossify.commons.extensions.notificationManager
import org.fossify.commons.extensions.showErrorToast
import org.fossify.commons.extensions.toInt
import org.fossify.commons.extensions.toast
@@ -88,7 +84,6 @@ import org.fossify.commons.helpers.TUESDAY_BIT
import org.fossify.commons.helpers.WEDNESDAY_BIT
import org.fossify.commons.helpers.ensureBackgroundThread
import org.fossify.commons.helpers.isOreoPlus
-import org.greenrobot.eventbus.EventBus
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
@@ -96,14 +91,26 @@ import kotlin.math.ceil
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
-val Context.config: Config get() = Config.newInstance(applicationContext)
+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)
+val Context.dbHelper: DBHelper
+ get() = DBHelper.newInstance(applicationContext)
+
+val Context.timerDb: TimerDao
+ get() = AppDatabase.getInstance(applicationContext).TimerDao()
+
+val Context.timerHelper: TimerHelper
+ get() = TimerHelper(this)
+
+val Context.alarmManager: AlarmManager
+ get() = getSystemService(Context.ALARM_SERVICE) as AlarmManager
+
+val Context.alarmController: AlarmController
+ get() = AlarmController.getInstance(applicationContext)
fun Context.getFormattedDate(calendar: Calendar): String {
- val dayOfWeek = (calendar.get(Calendar.DAY_OF_WEEK) + 5) % 7 // make sure index 0 means monday
+ val dayOfWeek = (calendar.get(Calendar.DAY_OF_WEEK) + 5) % 7 // make sure index 0 means monday
val dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH)
val month = calendar.get(Calendar.MONTH)
@@ -135,38 +142,39 @@ fun Context.getAllTimeZonesModified(): ArrayList {
return timeZones
}
-fun Context.getModifiedTimeZoneTitle(id: Int) = getAllTimeZonesModified().firstOrNull { it.id == id }?.title ?: getDefaultTimeZoneTitle(id)
+fun Context.getModifiedTimeZoneTitle(id: Int): String {
+ return getAllTimeZonesModified()
+ .firstOrNull { it.id == id }?.title ?: getDefaultTimeZoneTitle(id)
+}
fun Context.createNewAlarm(timeInMinutes: Int, weekDays: Int): Alarm {
val defaultAlarmSound = getDefaultAlarmSound(RingtoneManager.TYPE_ALARM)
- return Alarm(0, timeInMinutes, weekDays, false, false, defaultAlarmSound.title, defaultAlarmSound.uri, "")
+ return Alarm(
+ id = 0,
+ timeInMinutes = timeInMinutes,
+ days = weekDays,
+ isEnabled = false,
+ vibrate = false,
+ soundTitle = defaultAlarmSound.title,
+ soundUri = defaultAlarmSound.uri,
+ label = ""
+ )
}
fun Context.createNewTimer(): Timer {
return Timer(
- null,
- config.timerSeconds,
- TimerState.Idle,
- config.timerVibrate,
- config.timerSoundUri,
- config.timerSoundTitle,
- config.timerLabel ?: "",
- System.currentTimeMillis(),
- config.timerChannelId,
+ id = null,
+ seconds = config.timerSeconds,
+ state = TimerState.Idle,
+ vibrate = config.timerVibrate,
+ soundUri = config.timerSoundUri,
+ soundTitle = config.timerSoundTitle,
+ label = config.timerLabel ?: "",
+ createdAt = System.currentTimeMillis(),
+ channelId = config.timerChannelId,
)
}
-fun Context.scheduleNextAlarm(alarm: Alarm, showToast: Boolean) {
- val triggerTimeMillis = getTimeOfNextAlarm(alarm)?.timeInMillis ?: return
- setupAlarmClock(alarm = alarm, triggerTimeMillis = triggerTimeMillis)
-
- if (showToast) {
- val now = Calendar.getInstance()
- val triggerInMillis = triggerTimeMillis - now.timeInMillis
- showRemainingTimeMessage(triggerInMillis)
- }
-}
-
fun Context.showRemainingTimeMessage(triggerInMillis: Long) {
val totalSeconds = triggerInMillis.milliseconds.inWholeSeconds.toInt()
val remainingTime = if (totalSeconds >= MINUTE_SECONDS) {
@@ -185,74 +193,100 @@ fun Context.showRemainingTimeMessage(triggerInMillis: Long) {
}
fun Context.setupAlarmClock(alarm: Alarm, triggerTimeMillis: Long) {
- val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
-
+ val alarmManager = alarmManager
try {
- AlarmManagerCompat.setAlarmClock(alarmManager, triggerTimeMillis, getOpenAlarmTabIntent(), getAlarmIntent(alarm))
+ AlarmManagerCompat.setAlarmClock(
+ alarmManager,
+ triggerTimeMillis,
+ getOpenAlarmTabIntent(),
+ getAlarmIntent(alarm)
+ )
// show a notification to allow dismissing the alarm 10 minutes before it actually triggers
- val dismissalTriggerTime = if (triggerTimeMillis - System.currentTimeMillis() < 10.minutes.inWholeMilliseconds) {
- System.currentTimeMillis() + 500
- } else {
- triggerTimeMillis - 10.minutes.inWholeMilliseconds
- }
- AlarmManagerCompat.setExactAndAllowWhileIdle(alarmManager, 0, dismissalTriggerTime, getEarlyAlarmDismissalIntent(alarm))
+ val dismissalTriggerTime =
+ if (triggerTimeMillis - System.currentTimeMillis() < 10.minutes.inWholeMilliseconds) {
+ System.currentTimeMillis() + 500
+ } else {
+ triggerTimeMillis - 10.minutes.inWholeMilliseconds
+ }
+
+ AlarmManagerCompat.setExactAndAllowWhileIdle(
+ alarmManager,
+ 0,
+ dismissalTriggerTime,
+ getUpcomingAlarmPendingIntent(alarm)
+ )
} catch (e: Exception) {
showErrorToast(e)
}
}
-fun Context.getEarlyAlarmDismissalIntent(alarm: Alarm): PendingIntent {
- val intent = Intent(this, EarlyAlarmDismissalReceiver::class.java).apply {
+fun Context.getUpcomingAlarmPendingIntent(alarm: Alarm): PendingIntent {
+ val intent = Intent(this, UpcomingAlarmReceiver::class.java).apply {
putExtra(ALARM_ID, alarm.id)
}
- return PendingIntent.getBroadcast(this, EARLY_ALARM_DISMISSAL_INTENT_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+
+ return PendingIntent.getBroadcast(
+ this,
+ EARLY_ALARM_DISMISSAL_INTENT_ID,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
}
fun Context.getOpenAlarmTabIntent(): PendingIntent {
val intent = getLaunchIntent() ?: Intent(this, SplashActivity::class.java)
intent.putExtra(OPEN_TAB, TAB_ALARM)
- return PendingIntent.getActivity(this, OPEN_ALARMS_TAB_INTENT_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+ return PendingIntent.getActivity(
+ this,
+ OPEN_ALARMS_TAB_INTENT_ID,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
}
fun Context.getOpenTimerTabIntent(timerId: Int): PendingIntent {
val intent = getLaunchIntent() ?: Intent(this, SplashActivity::class.java)
intent.putExtra(OPEN_TAB, TAB_TIMER)
intent.putExtra(TIMER_ID, timerId)
- return PendingIntent.getActivity(this, timerId, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+ return PendingIntent.getActivity(
+ this,
+ timerId,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
}
fun Context.getOpenStopwatchTabIntent(): PendingIntent {
val intent = getLaunchIntent() ?: Intent(this, SplashActivity::class.java)
intent.putExtra(OPEN_TAB, TAB_STOPWATCH)
- return PendingIntent.getActivity(this, OPEN_STOPWATCH_TAB_INTENT_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+ return PendingIntent.getActivity(
+ this,
+ OPEN_STOPWATCH_TAB_INTENT_ID,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
}
fun Context.getAlarmIntent(alarm: Alarm): PendingIntent {
val intent = Intent(this, AlarmReceiver::class.java)
intent.putExtra(ALARM_ID, alarm.id)
- return PendingIntent.getBroadcast(this, alarm.id, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+ return PendingIntent.getBroadcast(
+ this,
+ alarm.id,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
}
fun Context.cancelAlarmClock(alarm: Alarm) {
- val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
+ val alarmManager = alarmManager
alarmManager.cancel(getAlarmIntent(alarm))
- alarmManager.cancel(getEarlyAlarmDismissalIntent(alarm))
+ alarmManager.cancel(getUpcomingAlarmPendingIntent(alarm))
}
fun Context.hideNotification(id: Int) {
- val manager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- manager.cancel(id)
-}
-
-fun Context.deleteNotificationChannel(channelId: String) {
- if (isOreoPlus()) {
- try {
- val manager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- manager.deleteNotificationChannel(channelId)
- } catch (_: Throwable) {
- }
- }
+ notificationManager.cancel(id)
}
fun Context.hideTimerNotification(timerId: Int) = hideNotification(timerId)
@@ -264,7 +298,9 @@ fun Context.updateWidgets() {
fun Context.updateDigitalWidgets() {
val component = ComponentName(applicationContext, MyDigitalTimeWidgetProvider::class.java)
- val widgetIds = AppWidgetManager.getInstance(applicationContext)?.getAppWidgetIds(component) ?: return
+ val widgetIds = AppWidgetManager.getInstance(applicationContext)
+ ?.getAppWidgetIds(component) ?: return
+
if (widgetIds.isNotEmpty()) {
val ids = intArrayOf(R.xml.widget_digital_clock_info)
Intent(applicationContext, MyDigitalTimeWidgetProvider::class.java).apply {
@@ -277,7 +313,9 @@ fun Context.updateDigitalWidgets() {
fun Context.updateAnalogueWidgets() {
val component = ComponentName(applicationContext, MyAnalogueTimeWidgetProvider::class.java)
- val widgetIds = AppWidgetManager.getInstance(applicationContext)?.getAppWidgetIds(component) ?: return
+ val widgetIds = AppWidgetManager.getInstance(applicationContext)
+ ?.getAppWidgetIds(component) ?: return
+
if (widgetIds.isNotEmpty()) {
val ids = intArrayOf(R.xml.widget_analogue_clock_info)
Intent(applicationContext, MyAnalogueTimeWidgetProvider::class.java).apply {
@@ -288,26 +326,57 @@ fun Context.updateAnalogueWidgets() {
}
}
-fun Context.getFormattedTime(passedSeconds: Int, showSeconds: Boolean, makeAmPmSmaller: Boolean): SpannableString {
+fun Context.getFormattedTime(
+ passedSeconds: Int,
+ showSeconds: Boolean,
+ makeAmPmSmaller: Boolean,
+): SpannableString {
val use24HourFormat = config.use24HourFormat
val hours = (passedSeconds / 3600) % 24
val minutes = (passedSeconds / 60) % 60
val seconds = passedSeconds % 60
return if (use24HourFormat) {
- val formattedTime = formatTime(showSeconds, use24HourFormat, hours, minutes, seconds)
+ val formattedTime = formatTime(
+ showSeconds = showSeconds,
+ use24HourFormat = true,
+ hours = hours,
+ minutes = minutes,
+ seconds = seconds
+ )
SpannableString(formattedTime)
} else {
- val formattedTime = formatTo12HourFormat(showSeconds, hours, minutes, seconds)
+ val formattedTime = formatTo12HourFormat(
+ showSeconds = showSeconds,
+ hours = hours,
+ minutes = minutes,
+ seconds = seconds
+ )
val spannableTime = SpannableString(formattedTime)
val amPmMultiplier = if (makeAmPmSmaller) 0.4f else 1f
- spannableTime.setSpan(RelativeSizeSpan(amPmMultiplier), spannableTime.length - 3, spannableTime.length, 0)
+ spannableTime.setSpan(
+ RelativeSizeSpan(amPmMultiplier),
+ spannableTime.length - 3,
+ spannableTime.length,
+ 0
+ )
spannableTime
}
}
-fun Context.formatTo12HourFormat(showSeconds: Boolean, hours: Int, minutes: Int, seconds: Int): String {
- val appendable = getString(if (hours >= 12) org.fossify.commons.R.string.p_m else org.fossify.commons.R.string.a_m)
+fun Context.formatTo12HourFormat(
+ showSeconds: Boolean,
+ hours: Int,
+ minutes: Int,
+ seconds: Int,
+): String {
+ val appendable = getString(
+ if (hours >= 12) {
+ org.fossify.commons.R.string.p_m
+ } else {
+ org.fossify.commons.R.string.a_m
+ }
+ )
val newHours = if (hours == 0 || hours == 12) 12 else hours % 12
return "${formatTime(showSeconds, false, newHours, minutes, seconds)} $appendable"
}
@@ -331,14 +400,16 @@ fun Context.getClosestEnabledAlarmString(callback: (result: String) -> Unit) {
}
val dayOfWeekIndex = (closestAlarmTime.get(Calendar.DAY_OF_WEEK) + 5) % 7
- val dayOfWeek = resources.getStringArray(org.fossify.commons.R.array.week_days_short)[dayOfWeekIndex]
+ val dayOfWeek =
+ resources.getStringArray(org.fossify.commons.R.array.week_days_short)[dayOfWeekIndex]
val pattern = if (config.use24HourFormat) {
FORMAT_24H
} else {
FORMAT_12H
}
- val formattedTime = SimpleDateFormat(pattern, Locale.getDefault()).format(closestAlarmTime.time)
+ val formattedTime =
+ SimpleDateFormat(pattern, Locale.getDefault()).format(closestAlarmTime.time)
callback("$dayOfWeek $formattedTime")
}
}
@@ -352,32 +423,7 @@ fun Context.getEnabledAlarms(callback: (result: List?) -> Unit) {
}
}
-fun Context.rescheduleEnabledAlarms() {
- dbHelper.getEnabledAlarms().forEach {
- if (it.days != TODAY_BIT || it.timeInMinutes > getCurrentDayMinutes()) {
- scheduleNextAlarm(it, false)
- }
- }
-}
-
-fun Context.isScreenOn() = (getSystemService(Context.POWER_SERVICE) as PowerManager).isScreenOn
-
-fun Context.showAlarmNotification(alarm: Alarm) {
- val pendingIntent = getOpenAlarmTabIntent()
- val notification = getAlarmNotification(pendingIntent, alarm)
- val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- try {
- notificationManager.notify(alarm.id, notification)
- } catch (e: Exception) {
- showErrorToast(e)
- }
-
- if (alarm.days > 0) {
- scheduleNextAlarm(alarm, false)
- }
-}
-
-fun Context.getTimerNotification(timer: Timer, pendingIntent: PendingIntent, addDeleteIntent: Boolean): Notification {
+fun Context.getTimerNotification(timer: Timer, pendingIntent: PendingIntent): Notification {
var soundUri = timer.soundUri
if (soundUri == SILENT) {
soundUri = ""
@@ -385,8 +431,8 @@ fun Context.getTimerNotification(timer: Timer, pendingIntent: PendingIntent, add
grantReadUriPermission(soundUri)
}
- val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- val channelId = timer.channelId ?: "simple_timer_channel_${soundUri}_${System.currentTimeMillis()}"
+ val channelId =
+ timer.channelId ?: "simple_timer_channel_${soundUri}_${System.currentTimeMillis()}"
timerHelper.insertOrUpdateTimer(timer.copy(channelId = channelId))
if (isOreoPlus()) {
@@ -418,11 +464,7 @@ fun Context.getTimerNotification(timer: Timer, pendingIntent: PendingIntent, add
}
}
- val title = timer.label.ifEmpty {
- getString(R.string.timer)
- }
-
- val reminderActivityIntent = getReminderActivityIntent()
+ val title = timer.label.ifEmpty { getString(R.string.timer) }
val builder = NotificationCompat.Builder(this)
.setContentTitle(title)
.setContentText(getString(R.string.time_expired))
@@ -431,23 +473,14 @@ fun Context.getTimerNotification(timer: Timer, pendingIntent: PendingIntent, add
.setPriority(NotificationCompat.PRIORITY_MAX)
.setDefaults(Notification.DEFAULT_LIGHTS)
.setCategory(Notification.CATEGORY_EVENT)
- .setAutoCancel(true)
.setSound(soundUri.toUri(), STREAM_ALARM)
.setChannelId(channelId)
.addAction(
org.fossify.commons.R.drawable.ic_cross_vector,
getString(org.fossify.commons.R.string.dismiss),
- if (addDeleteIntent) {
- reminderActivityIntent
- } else {
- getHideTimerPendingIntent(timer.id!!)
- }
+ getHideTimerPendingIntent(timer.id!!)
)
- if (addDeleteIntent) {
- builder.setDeleteIntent(reminderActivityIntent)
- }
-
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
if (timer.vibrate) {
@@ -463,104 +496,65 @@ fun Context.getTimerNotification(timer: Timer, pendingIntent: PendingIntent, add
fun Context.getHideTimerPendingIntent(timerId: Int): PendingIntent {
val intent = Intent(this, HideTimerReceiver::class.java)
intent.putExtra(TIMER_ID, timerId)
- return PendingIntent.getBroadcast(this, timerId, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+ return PendingIntent.getBroadcast(
+ this,
+ timerId,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
}
-fun Context.getHideAlarmPendingIntent(alarm: Alarm, channelId: String): PendingIntent {
- val intent = Intent(this, HideAlarmReceiver::class.java).apply {
+fun Context.getStopAlarmPendingIntent(alarm: Alarm): PendingIntent {
+ val intent = Intent(this, StopAlarmReceiver::class.java).apply {
putExtra(ALARM_ID, alarm.id)
- putExtra(ALARM_NOTIFICATION_CHANNEL_ID, channelId)
}
- return PendingIntent.getBroadcast(this, alarm.id, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+ return PendingIntent.getBroadcast(
+ this,
+ alarm.id,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
}
-fun Context.getDismissAlarmPendingIntent(alarmId: Int, notificationId: Int): PendingIntent {
- val intent = Intent(this, DismissAlarmReceiver::class.java).apply {
+fun Context.getSkipUpcomingAlarmPendingIntent(alarmId: Int, notificationId: Int): PendingIntent {
+ val intent = Intent(this, SkipUpcomingAlarmReceiver::class.java).apply {
putExtra(ALARM_ID, alarmId)
putExtra(NOTIFICATION_ID, notificationId)
}
- return PendingIntent.getBroadcast(this, alarmId, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+ return PendingIntent.getBroadcast(
+ this,
+ alarmId,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
}
-fun Context.getAlarmNotification(pendingIntent: PendingIntent, alarm: Alarm): Notification {
- val soundUri = alarm.soundUri
- if (soundUri != SILENT) {
- grantReadUriPermission(soundUri)
- }
- val channelId = "simple_alarm_channel_${soundUri}_${alarm.vibrate}"
- val label = alarm.label.ifEmpty {
- getString(org.fossify.commons.R.string.alarm)
- }
-
- if (isOreoPlus()) {
- val audioAttributes = AudioAttributes.Builder()
- .setUsage(AudioAttributes.USAGE_ALARM)
- .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
- .setLegacyStreamType(STREAM_ALARM)
- .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
- .build()
-
- val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- val importance = NotificationManager.IMPORTANCE_HIGH
- NotificationChannel(channelId, label, importance).apply {
- setBypassDnd(true)
- enableLights(true)
- lightColor = getProperPrimaryColor()
- enableVibration(alarm.vibrate)
- setSound(soundUri.toUri(), audioAttributes)
- notificationManager.createNotificationChannel(this)
- }
- }
-
- val dismissIntent = getHideAlarmPendingIntent(alarm, channelId)
- val builder = NotificationCompat.Builder(this)
- .setContentTitle(label)
- .setContentText(getFormattedTime(getPassedSeconds(), false, false))
- .setSmallIcon(R.drawable.ic_alarm_vector)
- .setContentIntent(pendingIntent)
- .setPriority(Notification.PRIORITY_HIGH)
- .setDefaults(Notification.DEFAULT_LIGHTS)
- .setAutoCancel(true)
- .setChannelId(channelId)
- .addAction(
- org.fossify.commons.R.drawable.ic_snooze_vector,
- getString(org.fossify.commons.R.string.snooze),
- getSnoozePendingIntent(alarm)
- )
- .addAction(org.fossify.commons.R.drawable.ic_cross_vector, getString(org.fossify.commons.R.string.dismiss), dismissIntent)
- .setDeleteIntent(dismissIntent)
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
-
- if (soundUri != SILENT) {
- builder.setSound(soundUri.toUri(), STREAM_ALARM)
- }
-
- if (alarm.vibrate) {
- val vibrateArray = LongArray(2) { 500 }
- builder.setVibrate(vibrateArray)
+fun Context.getSnoozePendingIntent(alarm: Alarm): PendingIntent {
+ val snoozeClass = if (config.useSameSnooze) {
+ SnoozeService::class.java
+ } else {
+ SnoozeReminderActivity::class.java
}
- val notification = builder.build()
- notification.flags = notification.flags or Notification.FLAG_INSISTENT
- return notification
-}
-
-fun Context.getSnoozePendingIntent(alarm: Alarm): PendingIntent {
- val snoozeClass = if (config.useSameSnooze) SnoozeService::class.java else SnoozeReminderActivity::class.java
val intent = Intent(this, snoozeClass).setAction("Snooze")
intent.putExtra(ALARM_ID, alarm.id)
return if (config.useSameSnooze) {
- PendingIntent.getService(this, alarm.id, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+ PendingIntent.getService(
+ this,
+ alarm.id,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
} else {
- PendingIntent.getActivity(this, alarm.id, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+ PendingIntent.getActivity(
+ this,
+ alarm.id,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
}
}
-fun Context.getReminderActivityIntent(): PendingIntent {
- val intent = Intent(this, ReminderActivity::class.java)
- return PendingIntent.getActivity(this, REMINDER_ACTIVITY_INTENT_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
-}
-
fun Context.checkAlarmsWithDeletedSoundUri(uri: String) {
val defaultAlarmSound = getDefaultAlarmSound(RingtoneManager.TYPE_ALARM)
dbHelper.getAlarmsWithUri(uri).forEach {
@@ -592,7 +586,17 @@ fun Context.firstDayOrder(bitMask: Int): Int {
if (bitMask == TODAY_BIT) return -2
if (bitMask == TOMORROW_BIT) return -1
- val dayBits = orderDaysList(arrayListOf(MONDAY_BIT, TUESDAY_BIT, WEDNESDAY_BIT, THURSDAY_BIT, FRIDAY_BIT, SATURDAY_BIT, SUNDAY_BIT))
+ val dayBits = orderDaysList(
+ arrayListOf(
+ MONDAY_BIT,
+ TUESDAY_BIT,
+ WEDNESDAY_BIT,
+ THURSDAY_BIT,
+ FRIDAY_BIT,
+ SATURDAY_BIT,
+ SUNDAY_BIT
+ )
+ )
dayBits.forEachIndexed { i, bit ->
if (bitMask and bit != 0) {
@@ -603,16 +607,26 @@ fun Context.firstDayOrder(bitMask: Int): Int {
return bitMask
}
-fun Context.disableExpiredAlarm(alarm: Alarm) {
- if (alarm.days < 0) {
- if (alarm.oneShot) {
- alarm.isEnabled = false
- dbHelper.deleteAlarms(arrayListOf(alarm))
- } else {
- dbHelper.updateAlarmEnabledState(alarm.id, false)
+fun Context.startAlarmService(alarmId: Int) {
+ try {
+ Intent(this, AlarmService::class.java).apply {
+ putExtra(ALARM_ID, alarmId)
+ if (isOreoPlus()) {
+ startForegroundService(this)
+ } else {
+ startService(this)
+ }
}
+ } catch (e: Exception) {
+ showErrorToast(e)
+ }
+}
- updateWidgets()
- EventBus.getDefault().post(AlarmEvent.Refresh)
+fun Context.stopAlarmService() {
+ try {
+ val serviceIntent = Intent(this, AlarmService::class.java)
+ stopService(serviceIntent)
+ } catch (e: Exception) {
+ showErrorToast(e)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/kotlin/org/fossify/clock/fragments/AlarmFragment.kt b/app/src/main/kotlin/org/fossify/clock/fragments/AlarmFragment.kt
index 5af7b6e9..4838deaa 100644
--- a/app/src/main/kotlin/org/fossify/clock/fragments/AlarmFragment.kt
+++ b/app/src/main/kotlin/org/fossify/clock/fragments/AlarmFragment.kt
@@ -11,13 +11,13 @@ import org.fossify.clock.adapters.AlarmsAdapter
import org.fossify.clock.databinding.FragmentAlarmBinding
import org.fossify.clock.dialogs.ChangeAlarmSortDialog
import org.fossify.clock.dialogs.EditAlarmDialog
+import org.fossify.clock.extensions.alarmController
import org.fossify.clock.extensions.cancelAlarmClock
import org.fossify.clock.extensions.config
import org.fossify.clock.extensions.createNewAlarm
import org.fossify.clock.extensions.dbHelper
import org.fossify.clock.extensions.firstDayOrder
import org.fossify.clock.extensions.handleFullScreenNotificationsPermission
-import org.fossify.clock.extensions.scheduleNextAlarm
import org.fossify.clock.extensions.updateWidgets
import org.fossify.clock.helpers.DEFAULT_ALARM_MINUTES
import org.fossify.clock.helpers.SORT_BY_ALARM_TIME
@@ -195,7 +195,7 @@ class AlarmFragment : Fragment(), ToggleAlarmInterface {
private fun checkAlarmState(alarm: Alarm) {
if (alarm.isEnabled) {
- context?.scheduleNextAlarm(alarm, true)
+ context?.alarmController?.scheduleNextOccurrence(alarm, true)
} else {
context?.cancelAlarmClock(alarm)
}
diff --git a/app/src/main/kotlin/org/fossify/clock/helpers/AlarmController.kt b/app/src/main/kotlin/org/fossify/clock/helpers/AlarmController.kt
new file mode 100644
index 00000000..17d6ddc8
--- /dev/null
+++ b/app/src/main/kotlin/org/fossify/clock/helpers/AlarmController.kt
@@ -0,0 +1,220 @@
+package org.fossify.clock.helpers
+
+import android.app.Application
+import android.content.Context
+import org.fossify.clock.extensions.cancelAlarmClock
+import org.fossify.clock.extensions.dbHelper
+import org.fossify.clock.extensions.setupAlarmClock
+import org.fossify.clock.extensions.showRemainingTimeMessage
+import org.fossify.clock.extensions.startAlarmService
+import org.fossify.clock.extensions.stopAlarmService
+import org.fossify.clock.extensions.updateWidgets
+import org.fossify.clock.models.Alarm
+import org.fossify.clock.models.AlarmEvent
+import org.fossify.commons.extensions.removeBit
+import org.fossify.commons.helpers.ensureBackgroundThread
+import org.greenrobot.eventbus.EventBus
+import java.util.Calendar
+
+/**
+ * Centralized class for handling alarm operations including dismissal, cancellation, scheduling,
+ * and state management.
+ */
+class AlarmController(
+ private val context: Application,
+ private val db: DBHelper,
+ private val bus: EventBus,
+) {
+ /**
+ * Reschedules all enabled alarms with the exception of one-time alarms that were scheduled
+ * for today (to avoid rescheduling skipped upcoming alarms, yeah).
+ */
+ fun rescheduleEnabledAlarms() {
+ db.getEnabledAlarms().forEach {
+ if (!it.isToday() || it.timeInMinutes > getCurrentDayMinutes()) {
+ scheduleNextOccurrence(it, false)
+ }
+ }
+ }
+
+ /**
+ * Schedules the next occurrence of a repeating alarm based on its repetition rules.
+ *
+ * @param alarm The alarm to schedule.
+ * @param showToasts If true, a remaining time toast will be shown for the alarm.
+ */
+ fun scheduleNextOccurrence(alarm: Alarm, showToasts: Boolean = false) {
+ ensureBackgroundThread {
+ scheduleNextAlarm(alarm, showToasts)
+ notifyObservers()
+ }
+ }
+
+ /**
+ * Skips (cancels) the *next scheduled occurrence* of an alarm before it rings.
+ * If the alarm is repeating, it cancels the upcoming alert and schedules the *following*
+ * occurrence based on repetition rules. If the alarm is a one-time alarm, it cancels and
+ * disables or deletes it.
+ *
+ * @param alarmId The ID of the upcoming alarm trigger to skip/cancel.
+ */
+ fun skipNextOccurrence(alarmId: Int) {
+ ensureBackgroundThread {
+ val alarm = db.getAlarmWithId(alarmId) ?: return@ensureBackgroundThread
+ context.cancelAlarmClock(alarm)
+
+ // Schedule the *next* occurrence based on the original repeating schedule.
+ if (alarm.isRecurring()) {
+ // TODO: This is a bit of a hack. Skipped alarms should be tracked properly.
+ val todayBitmask = getTodayBit()
+ if (alarm.days and todayBitmask != 0) {
+ // If there are other days set, schedule based on those remaining days.
+ val remainingDays = alarm.days.removeBit(todayBitmask)
+ if (remainingDays > 0) {
+ val alarmForScheduling = alarm.copy(days = remainingDays)
+ scheduleNextAlarm(alarmForScheduling)
+ } else {
+ // Today was the ONLY weekday set. Skipping it means no weekdays are left.
+ // TODO: But does this mean the alarm won't be scheduled for next week?
+ }
+ } else {
+ // Not scheduled for today anyway, just reschedule the alarm.
+ scheduleNextAlarm(alarm)
+ }
+ } else {
+ disableOrDeleteOneTimeAlarm(alarm)
+ }
+
+ notifyObservers()
+ }
+ }
+
+ /**
+ * Handles the triggering of an alarm, scheduling the next occurrence and starting the service
+ * for sounding the alarm.
+ *
+ * @param alarmId The ID of the alarm that was triggered.
+ */
+ fun onAlarmTriggered(alarmId: Int) {
+ ensureBackgroundThread {
+ // Reschedule the next occurrence right away
+ val alarm = db.getAlarmWithId(alarmId) ?: return@ensureBackgroundThread
+ if (alarm.isRecurring()) {
+ scheduleNextOccurrence(alarm)
+ }
+ }
+
+ context.startAlarmService(alarmId)
+ }
+
+ /**
+ * Dismisses an alarm that is currently ringing or has just finished ringing.
+ *
+ * - Stops the alarm sound/vibration service ([stopAlarmService]).
+ * - If the alarm is *not* repeating and the `disable` parameter is true, the alarm is
+ * disabled or deleted via [disableOrDeleteOneTimeAlarm].
+ *
+ * @param alarmId The ID of the alarm to dismiss.
+ * @param disable If true and the alarm is a one-time alarm, it will be disabled or deleted.
+ * This parameter has no effect on repeating alarms.
+ */
+ fun stopAlarm(alarmId: Int, disable: Boolean = true) {
+ context.stopAlarmService()
+ bus.post(AlarmEvent.Stopped(alarmId))
+
+ ensureBackgroundThread {
+ val alarm = db.getAlarmWithId(alarmId)
+
+ // We don't reschedule alarms here.
+ if (alarm != null && !alarm.isRecurring() && disable) {
+ context.cancelAlarmClock(alarm)
+ disableOrDeleteOneTimeAlarm(alarm)
+ }
+
+ notifyObservers()
+ }
+ }
+
+ /**
+ * Snoozes an alarm that is currently ringing.
+ *
+ * - Stops the alarm sound/vibration service ([stopAlarmService]).
+ * - Schedules the alarm to ring again after [snoozeMinutes] using [setupAlarmClock]
+ * with a calculated future trigger time.
+ *
+ * TODO: This works but it is very rudimentary. Snoozed alarms should be tracked properly.
+ *
+ * @param alarmId The ID of the alarm to snooze.
+ * @param snoozeMinutes The number of minutes from now until the alarm should ring again.
+ */
+ fun snoozeAlarm(alarmId: Int, snoozeMinutes: Int) {
+ context.stopAlarmService()
+ bus.post(AlarmEvent.Stopped(alarmId))
+
+ ensureBackgroundThread {
+ val alarm = db.getAlarmWithId(alarmId)
+ if (alarm != null) {
+ context.setupAlarmClock(
+ alarm = alarm,
+ triggerTimeMillis = Calendar.getInstance()
+ .apply { add(Calendar.MINUTE, snoozeMinutes) }
+ .timeInMillis
+ )
+ }
+
+ notifyObservers()
+ }
+ }
+
+ /**
+ * Handles disabling or deleting a *one-time* (non-repeating) alarm based on `oneShot` property.
+ * This is typically called after a one-time alarm has rung and been dismissed or stopped,
+ * or when it's explicitly skipped.
+ *
+ * @param alarm The one-time alarm to disable or delete. Must not be repeating.
+ */
+ private fun disableOrDeleteOneTimeAlarm(alarm: Alarm) {
+ require(!alarm.isRecurring()) {
+ "Alarm ${alarm.id} is repeating but was passed to disableOrDeleteOneTimeAlarm()"
+ }
+
+ if (alarm.oneShot) {
+ alarm.isEnabled = false
+ db.deleteAlarms(arrayListOf(alarm))
+ } else {
+ db.updateAlarmEnabledState(alarm.id, false)
+ }
+ }
+
+ private fun scheduleNextAlarm(alarm: Alarm, showToast: Boolean = false) {
+ val triggerTimeMillis = getTimeOfNextAlarm(alarm)?.timeInMillis ?: return
+ context.setupAlarmClock(alarm = alarm, triggerTimeMillis = triggerTimeMillis)
+
+ if (showToast) {
+ val now = Calendar.getInstance()
+ val triggerInMillis = triggerTimeMillis - now.timeInMillis
+ context.showRemainingTimeMessage(triggerInMillis)
+ }
+ }
+
+ private fun notifyObservers() {
+ context.updateWidgets()
+ bus.post(AlarmEvent.Refresh)
+ }
+
+ companion object {
+ @Volatile
+ private var instance: AlarmController? = null
+
+ fun getInstance(context: Context): AlarmController {
+ val appContext = context.applicationContext as Application
+ return instance ?: synchronized(this) {
+ instance ?: AlarmController(
+ context = appContext,
+ db = appContext.dbHelper,
+ bus = EventBus.getDefault()
+ ).also { instance = it }
+ }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/org/fossify/clock/helpers/Constants.kt b/app/src/main/kotlin/org/fossify/clock/helpers/Constants.kt
index bfce9b5e..6ddb6104 100644
--- a/app/src/main/kotlin/org/fossify/clock/helpers/Constants.kt
+++ b/app/src/main/kotlin/org/fossify/clock/helpers/Constants.kt
@@ -14,7 +14,6 @@ import org.fossify.commons.helpers.isPiePlus
import java.util.Calendar
import java.util.Date
import java.util.TimeZone
-import kotlin.math.pow
// shared preferences
const val SELECTED_TIME_ZONES = "selected_time_zones"
@@ -54,14 +53,13 @@ const val EARLY_ALARM_DISMISSAL_CHANNEL_ID = "Early Alarm Dismissal"
const val OPEN_STOPWATCH_TAB_INTENT_ID = 9993
const val PICK_AUDIO_FILE_INTENT_ID = 9994
-const val REMINDER_ACTIVITY_INTENT_ID = 9995
const val OPEN_ALARMS_TAB_INTENT_ID = 9996
const val OPEN_APP_INTENT_ID = 9997
const val ALARM_NOTIF_ID = 9998
const val TIMER_RUNNING_NOTIF_ID = 10000
const val STOPWATCH_RUNNING_NOTIF_ID = 10001
const val EARLY_ALARM_DISMISSAL_INTENT_ID = 10002
-const val EARLY_ALARM_NOTIF_ID = 10003
+const val UPCOMING_ALARM_NOTIFICATION_ID = 10003
const val OPEN_TAB = "open_tab"
const val TAB_CLOCK = 1
@@ -157,13 +155,13 @@ fun getTomorrowBit(): Int {
val calendar = Calendar.getInstance()
calendar.add(Calendar.DAY_OF_WEEK, 1)
val dayOfWeek = getDayNumber(calendar.get(Calendar.DAY_OF_WEEK))
- return 2.0.pow(dayOfWeek).toInt()
+ return 1 shl dayOfWeek
}
fun getTodayBit(): Int {
val calendar = Calendar.getInstance()
val dayOfWeek = getDayNumber(calendar.get(Calendar.DAY_OF_WEEK))
- return 2.0.pow(dayOfWeek).toInt()
+ return 1 shl dayOfWeek
}
fun getBitForCalendarDay(day: Int): Int {
@@ -274,7 +272,6 @@ fun getTimeOfNextAlarm(alarm: Alarm): Calendar? {
fun getTimeOfNextAlarm(alarmTimeInMinutes: Int, days: Int): Calendar? {
val nextAlarmTime = Calendar.getInstance().apply {
- firstDayOfWeek = Calendar.MONDAY // why is this here? seems unnecessary
set(Calendar.HOUR_OF_DAY, alarmTimeInMinutes / 60)
set(Calendar.MINUTE, alarmTimeInMinutes % 60)
set(Calendar.SECOND, 0)
diff --git a/app/src/main/kotlin/org/fossify/clock/models/Alarm.kt b/app/src/main/kotlin/org/fossify/clock/models/Alarm.kt
index 9d068411..263ca1fa 100644
--- a/app/src/main/kotlin/org/fossify/clock/models/Alarm.kt
+++ b/app/src/main/kotlin/org/fossify/clock/models/Alarm.kt
@@ -1,6 +1,8 @@
package org.fossify.clock.models
import androidx.annotation.Keep
+import org.fossify.clock.helpers.TODAY_BIT
+import org.fossify.clock.helpers.TOMORROW_BIT
@Keep
@kotlinx.serialization.Serializable
@@ -14,7 +16,13 @@ data class Alarm(
var soundUri: String,
var label: String,
var oneShot: Boolean = false,
-)
+) {
+ fun isRecurring() = days > 0
+
+ fun isToday() = days == TODAY_BIT
+
+ fun isTomorrow() = days == TOMORROW_BIT
+}
@Keep
data class ObfuscatedAlarm(
diff --git a/app/src/main/kotlin/org/fossify/clock/models/AlarmEvent.kt b/app/src/main/kotlin/org/fossify/clock/models/AlarmEvent.kt
index 6d98634a..3d6c3893 100644
--- a/app/src/main/kotlin/org/fossify/clock/models/AlarmEvent.kt
+++ b/app/src/main/kotlin/org/fossify/clock/models/AlarmEvent.kt
@@ -1,5 +1,6 @@
package org.fossify.clock.models
sealed interface AlarmEvent {
- object Refresh : AlarmEvent
+ data object Refresh : AlarmEvent
+ data class Stopped(val alarmId: Int) : AlarmEvent
}
diff --git a/app/src/main/kotlin/org/fossify/clock/receivers/AlarmReceiver.kt b/app/src/main/kotlin/org/fossify/clock/receivers/AlarmReceiver.kt
index a0799fb9..f06bfd26 100644
--- a/app/src/main/kotlin/org/fossify/clock/receivers/AlarmReceiver.kt
+++ b/app/src/main/kotlin/org/fossify/clock/receivers/AlarmReceiver.kt
@@ -1,101 +1,31 @@
package org.fossify.clock.receivers
-import android.app.NotificationChannel
-import android.app.NotificationManager
-import android.app.PendingIntent
-import android.app.PendingIntent.FLAG_IMMUTABLE
-import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
-import android.os.Build
-import android.os.Handler
-import android.os.Looper
-import androidx.annotation.RequiresApi
-import androidx.core.app.NotificationCompat
-import org.fossify.clock.R
-import org.fossify.clock.activities.ReminderActivity
-import org.fossify.clock.extensions.config
-import org.fossify.clock.extensions.dbHelper
-import org.fossify.clock.extensions.disableExpiredAlarm
+import org.fossify.clock.extensions.alarmController
+import org.fossify.clock.extensions.goAsync
import org.fossify.clock.extensions.hideNotification
-import org.fossify.clock.extensions.isScreenOn
-import org.fossify.clock.extensions.showAlarmNotification
import org.fossify.clock.helpers.ALARM_ID
-import org.fossify.clock.helpers.ALARM_NOTIFICATION_CHANNEL_ID
-import org.fossify.clock.helpers.ALARM_NOTIF_ID
-import org.fossify.clock.helpers.EARLY_ALARM_NOTIF_ID
-import org.fossify.commons.extensions.notificationManager
-import org.fossify.commons.extensions.showErrorToast
-import org.fossify.commons.helpers.isOreoPlus
+import org.fossify.clock.helpers.UPCOMING_ALARM_NOTIFICATION_ID
+/**
+ * Receiver responsible for sounding alarms. It is also responsible for hiding the
+ * upcoming alarm notification and scheduling the next occurrence.
+ */
class AlarmReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val id = intent.getIntExtra(ALARM_ID, -1)
- val alarm = context.dbHelper.getAlarmWithId(id) ?: return
+ if (id == -1) return
- // Hide early dismissal notification if not already dismissed
- context.hideNotification(EARLY_ALARM_NOTIF_ID)
-
- if (context.isScreenOn()) {
- context.showAlarmNotification(alarm)
- Handler(Looper.getMainLooper()).postDelayed({
- context.hideNotification(id)
- context.disableExpiredAlarm(alarm)
- }, context.config.alarmMaxReminderSecs * 1000L)
- } else {
- if (isOreoPlus()) {
- val notificationManager = context.notificationManager
- if (notificationManager.getNotificationChannel(ALARM_NOTIFICATION_CHANNEL_ID) == null) {
- // cleans up previous notification channel that had sound properties
- oldNotificationChannelCleanup(notificationManager)
-
- NotificationChannel(
- ALARM_NOTIFICATION_CHANNEL_ID,
- "Alarm",
- NotificationManager.IMPORTANCE_HIGH
- ).apply {
- setBypassDnd(true)
- setSound(null, null)
- notificationManager.createNotificationChannel(this)
- }
- }
-
- val reminderIntent = Intent(context, ReminderActivity::class.java).apply {
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- putExtra(ALARM_ID, id)
- }
-
- val pendingIntent = PendingIntent.getActivity(
- context, 0, reminderIntent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE
- )
-
- val builder = NotificationCompat.Builder(context, ALARM_NOTIFICATION_CHANNEL_ID)
- .setSmallIcon(R.drawable.ic_alarm_vector)
- .setContentTitle(context.getString(org.fossify.commons.R.string.alarm))
- .setAutoCancel(true)
- .setPriority(NotificationCompat.PRIORITY_HIGH)
- .setCategory(NotificationCompat.CATEGORY_ALARM)
- .setFullScreenIntent(pendingIntent, true)
-
- try {
- notificationManager.notify(ALARM_NOTIF_ID, builder.build())
- } catch (e: Exception) {
- context.showErrorToast(e)
- }
- } else {
- Intent(context, ReminderActivity::class.java).apply {
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- putExtra(ALARM_ID, id)
- context.startActivity(this)
- }
- }
+ cancelUpcomingAlarmNotification(context)
+ goAsync {
+ context.alarmController.onAlarmTriggered(id)
}
}
- @RequiresApi(Build.VERSION_CODES.O)
- private fun oldNotificationChannelCleanup(notificationManager: NotificationManager) {
- notificationManager.deleteNotificationChannel("Alarm")
+ private fun cancelUpcomingAlarmNotification(context: Context) {
+ context.hideNotification(UPCOMING_ALARM_NOTIFICATION_ID)
}
}
diff --git a/app/src/main/kotlin/org/fossify/clock/receivers/BootCompletedReceiver.kt b/app/src/main/kotlin/org/fossify/clock/receivers/BootCompletedReceiver.kt
index e4b40e0c..98c34114 100644
--- a/app/src/main/kotlin/org/fossify/clock/receivers/BootCompletedReceiver.kt
+++ b/app/src/main/kotlin/org/fossify/clock/receivers/BootCompletedReceiver.kt
@@ -3,11 +3,14 @@ package org.fossify.clock.receivers
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
-import org.fossify.clock.extensions.rescheduleEnabledAlarms
+import org.fossify.clock.extensions.alarmController
+import org.fossify.clock.extensions.goAsync
class BootCompletedReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
- context.rescheduleEnabledAlarms()
+ goAsync {
+ context.alarmController.rescheduleEnabledAlarms()
+ }
}
}
diff --git a/app/src/main/kotlin/org/fossify/clock/receivers/DismissAlarmReceiver.kt b/app/src/main/kotlin/org/fossify/clock/receivers/DismissAlarmReceiver.kt
deleted file mode 100644
index 80e9f0a1..00000000
--- a/app/src/main/kotlin/org/fossify/clock/receivers/DismissAlarmReceiver.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-package org.fossify.clock.receivers
-
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import org.fossify.clock.extensions.cancelAlarmClock
-import org.fossify.clock.extensions.disableExpiredAlarm
-import org.fossify.clock.extensions.dbHelper
-import org.fossify.clock.extensions.hideNotification
-import org.fossify.clock.extensions.scheduleNextAlarm
-import org.fossify.clock.helpers.ALARM_ID
-import org.fossify.clock.helpers.NOTIFICATION_ID
-import org.fossify.clock.models.Alarm
-import org.fossify.commons.extensions.removeBit
-import org.fossify.commons.helpers.ensureBackgroundThread
-import java.util.Calendar
-import kotlin.math.pow
-
-class DismissAlarmReceiver : BroadcastReceiver() {
- override fun onReceive(context: Context, intent: Intent) {
- val alarmId = intent.getIntExtra(ALARM_ID, -1)
- val notificationId = intent.getIntExtra(NOTIFICATION_ID, -1)
- if (alarmId == -1) {
- return
- }
-
- context.hideNotification(notificationId)
-
- ensureBackgroundThread {
- context.dbHelper.getAlarmWithId(alarmId)?.let { alarm ->
- context.cancelAlarmClock(alarm)
- scheduleNextAlarm(alarm, context)
- context.disableExpiredAlarm(alarm)
- }
- }
- }
-
- private fun scheduleNextAlarm(alarm: Alarm, context: Context) {
- val oldBitmask = alarm.days
- alarm.days = removeTodayFromBitmask(oldBitmask)
- context.scheduleNextAlarm(alarm, false)
- alarm.days = oldBitmask
- }
-
- private fun removeTodayFromBitmask(bitmask: Int): Int {
- val calendar = Calendar.getInstance()
- calendar.firstDayOfWeek = Calendar.MONDAY
- val dayOfWeek = (calendar.get(Calendar.DAY_OF_WEEK) + 5) % 7
- val todayBitmask = 2.0.pow(dayOfWeek).toInt()
- return bitmask.removeBit(todayBitmask)
- }
-}
diff --git a/app/src/main/kotlin/org/fossify/clock/receivers/HideAlarmReceiver.kt b/app/src/main/kotlin/org/fossify/clock/receivers/HideAlarmReceiver.kt
deleted file mode 100644
index 0edd6dac..00000000
--- a/app/src/main/kotlin/org/fossify/clock/receivers/HideAlarmReceiver.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package org.fossify.clock.receivers
-
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import org.fossify.clock.extensions.disableExpiredAlarm
-import org.fossify.clock.extensions.dbHelper
-import org.fossify.clock.extensions.deleteNotificationChannel
-import org.fossify.clock.extensions.hideNotification
-import org.fossify.clock.helpers.ALARM_ID
-import org.fossify.clock.helpers.ALARM_NOTIFICATION_CHANNEL_ID
-import org.fossify.commons.helpers.ensureBackgroundThread
-
-class HideAlarmReceiver : BroadcastReceiver() {
- override fun onReceive(context: Context, intent: Intent) {
- val id = intent.getIntExtra(ALARM_ID, -1)
- val channelId = intent.getStringExtra(ALARM_NOTIFICATION_CHANNEL_ID)
- channelId?.let { context.deleteNotificationChannel(channelId) }
- context.hideNotification(id)
-
- ensureBackgroundThread {
- val alarm = context.dbHelper.getAlarmWithId(id)
- if (alarm != null) {
- context.disableExpiredAlarm(alarm)
- }
- }
- }
-}
diff --git a/app/src/main/kotlin/org/fossify/clock/receivers/SkipUpcomingAlarmReceiver.kt b/app/src/main/kotlin/org/fossify/clock/receivers/SkipUpcomingAlarmReceiver.kt
new file mode 100644
index 00000000..15eec78a
--- /dev/null
+++ b/app/src/main/kotlin/org/fossify/clock/receivers/SkipUpcomingAlarmReceiver.kt
@@ -0,0 +1,29 @@
+package org.fossify.clock.receivers
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import org.fossify.clock.extensions.alarmController
+import org.fossify.clock.extensions.goAsync
+import org.fossify.clock.extensions.hideNotification
+import org.fossify.clock.helpers.ALARM_ID
+import org.fossify.clock.helpers.NOTIFICATION_ID
+
+/**
+ * Receiver responsible for dismissing *UPCOMING* alarms.
+ */
+class SkipUpcomingAlarmReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ val alarmId = intent.getIntExtra(ALARM_ID, -1)
+ if (alarmId != -1) {
+ goAsync {
+ context.alarmController.skipNextOccurrence(alarmId)
+ }
+ }
+
+ val notificationId = intent.getIntExtra(NOTIFICATION_ID, -1)
+ if (notificationId != -1) {
+ context.hideNotification(notificationId)
+ }
+ }
+}
diff --git a/app/src/main/kotlin/org/fossify/clock/receivers/StopAlarmReceiver.kt b/app/src/main/kotlin/org/fossify/clock/receivers/StopAlarmReceiver.kt
new file mode 100644
index 00000000..2ba3c95b
--- /dev/null
+++ b/app/src/main/kotlin/org/fossify/clock/receivers/StopAlarmReceiver.kt
@@ -0,0 +1,22 @@
+package org.fossify.clock.receivers
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import org.fossify.clock.extensions.alarmController
+import org.fossify.clock.extensions.goAsync
+import org.fossify.clock.helpers.ALARM_ID
+
+/**
+ * Receiver responsible for stopping running alarms.
+ */
+class StopAlarmReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ val id = intent.getIntExtra(ALARM_ID, -1)
+ if (id != -1) {
+ goAsync {
+ context.alarmController.stopAlarm(id)
+ }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/org/fossify/clock/receivers/EarlyAlarmDismissalReceiver.kt b/app/src/main/kotlin/org/fossify/clock/receivers/UpcomingAlarmReceiver.kt
similarity index 62%
rename from app/src/main/kotlin/org/fossify/clock/receivers/EarlyAlarmDismissalReceiver.kt
rename to app/src/main/kotlin/org/fossify/clock/receivers/UpcomingAlarmReceiver.kt
index 6db52dab..b73c2c6e 100644
--- a/app/src/main/kotlin/org/fossify/clock/receivers/EarlyAlarmDismissalReceiver.kt
+++ b/app/src/main/kotlin/org/fossify/clock/receivers/UpcomingAlarmReceiver.kt
@@ -9,14 +9,20 @@ import android.content.Intent
import androidx.core.app.NotificationCompat
import org.fossify.clock.R
import org.fossify.clock.extensions.getClosestEnabledAlarmString
-import org.fossify.clock.extensions.getDismissAlarmPendingIntent
import org.fossify.clock.extensions.getOpenAlarmTabIntent
+import org.fossify.clock.extensions.getSkipUpcomingAlarmPendingIntent
+import org.fossify.clock.extensions.goAsync
import org.fossify.clock.helpers.ALARM_ID
import org.fossify.clock.helpers.EARLY_ALARM_DISMISSAL_CHANNEL_ID
-import org.fossify.clock.helpers.EARLY_ALARM_NOTIF_ID
+import org.fossify.clock.helpers.UPCOMING_ALARM_NOTIFICATION_ID
+import org.fossify.commons.extensions.notificationManager
import org.fossify.commons.helpers.isOreoPlus
-class EarlyAlarmDismissalReceiver : BroadcastReceiver() {
+/**
+ * Receiver responsible for showing a notification that allows users to skip an upcoming alarm.
+ * This notification appears 10 minutes before (hardcoded) the alarm is scheduled to trigger.
+ */
+class UpcomingAlarmReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val alarmId = intent.getIntExtra(ALARM_ID, -1)
@@ -24,12 +30,14 @@ class EarlyAlarmDismissalReceiver : BroadcastReceiver() {
return
}
- triggerEarlyDismissalNotification(context, alarmId)
+ goAsync {
+ showUpcomingAlarmNotification(context, alarmId)
+ }
}
- private fun triggerEarlyDismissalNotification(context: Context, alarmId: Int) {
+ private fun showUpcomingAlarmNotification(context: Context, alarmId: Int) {
context.getClosestEnabledAlarmString { alarmString ->
- val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ val notificationManager = context.notificationManager
if (isOreoPlus()) {
NotificationChannel(
EARLY_ALARM_DISMISSAL_CHANNEL_ID,
@@ -41,22 +49,29 @@ class EarlyAlarmDismissalReceiver : BroadcastReceiver() {
notificationManager.createNotificationChannel(this)
}
}
- val dismissIntent = context.getDismissAlarmPendingIntent(alarmId, EARLY_ALARM_NOTIF_ID)
+
val contentIntent = context.getOpenAlarmTabIntent()
+ val dismissIntent = context.getSkipUpcomingAlarmPendingIntent(
+ alarmId = alarmId, notificationId = UPCOMING_ALARM_NOTIFICATION_ID
+ )
+
val notification = NotificationCompat.Builder(context)
.setContentTitle(context.getString(R.string.upcoming_alarm))
.setContentText(alarmString)
.setSmallIcon(R.drawable.ic_alarm_vector)
.setPriority(Notification.PRIORITY_LOW)
- .addAction(0, context.getString(org.fossify.commons.R.string.dismiss), dismissIntent)
+ .addAction(
+ 0,
+ context.getString(org.fossify.commons.R.string.dismiss),
+ dismissIntent
+ )
.setContentIntent(contentIntent)
.setSound(null)
.setAutoCancel(true)
.setChannelId(EARLY_ALARM_DISMISSAL_CHANNEL_ID)
.build()
- notificationManager.notify(EARLY_ALARM_NOTIF_ID, notification)
+ notificationManager.notify(UPCOMING_ALARM_NOTIFICATION_ID, notification)
}
}
-
}
diff --git a/app/src/main/kotlin/org/fossify/clock/services/AlarmService.kt b/app/src/main/kotlin/org/fossify/clock/services/AlarmService.kt
new file mode 100644
index 00000000..8f283fa3
--- /dev/null
+++ b/app/src/main/kotlin/org/fossify/clock/services/AlarmService.kt
@@ -0,0 +1,229 @@
+package org.fossify.clock.services
+
+import android.annotation.SuppressLint
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.PendingIntent.FLAG_IMMUTABLE
+import android.app.PendingIntent.FLAG_UPDATE_CURRENT
+import android.app.Service
+import android.content.Intent
+import android.media.AudioAttributes
+import android.media.AudioManager
+import android.media.AudioManager.STREAM_ALARM
+import android.media.MediaPlayer
+import android.os.Handler
+import android.os.Looper
+import android.os.VibrationEffect
+import android.os.Vibrator
+import androidx.core.app.NotificationCompat
+import androidx.core.net.toUri
+import org.fossify.clock.R
+import org.fossify.clock.activities.AlarmActivity
+import org.fossify.clock.extensions.alarmController
+import org.fossify.clock.extensions.config
+import org.fossify.clock.extensions.dbHelper
+import org.fossify.clock.extensions.getFormattedTime
+import org.fossify.clock.extensions.getSnoozePendingIntent
+import org.fossify.clock.extensions.getStopAlarmPendingIntent
+import org.fossify.clock.helpers.ALARM_ID
+import org.fossify.clock.helpers.ALARM_NOTIFICATION_CHANNEL_ID
+import org.fossify.clock.helpers.ALARM_NOTIF_ID
+import org.fossify.clock.models.Alarm
+import org.fossify.commons.extensions.notificationManager
+import org.fossify.commons.helpers.SILENT
+import org.fossify.commons.helpers.isOreoPlus
+import kotlin.time.Duration.Companion.seconds
+
+/**
+ * Service responsible for sounding the alarms and vibrations.
+ * It also shows a notification with actions to dismiss or snooze an alarm.
+ * Totally based on the previous implementation in the [AlarmActivity].
+ */
+class AlarmService : Service() {
+
+ companion object {
+ private const val DEFAULT_ALARM_VOLUME = 7
+ private const val INCREASE_VOLUME_DELAY = 300L
+ private const val MIN_ALARM_VOLUME_FOR_INCREASING_ALARMS = 1
+ }
+
+ private var alarm: Alarm? = null
+ private var audioManager: AudioManager? = null
+ private var initialAlarmVolume = DEFAULT_ALARM_VOLUME
+ private var mediaPlayer: MediaPlayer? = null
+ private var vibrator: Vibrator? = null
+
+ private val autoDismissHandler = Handler(Looper.getMainLooper())
+ private val increaseVolumeHandler = Handler(Looper.getMainLooper())
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ val alarmId = intent?.getIntExtra(ALARM_ID, -1) ?: -1
+ alarm = if (alarmId != -1) {
+ applicationContext.dbHelper.getAlarmWithId(alarmId)
+ } else {
+ null
+ }
+
+ if (alarm == null) {
+ stopSelf()
+ return START_NOT_STICKY
+ }
+
+ val notification = buildNotification(alarm!!)
+ startForeground(ALARM_NOTIF_ID, notification)
+ startAlarmEffects(alarm!!)
+ startAutoDismiss(config.alarmMaxReminderSecs)
+ return START_STICKY
+ }
+
+ private fun buildNotification(alarm: Alarm): Notification {
+ val channelId = ALARM_NOTIFICATION_CHANNEL_ID
+ if (isOreoPlus()) {
+ val channel = NotificationChannel(
+ channelId,
+ getString(org.fossify.commons.R.string.alarm),
+ NotificationManager.IMPORTANCE_HIGH
+ ).apply {
+ setBypassDnd(true)
+ setSound(null, null)
+ }
+
+ notificationManager.createNotificationChannel(channel)
+ }
+
+ val contentTitle = alarm.label.ifEmpty {
+ getString(org.fossify.commons.R.string.alarm)
+ }
+
+ val contentText = getFormattedTime(
+ passedSeconds = alarm.timeInMinutes * 60,
+ showSeconds = false,
+ makeAmPmSmaller = false
+ )
+
+ val reminderIntent = Intent(this, AlarmActivity::class.java).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ putExtra(ALARM_ID, alarm.id)
+ }
+
+ val pendingIntent = PendingIntent.getActivity(
+ this, 0, reminderIntent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE
+ )
+
+ val dismissIntent = applicationContext.getStopAlarmPendingIntent(alarm)
+ val snoozeIntent = applicationContext.getSnoozePendingIntent(alarm)
+
+ return NotificationCompat.Builder(this, channelId)
+ .setContentTitle(contentTitle)
+ .setContentText(contentText)
+ .setSmallIcon(R.drawable.ic_alarm_vector)
+ .setContentIntent(pendingIntent)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setCategory(NotificationCompat.CATEGORY_ALARM)
+ .setDefaults(NotificationCompat.DEFAULT_LIGHTS)
+ .addAction(
+ org.fossify.commons.R.drawable.ic_snooze_vector,
+ getString(org.fossify.commons.R.string.snooze),
+ snoozeIntent
+ )
+ .addAction(
+ org.fossify.commons.R.drawable.ic_cross_vector,
+ getString(org.fossify.commons.R.string.dismiss),
+ dismissIntent
+ )
+ .setDeleteIntent(dismissIntent)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .setFullScreenIntent(pendingIntent, true)
+ .build()
+ }
+
+ private fun startAlarmEffects(alarm: Alarm) {
+ if (alarm.soundUri != SILENT) {
+ try {
+ val audioAttributes = AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_ALARM)
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
+ .build()
+
+ mediaPlayer = MediaPlayer().apply {
+ setAudioAttributes(audioAttributes)
+ setDataSource(this@AlarmService, alarm.soundUri.toUri())
+ isLooping = true
+ prepare()
+ start()
+ }
+
+ if (config.increaseVolumeGradually) {
+ initialAlarmVolume = audioManager?.getStreamVolume(STREAM_ALARM)
+ ?: DEFAULT_ALARM_VOLUME
+
+ scheduleVolumeIncrease(
+ lastVolume = MIN_ALARM_VOLUME_FOR_INCREASING_ALARMS.toFloat(),
+ maxVolume = initialAlarmVolume.toFloat(),
+ delay = 0
+ )
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ if (alarm.vibrate && isOreoPlus()) {
+ vibrator = getSystemService(VIBRATOR_SERVICE) as Vibrator
+ val timing = 500L
+ val repeatIndex = 0
+ vibrator?.vibrate(
+ VibrationEffect.createWaveform(
+ longArrayOf(timing, timing), repeatIndex
+ )
+ )
+ }
+ }
+
+ private fun scheduleVolumeIncrease(lastVolume: Float, maxVolume: Float, delay: Long) {
+ increaseVolumeHandler.postDelayed({
+ val volumeFlags = 0
+ val newVolume = (lastVolume + 0.1f).coerceAtMost(maxVolume)
+ audioManager?.setStreamVolume(STREAM_ALARM, newVolume.toInt(), volumeFlags)
+ if (newVolume < maxVolume) {
+ scheduleVolumeIncrease(newVolume, maxVolume, INCREASE_VOLUME_DELAY)
+ }
+ }, delay)
+ }
+
+ private fun resetVolumeToInitialValue() {
+ if (config.increaseVolumeGradually) {
+ val volumeFlags = 0
+ audioManager?.setStreamVolume(STREAM_ALARM, initialAlarmVolume, volumeFlags)
+ }
+ }
+
+ private fun startAutoDismiss(durationSecs: Int) {
+ val alarmId = alarm?.id ?: return
+ autoDismissHandler.postDelayed({
+ alarmController.stopAlarm(alarmId)
+ }, durationSecs.seconds.inWholeMilliseconds)
+ }
+
+ @SuppressLint("InlinedApi")
+ override fun onDestroy() {
+ super.onDestroy()
+ stopForeground(STOP_FOREGROUND_REMOVE)
+
+ mediaPlayer?.stop()
+ mediaPlayer?.release()
+ mediaPlayer = null
+ vibrator?.cancel()
+ vibrator = null
+
+ // Clear any scheduled volume changes or auto-dismiss messages
+ increaseVolumeHandler.removeCallbacksAndMessages(null)
+ autoDismissHandler.removeCallbacksAndMessages(null)
+ resetVolumeToInitialValue()
+ }
+
+ override fun onBind(intent: Intent?) = null
+}
diff --git a/app/src/main/kotlin/org/fossify/clock/services/SnoozeService.kt b/app/src/main/kotlin/org/fossify/clock/services/SnoozeService.kt
index 68f3c473..367b63f2 100644
--- a/app/src/main/kotlin/org/fossify/clock/services/SnoozeService.kt
+++ b/app/src/main/kotlin/org/fossify/clock/services/SnoozeService.kt
@@ -2,23 +2,13 @@ package org.fossify.clock.services
import android.app.IntentService
import android.content.Intent
+import org.fossify.clock.extensions.alarmController
import org.fossify.clock.extensions.config
-import org.fossify.clock.extensions.dbHelper
-import org.fossify.clock.extensions.hideNotification
-import org.fossify.clock.extensions.setupAlarmClock
import org.fossify.clock.helpers.ALARM_ID
-import java.util.Calendar
class SnoozeService : IntentService("Snooze") {
override fun onHandleIntent(intent: Intent?) {
val id = intent!!.getIntExtra(ALARM_ID, -1)
- val alarm = dbHelper.getAlarmWithId(id) ?: return
- hideNotification(id)
- setupAlarmClock(
- alarm = alarm,
- triggerTimeMillis = Calendar.getInstance()
- .apply { add(Calendar.MINUTE, config.snoozeTime) }
- .timeInMillis
- )
+ alarmController.snoozeAlarm(id, config.snoozeTime)
}
}
diff --git a/app/src/main/kotlin/org/fossify/clock/services/StopwatchService.kt b/app/src/main/kotlin/org/fossify/clock/services/StopwatchService.kt
index 3267614e..5c422703 100644
--- a/app/src/main/kotlin/org/fossify/clock/services/StopwatchService.kt
+++ b/app/src/main/kotlin/org/fossify/clock/services/StopwatchService.kt
@@ -18,6 +18,7 @@ import org.fossify.clock.helpers.STOPWATCH_RUNNING_NOTIF_ID
import org.fossify.clock.helpers.Stopwatch
import org.fossify.clock.helpers.Stopwatch.State
import org.fossify.clock.helpers.Stopwatch.UpdateListener
+import org.fossify.commons.extensions.notificationManager
import org.fossify.commons.extensions.showErrorToast
import org.fossify.commons.helpers.isOreoPlus
import org.greenrobot.eventbus.EventBus
@@ -26,14 +27,12 @@ import org.greenrobot.eventbus.ThreadMode
class StopwatchService : Service() {
private val bus = EventBus.getDefault()
- private lateinit var notificationManager: NotificationManager
private lateinit var notificationBuilder: NotificationCompat.Builder
private var isStopping = false
override fun onCreate() {
super.onCreate()
bus.register(this)
- notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationBuilder = getServiceNotificationBuilder(
getString(R.string.app_name),
getString(R.string.stopwatch)
@@ -71,8 +70,8 @@ class StopwatchService : Service() {
): NotificationCompat.Builder {
val channelId = "simple_alarm_stopwatch"
val label = getString(R.string.stopwatch)
- val importance = NotificationManager.IMPORTANCE_DEFAULT
if (isOreoPlus()) {
+ val importance = NotificationManager.IMPORTANCE_DEFAULT
NotificationChannel(channelId, label, importance).apply {
setSound(null, null)
notificationManager.createNotificationChannel(this)
@@ -93,7 +92,8 @@ class StopwatchService : Service() {
private fun updateNotification(totalTime: Long) {
val formattedDuration = totalTime.getFormattedDuration()
- notificationBuilder.setContentTitle(formattedDuration).setContentText(getString(R.string.stopwatch))
+ notificationBuilder.setContentTitle(formattedDuration)
+ .setContentText(getString(R.string.stopwatch))
notificationManager.notify(STOPWATCH_RUNNING_NOTIF_ID, notificationBuilder.build())
}
@@ -127,7 +127,10 @@ class StopwatchService : Service() {
fun startStopwatchService(context: Context) {
Handler(Looper.getMainLooper()).post {
try {
- ContextCompat.startForegroundService(context, Intent(context, StopwatchService::class.java))
+ ContextCompat.startForegroundService(
+ context,
+ Intent(context, StopwatchService::class.java)
+ )
} catch (e: Exception) {
context.showErrorToast(e)
}
diff --git a/app/src/main/kotlin/org/fossify/clock/services/TimerService.kt b/app/src/main/kotlin/org/fossify/clock/services/TimerService.kt
index 18afb4ac..416ae065 100644
--- a/app/src/main/kotlin/org/fossify/clock/services/TimerService.kt
+++ b/app/src/main/kotlin/org/fossify/clock/services/TimerService.kt
@@ -19,6 +19,7 @@ import org.fossify.clock.helpers.INVALID_TIMER_ID
import org.fossify.clock.helpers.TIMER_RUNNING_NOTIF_ID
import org.fossify.clock.models.TimerEvent
import org.fossify.clock.models.TimerState
+import org.fossify.commons.extensions.notificationManager
import org.fossify.commons.extensions.showErrorToast
import org.fossify.commons.helpers.isOreoPlus
import org.greenrobot.eventbus.EventBus
@@ -40,7 +41,14 @@ class TimerService : Service() {
super.onStartCommand(intent, flags, startId)
isStopping = false
updateNotification()
- startForeground(TIMER_RUNNING_NOTIF_ID, notification(getString(R.string.app_name), getString(R.string.timers_notification_msg), INVALID_TIMER_ID))
+ startForeground(
+ TIMER_RUNNING_NOTIF_ID,
+ notification(
+ title = getString(R.string.app_name),
+ contentText = getString(R.string.timers_notification_msg),
+ firstRunningTimerId = INVALID_TIMER_ID
+ )
+ )
return START_NOT_STICKY
}
@@ -49,15 +57,31 @@ class TimerService : Service() {
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 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)
+ 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
+ )
}
Handler(Looper.getMainLooper()).post {
try {
- startForeground(TIMER_RUNNING_NOTIF_ID, notification(formattedDuration, contextText, firstTimer.id!!))
+ startForeground(
+ TIMER_RUNNING_NOTIF_ID,
+ notification(
+ title = formattedDuration,
+ contentText = contextText,
+ firstRunningTimerId = firstTimer.id!!
+ )
+ )
} catch (e: Exception) {
showErrorToast(e)
}
@@ -94,10 +118,13 @@ class TimerService : Service() {
bus.unregister(this)
}
- private fun notification(title: String, contentText: String, firstRunningTimerId: Int): 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
if (isOreoPlus()) {
val importance = NotificationManager.IMPORTANCE_DEFAULT
NotificationChannel(channelId, label, importance).apply {
diff --git a/app/src/main/res/layout/activity_reminder.xml b/app/src/main/res/layout/activity_alarm.xml
similarity index 89%
rename from app/src/main/res/layout/activity_reminder.xml
rename to app/src/main/res/layout/activity_alarm.xml
index 95403643..ee3191cf 100644
--- a/app/src/main/res/layout/activity_reminder.xml
+++ b/app/src/main/res/layout/activity_alarm.xml
@@ -16,7 +16,7 @@
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
- tools:text="@string/time_expired" />
+ tools:text="Wake up!" />
+ tools:text="05:00 AM" />
-
-