-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Restore notifications when app starts #1509
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from 7 commits
33100df
0d1d8a9
794289b
09c46f5
857e21a
68f2639
4174359
f1c104a
015c0d1
1540427
e65357b
ee0663e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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.") | ||
| } | ||
| } | ||
|
Comment on lines
+8
to
+14
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need a new (empty) receiver? Could we add
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See comment above. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<Habit, NotificationTray.NotificationData>) { | ||
| val activeById = activeNotifications.mapKeys { it.key.id } | ||
| val serialized = Json.encodeToString(activeById) | ||
| storage.putString("pref_active_notifications", serialized) | ||
| } | ||
|
|
||
| internal fun getActiveNotifications(habitList: HabitList): HashMap<Habit, NotificationTray.NotificationData> { | ||
| 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) | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<Habit, NotificationData> = HashMap() | ||
|
|
||
| /** | ||
| * A mapping from habits to active notifications, automatically persisting on removal. | ||
| */ | ||
| private val active = object { | ||
| private val m: HashMap<Habit, NotificationData> = | ||
| 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,8 +87,7 @@ 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)) | ||
| taskRunner.execute(ShowNotificationTask(habit, data, false)) | ||
| } | ||
|
|
||
| fun startListening() { | ||
|
|
@@ -83,9 +105,9 @@ 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)) | ||
| taskRunner.execute(ShowNotificationTask(habit, data, true)) | ||
|
||
| } | ||
| } | ||
|
|
||
|
|
@@ -95,18 +117,26 @@ class NotificationTray @Inject constructor( | |
| habit: Habit, | ||
| notificationId: Int, | ||
| timestamp: Timestamp, | ||
| reminderTime: Long | ||
| reminderTime: Long, | ||
| silent: Boolean = false | ||
| ) | ||
|
|
||
| fun log(msg: String) | ||
| } | ||
|
|
||
| internal class NotificationData(val timestamp: Timestamp, val reminderTime: Long) | ||
| private inner class ShowNotificationTask(private val habit: Habit, data: NotificationData) : | ||
| @Serializable | ||
| internal class NotificationData( | ||
| val timestamp: Timestamp, | ||
| val reminderTime: Long, | ||
| ) | ||
|
|
||
| private inner class ShowNotificationTask( | ||
| private val habit: Habit, | ||
| private val data: NotificationData, | ||
| private val shown: Boolean | ||
|
||
| ) : | ||
| Task { | ||
| var isCompleted = false | ||
| private val timestamp: Timestamp = data.timestamp | ||
| private val reminderTime: Long = data.reminderTime | ||
|
|
||
| override fun doInBackground() { | ||
| isCompleted = habit.isCompletedToday() | ||
|
|
@@ -122,6 +152,7 @@ class NotificationTray @Inject constructor( | |
| habit.id | ||
| ) | ||
| ) | ||
| active.remove(habit) | ||
| return | ||
| } | ||
| if (!habit.hasReminder()) { | ||
|
|
@@ -132,6 +163,7 @@ class NotificationTray @Inject constructor( | |
| habit.id | ||
| ) | ||
| ) | ||
| active.remove(habit) | ||
| return | ||
| } | ||
| if (habit.isArchived) { | ||
|
|
@@ -142,6 +174,7 @@ class NotificationTray @Inject constructor( | |
| habit.id | ||
| ) | ||
| ) | ||
| active.remove(habit) | ||
| return | ||
| } | ||
| if (!shouldShowReminderToday()) { | ||
|
|
@@ -152,21 +185,33 @@ class NotificationTray @Inject constructor( | |
| habit.id | ||
| ) | ||
| ) | ||
| active.remove(habit) | ||
| return | ||
| } | ||
| systemTray.showNotification( | ||
| habit, | ||
| getNotificationId(habit), | ||
| timestamp, | ||
| reminderTime | ||
| data.timestamp, | ||
| data.reminderTime, | ||
| silent = shown | ||
| ) | ||
| if (shown) { | ||
| systemTray.log( | ||
| String.format( | ||
| Locale.US, | ||
| "Showing notification for habit %d silently because it has been shown before.", | ||
| habit.id | ||
| ) | ||
| ) | ||
| } | ||
| active[habit] = data | ||
| } | ||
|
|
||
| private fun shouldShowReminderToday(): Boolean { | ||
| 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] | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you clarify why do we still need this block? We would still receive
ACTION_BOOT_COMPLETED, even if we remove the block.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The block is not needed (and I can see the intent is also logged above); What I deemed important is that somewhere it is noted that it is important that the
ACTION_BOOT_COMPLETEDintent is received.Actually, as for both
ACTION_BOOT_COMPLETEDandMY_PACKAGE_REPLACEDthe only thing we need is that the application is started (and all the code fromReminderReceiveris not needed), I would suggest that both should be received by a common dummy receiver (as currently withUpdateReceiver), which we could callStartAppReceiveror similar. Then it is clear that these intents are solely received to let the app start.