-
Notifications
You must be signed in to change notification settings - Fork 935
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
Restore notifications when app starts #1509
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
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 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) | ||
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. What happens if the decoding fails? Does the app fail to boot? I think this could happen if: (i) user receives some notifications; (ii) we update I suggest adding a try/catch block here, and returning 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. Yes currently there would be an uncaught exception if decoding fails, and the app would crash on startup. That is of course bad, even though unlikely. Returning an empty map in case it cannot be deserialized is fine, as (as far as I can see) no other functionality depends on all active notifications being available in the map. What we could do is show a notification that notifications could not be restored and the user should make sure to look at the habits manually, but regarding that it is a corner case it's probably not worth it. Btw., decoding will not fail if fields not present in the encoded string have default values in the Kotlin class. |
||
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)) | ||
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. For boolean arguments, it's best to add the argument name ( 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. Done. |
||
} | ||
} | ||
|
||
|
@@ -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 | ||
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.
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. Agree, done. |
||
) : | ||
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_COMPLETED
intent is received.Actually, as for both
ACTION_BOOT_COMPLETED
andMY_PACKAGE_REPLACED
the only thing we need is that the application is started (and all the code fromReminderReceiver
is not needed), I would suggest that both should be received by a common dummy receiver (as currently withUpdateReceiver
), which we could callStartAppReceiver
or similar. Then it is clear that these intents are solely received to let the app start.