From 33100df63352fcbd881f2a4ccbaeb46d802b2f83 Mon Sep 17 00:00:00 2001 From: Felix Wiemuth <533601+felixwiemuth@users.noreply.github.com> Date: Fri, 16 Sep 2022 14:23:14 +0200 Subject: [PATCH 01/12] Remove habits from active list when should not be shown --- .../java/org/isoron/uhabits/core/ui/NotificationTray.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt index 5239ed44b..b2f085d34 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt @@ -122,6 +122,7 @@ class NotificationTray @Inject constructor( habit.id ) ) + active.remove(habit) return } if (!habit.hasReminder()) { @@ -132,6 +133,7 @@ class NotificationTray @Inject constructor( habit.id ) ) + active.remove(habit) return } if (habit.isArchived) { @@ -142,6 +144,7 @@ class NotificationTray @Inject constructor( habit.id ) ) + active.remove(habit) return } if (!shouldShowReminderToday()) { @@ -152,6 +155,7 @@ class NotificationTray @Inject constructor( habit.id ) ) + active.remove(habit) return } systemTray.showNotification( From 0d1d8a9eeb9acfed3ca4b572172e0020d3611a84 Mon Sep 17 00:00:00 2001 From: Felix Wiemuth <533601+felixwiemuth@users.noreply.github.com> Date: Fri, 16 Sep 2022 14:23:14 +0200 Subject: [PATCH 02/12] Remove redundant rescheduling after reboot The action is already performed via HabitsApplication.onCreate. It is sufficient that there is a boot receiver. --- .../java/org/isoron/uhabits/receivers/ReminderController.kt | 4 ---- .../java/org/isoron/uhabits/receivers/ReminderReceiver.kt | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderController.kt b/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderController.kt index 38ba3a1b4..718e7f535 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderController.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderController.kt @@ -37,10 +37,6 @@ class ReminderController @Inject constructor( private val notificationTray: NotificationTray, private val preferences: Preferences ) { - fun onBootCompleted() { - reminderScheduler.scheduleAll() - } - fun onShowReminder( habit: Habit, timestamp: Timestamp, diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderReceiver.kt b/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderReceiver.kt index 6eb10dc7e..a08e817e6 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderReceiver.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderReceiver.kt @@ -96,7 +96,7 @@ class ReminderReceiver : BroadcastReceiver() { } Intent.ACTION_BOOT_COMPLETED -> { Log.d("ReminderReceiver", "onBootCompleted") - reminderController.onBootCompleted() + // NOTE: Some activity is executed after boot through HabitsApplication, so receiving ACTION_BOOT_COMPLETED is essential. } } } catch (e: RuntimeException) { From 794289b9b80d5657ba6b84299367cef5b8cf2b46 Mon Sep 17 00:00:00 2001 From: Felix Wiemuth <533601+felixwiemuth@users.noreply.github.com> Date: Fri, 16 Sep 2022 14:23:14 +0200 Subject: [PATCH 03/12] Add dependency and plugin: kotlinx-serialization-json --- uhabits-core/build.gradle.kts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/uhabits-core/build.gradle.kts b/uhabits-core/build.gradle.kts index 15f2615d4..efaf902a5 100644 --- a/uhabits-core/build.gradle.kts +++ b/uhabits-core/build.gradle.kts @@ -19,6 +19,7 @@ plugins { kotlin("multiplatform") + kotlin("plugin.serialization") version "1.7.10" id("org.jlleitschuh.gradle.ktlint") } @@ -30,6 +31,7 @@ kotlin { dependencies { implementation(kotlin("stdlib-common")) implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-common:1.3.8") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.0") } } From 09c46f5b88ac207944e9b014b5702ecab0fd99ec Mon Sep 17 00:00:00 2001 From: Felix Wiemuth <533601+felixwiemuth@users.noreply.github.com> Date: Fri, 16 Sep 2022 14:23:14 +0200 Subject: [PATCH 04/12] Persist notifications - Persist the map of active notifications on every change and reload it on application startup. - Reshow all active notifications on application startup using the original reminder due time. --- .../org/isoron/uhabits/HabitsApplication.kt | 1 + .../org/isoron/uhabits/inject/HabitsModule.kt | 5 ++- .../isoron/uhabits/core/models/Timestamp.kt | 2 + .../uhabits/core/preferences/Preferences.kt | 23 ++++++++++ .../uhabits/core/ui/NotificationTray.kt | 44 ++++++++++++++++--- 5 files changed, 68 insertions(+), 7 deletions(-) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/HabitsApplication.kt b/uhabits-android/src/main/java/org/isoron/uhabits/HabitsApplication.kt index 4f00f4d1f..965fab31f 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/HabitsApplication.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/HabitsApplication.kt @@ -94,6 +94,7 @@ class HabitsApplication : Application() { taskRunner.execute { reminderScheduler.scheduleAll() widgetUpdater.updateWidgets() + notificationTray.reshowAll() } } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/inject/HabitsModule.kt b/uhabits-android/src/main/java/org/isoron/uhabits/inject/HabitsModule.kt index c7b6843d0..a95e610dd 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/inject/HabitsModule.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/inject/HabitsModule.kt @@ -72,9 +72,10 @@ class HabitsModule(dbFile: File) { taskRunner: TaskRunner, commandRunner: CommandRunner, preferences: Preferences, - screen: AndroidNotificationTray + screen: AndroidNotificationTray, + habitList: HabitList ): NotificationTray { - return NotificationTray(taskRunner, commandRunner, preferences, screen) + return NotificationTray(taskRunner, commandRunner, preferences, screen, habitList) } @Provides diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Timestamp.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Timestamp.kt index 2233237c0..74e7e9d10 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Timestamp.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Timestamp.kt @@ -18,6 +18,7 @@ */ package org.isoron.uhabits.core.models +import kotlinx.serialization.Serializable import org.isoron.platform.time.LocalDate import org.isoron.uhabits.core.utils.DateFormats.Companion.getCSVDateFormat import org.isoron.uhabits.core.utils.DateFormats.Companion.getDialogDateFormat @@ -29,6 +30,7 @@ import java.util.Date import java.util.GregorianCalendar import java.util.TimeZone +@Serializable data class Timestamp(var unixTime: Long) : Comparable { constructor(cal: GregorianCalendar) : this(cal.timeInMillis) diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/Preferences.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/Preferences.kt index 1598b2ca1..ff7390472 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/Preferences.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/Preferences.kt @@ -18,11 +18,17 @@ */ package org.isoron.uhabits.core.preferences +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import org.isoron.platform.time.DayOfWeek import org.isoron.platform.utils.StringUtils.Companion.joinLongs import org.isoron.platform.utils.StringUtils.Companion.splitLongs +import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.HabitList import org.isoron.uhabits.core.models.Timestamp +import org.isoron.uhabits.core.ui.NotificationTray import org.isoron.uhabits.core.ui.ThemeSwitcher import org.isoron.uhabits.core.utils.DateUtils.Companion.getFirstWeekdayNumberAccordingToLocale import java.util.LinkedList @@ -135,6 +141,23 @@ open class Preferences(private val storage: Storage) { storage.putBoolean("pref_short_toggle", enabled) } + internal fun setActiveNotifications(activeNotifications: Map) { + val activeById = activeNotifications.mapKeys { it.key.id } + val serialized = Json.encodeToString(activeById) + storage.putString("pref_active_notifications", serialized) + } + + internal fun getActiveNotifications(habitList: HabitList): HashMap { + val serialized = storage.getString("pref_active_notifications", "") + return if (serialized == "") { + HashMap() + } else { + val activeById = Json.decodeFromString(MapSerializer(Long.serializer(), NotificationTray.NotificationData.serializer()), serialized) + val activeByHabit = activeById.mapNotNull { (id, v) -> habitList.getById(id)?.let { it to v } } + activeByHabit.toMap(HashMap()) + } + } + fun removeListener(listener: Listener) { listeners.remove(listener) } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt index b2f085d34..f4439fe2f 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt @@ -18,17 +18,18 @@ */ package org.isoron.uhabits.core.ui +import kotlinx.serialization.Serializable import org.isoron.uhabits.core.AppScope import org.isoron.uhabits.core.commands.Command import org.isoron.uhabits.core.commands.CommandRunner import org.isoron.uhabits.core.commands.CreateRepetitionCommand import org.isoron.uhabits.core.commands.DeleteHabitsCommand import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.HabitList import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.tasks.Task import org.isoron.uhabits.core.tasks.TaskRunner -import java.util.HashMap import java.util.Locale import java.util.Objects import javax.inject.Inject @@ -38,9 +39,31 @@ class NotificationTray @Inject constructor( private val taskRunner: TaskRunner, private val commandRunner: CommandRunner, private val preferences: Preferences, - private val systemTray: SystemTray + private val systemTray: SystemTray, + private val habitList: HabitList ) : CommandRunner.Listener, Preferences.Listener { - private val active: HashMap = HashMap() + + /** + * A mapping from habits to active notifications, automatically persisting on removal. + */ + private val active = object { + private val m: HashMap = + preferences.getActiveNotifications(habitList) + + val entries get() = m.entries + + operator fun set(habit: Habit, notificationData: NotificationData) { + m[habit] = notificationData + persist() + } + + fun remove(habit: Habit) { + m.remove(habit)?.let { persist() } // persist if changed + } + + fun persist() = preferences.setActiveNotifications(m) + } + fun cancel(habit: Habit) { val notificationId = getNotificationId(habit) systemTray.removeNotification(notificationId) @@ -64,7 +87,6 @@ class NotificationTray @Inject constructor( fun show(habit: Habit, timestamp: Timestamp, reminderTime: Long) { val data = NotificationData(timestamp, reminderTime) - active[habit] = data taskRunner.execute(ShowNotificationTask(habit, data)) } @@ -83,7 +105,7 @@ class NotificationTray @Inject constructor( return (id % Int.MAX_VALUE).toInt() } - private fun reshowAll() { + fun reshowAll() { for ((habit, data) in active.entries) { taskRunner.execute(ShowNotificationTask(habit, data)) } @@ -101,6 +123,7 @@ class NotificationTray @Inject constructor( fun log(msg: String) } + @Serializable internal class NotificationData(val timestamp: Timestamp, val reminderTime: Long) private inner class ShowNotificationTask(private val habit: Habit, data: NotificationData) : Task { @@ -164,6 +187,17 @@ class NotificationTray @Inject constructor( timestamp, reminderTime ) + if (data.shown) { + systemTray.log( + String.format( + Locale.US, + "Showing notification for habit %d silently because it has been shown before.", + habit.id + ) + ) + } + data.shown = true + active[habit] = data } private fun shouldShowReminderToday(): Boolean { From 857e21af1d5be331acf3540b66bf093f8443645d Mon Sep 17 00:00:00 2001 From: Felix Wiemuth <533601+felixwiemuth@users.noreply.github.com> Date: Fri, 16 Sep 2022 14:23:14 +0200 Subject: [PATCH 05/12] Show previously shown notifications silently --- .../notifications/AndroidNotificationTray.kt | 12 ++++++---- .../uhabits/core/ui/NotificationTray.kt | 24 ++++++++++++------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/notifications/AndroidNotificationTray.kt b/uhabits-android/src/main/java/org/isoron/uhabits/notifications/AndroidNotificationTray.kt index 09d8f8b46..271892b9b 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/notifications/AndroidNotificationTray.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/notifications/AndroidNotificationTray.kt @@ -66,10 +66,11 @@ class AndroidNotificationTray habit: Habit, notificationId: Int, timestamp: Timestamp, - reminderTime: Long + reminderTime: Long, + silent: Boolean ) { val notificationManager = NotificationManagerCompat.from(context) - val notification = buildNotification(habit, reminderTime, timestamp) + val notification = buildNotification(habit, reminderTime, timestamp, silent = silent) createAndroidNotificationChannel(context) try { notificationManager.notify(notificationId, notification) @@ -83,7 +84,8 @@ class AndroidNotificationTray habit, reminderTime, timestamp, - disableSound = true + disableSound = true, + silent = silent ) notificationManager.notify(notificationId, n) } @@ -94,7 +96,8 @@ class AndroidNotificationTray habit: Habit, reminderTime: Long, timestamp: Timestamp, - disableSound: Boolean = false + disableSound: Boolean = false, + silent: Boolean = false ): Notification { val addRepetitionAction = Action( @@ -132,6 +135,7 @@ class AndroidNotificationTray .setSound(null) .setWhen(reminderTime) .setShowWhen(true) + .setSilent(silent) .setOngoing(preferences.shouldMakeNotificationsSticky()) if (habit.isNumerical) { diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt index f4439fe2f..09bb9596a 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt @@ -117,19 +117,26 @@ class NotificationTray @Inject constructor( habit: Habit, notificationId: Int, timestamp: Timestamp, - reminderTime: Long + reminderTime: Long, + silent: Boolean = false ) fun log(msg: String) } @Serializable - internal class NotificationData(val timestamp: Timestamp, val reminderTime: Long) - private inner class ShowNotificationTask(private val habit: Habit, data: NotificationData) : + internal class NotificationData( + val timestamp: Timestamp, + val reminderTime: Long, + var shown: Boolean = false + ) + + private inner class ShowNotificationTask( + private val habit: Habit, + private val data: NotificationData + ) : Task { var isCompleted = false - private val timestamp: Timestamp = data.timestamp - private val reminderTime: Long = data.reminderTime override fun doInBackground() { isCompleted = habit.isCompletedToday() @@ -184,8 +191,9 @@ class NotificationTray @Inject constructor( systemTray.showNotification( habit, getNotificationId(habit), - timestamp, - reminderTime + data.timestamp, + data.reminderTime, + silent = data.shown ) if (data.shown) { systemTray.log( @@ -204,7 +212,7 @@ class NotificationTray @Inject constructor( if (!habit.hasReminder()) return false val reminder = habit.reminder val reminderDays = Objects.requireNonNull(reminder)!!.days.toArray() - val weekday = timestamp.weekday + val weekday = data.timestamp.weekday return reminderDays[weekday] } } From 68f2639c77632a61dee03064e6a911a01cf22683 Mon Sep 17 00:00:00 2001 From: Felix Wiemuth <533601+felixwiemuth@users.noreply.github.com> Date: Sat, 17 Sep 2022 13:15:20 +0200 Subject: [PATCH 06/12] Add app update broadcast receiver So that notifications are also restored after app upgrade --- uhabits-android/src/main/AndroidManifest.xml | 8 ++++++++ .../org/isoron/uhabits/receivers/UpdateReceiver.kt | 14 ++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/receivers/UpdateReceiver.kt diff --git a/uhabits-android/src/main/AndroidManifest.xml b/uhabits-android/src/main/AndroidManifest.xml index 9c114d733..d92d66bd6 100644 --- a/uhabits-android/src/main/AndroidManifest.xml +++ b/uhabits-android/src/main/AndroidManifest.xml @@ -217,6 +217,14 @@ + + + + + + diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/receivers/UpdateReceiver.kt b/uhabits-android/src/main/java/org/isoron/uhabits/receivers/UpdateReceiver.kt new file mode 100644 index 000000000..63add51bc --- /dev/null +++ b/uhabits-android/src/main/java/org/isoron/uhabits/receivers/UpdateReceiver.kt @@ -0,0 +1,14 @@ +package org.isoron.uhabits.receivers + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log + +class UpdateReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent) { + // Dummy receiver, relevant code is executed through HabitsApplication. + Log.d("UpdateReceiver", "Update receiver called.") + } +} From 41743595d5cd65208561a9ee331e0d09d6ce8d3f Mon Sep 17 00:00:00 2001 From: Felix Wiemuth <533601+felixwiemuth@users.noreply.github.com> Date: Sun, 18 Sep 2022 10:22:29 +0200 Subject: [PATCH 07/12] Remove `shown` field from `NotificationData` It is not necessary to keep this field as in the map of active notifications (and its persisted form) this field is always `true`. --- .../org/isoron/uhabits/core/ui/NotificationTray.kt | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt index 09bb9596a..a5a46333c 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt @@ -87,7 +87,7 @@ class NotificationTray @Inject constructor( fun show(habit: Habit, timestamp: Timestamp, reminderTime: Long) { val data = NotificationData(timestamp, reminderTime) - taskRunner.execute(ShowNotificationTask(habit, data)) + taskRunner.execute(ShowNotificationTask(habit, data, false)) } fun startListening() { @@ -107,7 +107,7 @@ class NotificationTray @Inject constructor( fun reshowAll() { for ((habit, data) in active.entries) { - taskRunner.execute(ShowNotificationTask(habit, data)) + taskRunner.execute(ShowNotificationTask(habit, data, true)) } } @@ -128,12 +128,12 @@ class NotificationTray @Inject constructor( internal class NotificationData( val timestamp: Timestamp, val reminderTime: Long, - var shown: Boolean = false ) private inner class ShowNotificationTask( private val habit: Habit, - private val data: NotificationData + private val data: NotificationData, + private val shown: Boolean ) : Task { var isCompleted = false @@ -193,9 +193,9 @@ class NotificationTray @Inject constructor( getNotificationId(habit), data.timestamp, data.reminderTime, - silent = data.shown + silent = shown ) - if (data.shown) { + if (shown) { systemTray.log( String.format( Locale.US, @@ -204,7 +204,6 @@ class NotificationTray @Inject constructor( ) ) } - data.shown = true active[habit] = data } From f1c104a942dbf0598cdcd750427ff7caa1561ef8 Mon Sep 17 00:00:00 2001 From: Felix Wiemuth <533601+felixwiemuth@users.noreply.github.com> Date: Sun, 25 Sep 2022 10:10:40 +0200 Subject: [PATCH 08/12] Catch serialization exception In case serialized notifications cannot be deserialized, continue with an empty map of active notifications --- .../uhabits/core/preferences/Preferences.kt | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/Preferences.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/Preferences.kt index ff7390472..aa0a010ef 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/Preferences.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/Preferences.kt @@ -18,6 +18,7 @@ */ package org.isoron.uhabits.core.preferences +import kotlinx.serialization.SerializationException import kotlinx.serialization.builtins.MapSerializer import kotlinx.serialization.builtins.serializer import kotlinx.serialization.encodeToString @@ -152,9 +153,18 @@ open class Preferences(private val storage: Storage) { return if (serialized == "") { HashMap() } else { - val activeById = Json.decodeFromString(MapSerializer(Long.serializer(), NotificationTray.NotificationData.serializer()), serialized) - val activeByHabit = activeById.mapNotNull { (id, v) -> habitList.getById(id)?.let { it to v } } - activeByHabit.toMap(HashMap()) + try { + val activeById = Json.decodeFromString( + MapSerializer(Long.serializer(), NotificationTray.NotificationData.serializer()), + serialized + ) + val activeByHabit = activeById.mapNotNull { (id, v) -> habitList.getById(id)?.let { it to v } } + activeByHabit.toMap(HashMap()) + } catch (e : IllegalArgumentException) { + HashMap() + } catch (e : SerializationException) { + HashMap() + } } } From 015c0d12c764a6f14c861752912f26b4807d1dac Mon Sep 17 00:00:00 2001 From: Felix Wiemuth <533601+felixwiemuth@users.noreply.github.com> Date: Sun, 25 Sep 2022 14:58:17 +0200 Subject: [PATCH 09/12] Better name: `NotificationData.silent` --- .../org/isoron/uhabits/core/ui/NotificationTray.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt index a5a46333c..dff03efe4 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt @@ -87,7 +87,7 @@ class NotificationTray @Inject constructor( fun show(habit: Habit, timestamp: Timestamp, reminderTime: Long) { val data = NotificationData(timestamp, reminderTime) - taskRunner.execute(ShowNotificationTask(habit, data, false)) + taskRunner.execute(ShowNotificationTask(habit, data, silent = false)) } fun startListening() { @@ -107,7 +107,7 @@ class NotificationTray @Inject constructor( fun reshowAll() { for ((habit, data) in active.entries) { - taskRunner.execute(ShowNotificationTask(habit, data, true)) + taskRunner.execute(ShowNotificationTask(habit, data, silent = true)) } } @@ -133,7 +133,7 @@ class NotificationTray @Inject constructor( private inner class ShowNotificationTask( private val habit: Habit, private val data: NotificationData, - private val shown: Boolean + private val silent: Boolean ) : Task { var isCompleted = false @@ -193,9 +193,9 @@ class NotificationTray @Inject constructor( getNotificationId(habit), data.timestamp, data.reminderTime, - silent = shown + silent = silent ) - if (shown) { + if (silent) { systemTray.log( String.format( Locale.US, From 15404276dff7dc351c76c811a65e4d10011218ca Mon Sep 17 00:00:00 2001 From: Felix Wiemuth <533601+felixwiemuth@users.noreply.github.com> Date: Sun, 2 Oct 2022 11:54:40 +0200 Subject: [PATCH 10/12] Add tests for active notifications persistence --- .../uhabits/core/preferences/Preferences.kt | 4 +- .../uhabits/core/ui/NotificationTray.kt | 20 ++- .../core/preferences/PreferencesTest.kt | 28 ++++ .../uhabits/core/ui/NotificationTrayTest.kt | 125 ++++++++++++++++++ 4 files changed, 174 insertions(+), 3 deletions(-) create mode 100644 uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/NotificationTrayTest.kt diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/Preferences.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/Preferences.kt index aa0a010ef..37d0b6e06 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/Preferences.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/Preferences.kt @@ -142,13 +142,13 @@ open class Preferences(private val storage: Storage) { storage.putBoolean("pref_short_toggle", enabled) } - internal fun setActiveNotifications(activeNotifications: Map) { + internal open fun setActiveNotifications(activeNotifications: Map) { val activeById = activeNotifications.mapKeys { it.key.id } val serialized = Json.encodeToString(activeById) storage.putString("pref_active_notifications", serialized) } - internal fun getActiveNotifications(habitList: HabitList): HashMap { + internal open fun getActiveNotifications(habitList: HabitList): HashMap { val serialized = storage.getString("pref_active_notifications", "") return if (serialized == "") { HashMap() diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt index dff03efe4..29a84e6e2 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt @@ -128,7 +128,25 @@ class NotificationTray @Inject constructor( internal class NotificationData( val timestamp: Timestamp, val reminderTime: Long, - ) + ) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as NotificationData + + if (timestamp != other.timestamp) return false + if (reminderTime != other.reminderTime) return false + + return true + } + + override fun hashCode(): Int { + var result = timestamp.hashCode() + result = 31 * result + reminderTime.hashCode() + return result + } + } private inner class ShowNotificationTask( private val habit: Habit, diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/preferences/PreferencesTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/preferences/PreferencesTest.kt index 52117c181..4be8bedeb 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/preferences/PreferencesTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/preferences/PreferencesTest.kt @@ -25,8 +25,11 @@ import junit.framework.Assert.assertTrue import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat import org.isoron.uhabits.core.BaseUnitTest +import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.HabitList +import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.models.Timestamp.Companion.ZERO +import org.isoron.uhabits.core.ui.NotificationTray import org.isoron.uhabits.core.ui.ThemeSwitcher import org.junit.Before import org.junit.Test @@ -162,6 +165,31 @@ class PreferencesTest : BaseUnitTest() { assertFalse(prefs.showCompleted) } + @Test + @Throws(Exception::class) + fun testActiveNotifications() { + repeat(5) { habitList.add(fixtures.createEmptyHabit()) } + + // Initially no active notifications + assertThat(prefs.getActiveNotifications(habitList), equalTo(HashMap())) + + // Example map of active notifications + val a = HashMap() + for (i in listOf(0, 1, 3)) { + val habit = habitList.getByPosition(i) + val data = NotificationTray.NotificationData(Timestamp(10000L * i), 200000L * i) + a[habit] = data + } + + // Persist and retrieve active notifications + prefs.setActiveNotifications(a) + val b = prefs.getActiveNotifications(habitList) + + // Assert that persisted and retrieved maps are teh same + assertThat(a.keys, equalTo(b.keys)) + a.forEach { e -> assertThat(b[e.key], equalTo(e.value)) } + } + @Test @Throws(Exception::class) fun testMidnightDelay() { diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/NotificationTrayTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/NotificationTrayTest.kt new file mode 100644 index 000000000..931386367 --- /dev/null +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/NotificationTrayTest.kt @@ -0,0 +1,125 @@ +package org.isoron.uhabits.core.ui + +import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.MatcherAssert.assertThat +import org.isoron.uhabits.core.BaseUnitTest +import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.HabitList +import org.isoron.uhabits.core.models.Reminder +import org.isoron.uhabits.core.models.Timestamp +import org.isoron.uhabits.core.models.WeekdayList +import org.isoron.uhabits.core.preferences.Preferences +import org.isoron.uhabits.core.preferences.Preferences.Storage +import org.junit.Before +import org.junit.Test + +class NotificationTrayTest : BaseUnitTest() { + private val systemTray = object : NotificationTray.SystemTray { + override fun removeNotification(notificationId: Int) {} + + override fun showNotification( + habit: Habit, + notificationId: Int, + timestamp: Timestamp, + reminderTime: Long, + silent: Boolean + ) { + } + + override fun log(msg: String) {} + } + + private var preferences = MockPreferences() + private lateinit var notificationTray: NotificationTray + + class DummyStorage : Storage { + override fun clear() { + throw NotImplementedError("Mock implementation missing") + } + + override fun getBoolean(key: String, defValue: Boolean): Boolean { + throw NotImplementedError("Mock implementation missing") + } + + override fun getInt(key: String, defValue: Int): Int { + throw NotImplementedError("Mock implementation missing") + } + + override fun getLong(key: String, defValue: Long): Long { + throw NotImplementedError("Mock implementation missing") + } + + override fun getString(key: String, defValue: String): String { + throw NotImplementedError("Mock implementation missing") + } + + override fun onAttached(preferences: Preferences) { + } + + override fun putBoolean(key: String, value: Boolean) { + throw NotImplementedError("Mock implementation missing") + } + + override fun putInt(key: String, value: Int) { + throw NotImplementedError("Mock implementation missing") + } + + override fun putLong(key: String, value: Long) { + throw NotImplementedError("Mock implementation missing") + } + + override fun putString(key: String, value: String) { + throw NotImplementedError("Mock implementation missing") + } + + override fun remove(key: String) { + throw NotImplementedError("Mock implementation missing") + } + } + + class MockPreferences : Preferences(DummyStorage()) { + private var activeNotifications: HashMap = + HashMap() + + override fun setActiveNotifications(activeNotifications: Map) { + this.activeNotifications = HashMap(activeNotifications) + } + + override fun getActiveNotifications(habitList: HabitList): HashMap { + return activeNotifications + } + } + + @Before + @Throws(Exception::class) + override fun setUp() { + super.setUp() + notificationTray = + NotificationTray(taskRunner, commandRunner, preferences, systemTray, habitList) + } + + @Test + @Throws(Exception::class) + fun testShow() { + // Show a reminder for a habit + val habit = fixtures.createEmptyHabit() + habit.reminder = Reminder(8, 30, WeekdayList.EVERY_DAY) + val timestamp = Timestamp(System.currentTimeMillis()) + val reminderTime = System.currentTimeMillis() + notificationTray.show(habit, timestamp, reminderTime) + + // Verify that the active notifications include exactly the one shown reminder + // TODO are we guaranteed that task has executed? + assertThat(preferences.getActiveNotifications(habitList).size, equalTo(1)) + assertThat( + preferences.getActiveNotifications(habitList)[habit], + equalTo(NotificationTray.NotificationData(timestamp, reminderTime)) + ) + + // Remove the reminder from the notification tray and verify that active notifications are empty + notificationTray.cancel(habit) + assertThat(preferences.getActiveNotifications(habitList).size, equalTo(0)) + + // TODO test cases where reminders should be removed (e.g. reshowAll) + } +} \ No newline at end of file From e65357bc14aeb7aaf670cf594a289f26c4d5f73f Mon Sep 17 00:00:00 2001 From: Felix Wiemuth <533601+felixwiemuth@users.noreply.github.com> Date: Sun, 2 Oct 2022 12:14:10 +0200 Subject: [PATCH 11/12] Make `NotificationData` data class --- .../uhabits/core/ui/NotificationTray.kt | 22 ++----------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt index 29a84e6e2..f1a0abc96 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt @@ -125,28 +125,10 @@ class NotificationTray @Inject constructor( } @Serializable - internal class NotificationData( + internal data class NotificationData( val timestamp: Timestamp, val reminderTime: Long, - ) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as NotificationData - - if (timestamp != other.timestamp) return false - if (reminderTime != other.reminderTime) return false - - return true - } - - override fun hashCode(): Int { - var result = timestamp.hashCode() - result = 31 * result + reminderTime.hashCode() - return result - } - } + ) private inner class ShowNotificationTask( private val habit: Habit, From ee0663e5e5be5b1676542c9fa243b55e843e0533 Mon Sep 17 00:00:00 2001 From: Felix Wiemuth <533601+felixwiemuth@users.noreply.github.com> Date: Sun, 2 Oct 2022 12:19:11 +0200 Subject: [PATCH 12/12] Fix code style --- .../isoron/uhabits/core/preferences/Preferences.kt | 12 ++++++++---- .../isoron/uhabits/core/ui/NotificationTrayTest.kt | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/Preferences.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/Preferences.kt index 37d0b6e06..71308e8a1 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/Preferences.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/Preferences.kt @@ -155,14 +155,18 @@ open class Preferences(private val storage: Storage) { } else { try { val activeById = Json.decodeFromString( - MapSerializer(Long.serializer(), NotificationTray.NotificationData.serializer()), + MapSerializer( + Long.serializer(), + NotificationTray.NotificationData.serializer() + ), serialized ) - val activeByHabit = activeById.mapNotNull { (id, v) -> habitList.getById(id)?.let { it to v } } + val activeByHabit = + activeById.mapNotNull { (id, v) -> habitList.getById(id)?.let { it to v } } activeByHabit.toMap(HashMap()) - } catch (e : IllegalArgumentException) { + } catch (e: IllegalArgumentException) { HashMap() - } catch (e : SerializationException) { + } catch (e: SerializationException) { HashMap() } } diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/NotificationTrayTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/NotificationTrayTest.kt index 931386367..2febcf3f1 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/NotificationTrayTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/NotificationTrayTest.kt @@ -122,4 +122,4 @@ class NotificationTrayTest : BaseUnitTest() { // TODO test cases where reminders should be removed (e.g. reshowAll) } -} \ No newline at end of file +}