Implement notifications for both habit groups and sub habits

This commit is contained in:
Dharanish
2024-07-10 20:28:40 +02:00
parent 32c69772ae
commit 8fac8afadf
11 changed files with 471 additions and 53 deletions

View File

@@ -43,7 +43,7 @@ data class HabitGroup(
var observable = ModelObservable()
val uriString: String
get() = "content://org.isoron.uhabits/habit/$id"
get() = "content://org.isoron.uhabits/habitgroup/$id"
fun hasReminder(): Boolean = reminder != null

View File

@@ -24,6 +24,8 @@ 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.models.Habit
import org.isoron.uhabits.core.models.HabitGroup
import org.isoron.uhabits.core.models.HabitGroupList
import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.HabitMatcher
import org.isoron.uhabits.core.preferences.WidgetPreferences
@@ -39,6 +41,7 @@ import javax.inject.Inject
class ReminderScheduler @Inject constructor(
private val commandRunner: CommandRunner,
private val habitList: HabitList,
private val habitGroupList: HabitGroupList,
private val sys: SystemScheduler,
private val widgetPreferences: WidgetPreferences
) : CommandRunner.Listener {
@@ -83,6 +86,40 @@ class ReminderScheduler @Inject constructor(
scheduleAtTime(habit, reminderTime)
}
@Synchronized
fun schedule(habitGroup: HabitGroup) {
if (habitGroup.id == null) {
sys.log("ReminderScheduler", "Habit group has null id. Returning.")
return
}
if (!habitGroup.hasReminder()) {
sys.log("ReminderScheduler", "habit group=" + habitGroup.id + " has no reminder. Skipping.")
return
}
var reminderTime = Objects.requireNonNull(habitGroup.reminder)!!.timeInMillis
val snoozeReminderTime = widgetPreferences.getSnoozeTime(habitGroup.id!!)
if (snoozeReminderTime != 0L) {
val now = applyTimezone(getLocalTime())
sys.log(
"ReminderScheduler",
String.format(
Locale.US,
"Habit group %d has been snoozed until %d",
habitGroup.id,
snoozeReminderTime
)
)
if (snoozeReminderTime > now) {
sys.log("ReminderScheduler", "Snooze time is in the future. Accepting.")
reminderTime = snoozeReminderTime
} else {
sys.log("ReminderScheduler", "Snooze time is in the past. Discarding.")
widgetPreferences.removeSnoozeTime(habitGroup.id!!)
}
}
scheduleAtTime(habitGroup, reminderTime)
}
@Synchronized
fun scheduleAtTime(habit: Habit, reminderTime: Long) {
sys.log("ReminderScheduler", "Scheduling alarm for habit=" + habit.id)
@@ -108,16 +145,48 @@ class ReminderScheduler @Inject constructor(
sys.scheduleShowReminder(reminderTime, habit, timestamp)
}
@Synchronized
fun scheduleAtTime(habitGroup: HabitGroup, reminderTime: Long) {
sys.log("ReminderScheduler", "Scheduling alarm for habit group=" + habitGroup.id)
if (!habitGroup.hasReminder()) {
sys.log("ReminderScheduler", "habit group=" + habitGroup.id + " has no reminder. Skipping.")
return
}
if (habitGroup.isArchived) {
sys.log("ReminderScheduler", "habit group=" + habitGroup.id + " is archived. Skipping.")
return
}
val timestamp = getStartOfDayWithOffset(removeTimezone(reminderTime))
sys.log(
"ReminderScheduler",
String.format(
Locale.US,
"reminderTime=%d removeTimezone=%d timestamp=%d",
reminderTime,
removeTimezone(reminderTime),
timestamp
)
)
sys.scheduleShowReminder(reminderTime, habitGroup, timestamp)
}
@Synchronized
fun scheduleAll() {
sys.log("ReminderScheduler", "Scheduling all alarms")
val reminderHabits = habitList.getFiltered(HabitMatcher.WITH_ALARM)
val reminderSubHabits = habitGroupList.map { it.habitList.getFiltered(HabitMatcher.WITH_ALARM) }.flatten()
val reminderHabitGroups = habitGroupList.getFiltered(HabitMatcher.WITH_ALARM)
for (habit in reminderHabits) schedule(habit)
for (habit in reminderSubHabits) schedule(habit)
for (hgr in reminderHabitGroups) schedule(hgr)
}
@Synchronized
fun hasHabitsWithReminders(): Boolean {
return !habitList.getFiltered(HabitMatcher.WITH_ALARM).isEmpty
if (!habitList.getFiltered(HabitMatcher.WITH_ALARM).isEmpty) return true
if (habitGroupList.map { it.habitList.getFiltered(HabitMatcher.WITH_ALARM) }.flatten().isNotEmpty()) return true
if (!habitGroupList.getFiltered(HabitMatcher.WITH_ALARM).isEmpty) return true
return false
}
@Synchronized
@@ -138,6 +207,14 @@ class ReminderScheduler @Inject constructor(
schedule(habit)
}
@Synchronized
fun snoozeReminder(habitGroup: HabitGroup, minutes: Long) {
val now = applyTimezone(getLocalTime())
val snoozedUntil = now + minutes * 60 * 1000
widgetPreferences.setSnoozeTime(habitGroup.id!!, snoozedUntil)
schedule(habitGroup)
}
interface SystemScheduler {
fun scheduleShowReminder(
reminderTime: Long,
@@ -145,6 +222,12 @@ class ReminderScheduler @Inject constructor(
timestamp: Long
): SchedulerResult
fun scheduleShowReminder(
reminderTime: Long,
habitGroup: HabitGroup,
timestamp: Long
): SchedulerResult
fun scheduleWidgetUpdate(updateTime: Long): SchedulerResult?
fun log(componentName: String, msg: String)
}

View File

@@ -22,13 +22,14 @@ 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.DeleteHabitGroupsCommand
import org.isoron.uhabits.core.commands.DeleteHabitsCommand
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitGroup
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
@@ -40,11 +41,18 @@ class NotificationTray @Inject constructor(
private val preferences: Preferences,
private val systemTray: SystemTray
) : CommandRunner.Listener, Preferences.Listener {
private val active: HashMap<Habit, NotificationData> = HashMap()
private val activeHabits: HashMap<Habit, NotificationData> = HashMap()
private val activeHabitGroups: HashMap<HabitGroup, NotificationData> = HashMap()
fun cancel(habit: Habit) {
val notificationId = getNotificationId(habit)
systemTray.removeNotification(notificationId)
active.remove(habit)
activeHabits.remove(habit)
}
fun cancel(habitGroup: HabitGroup) {
val notificationId = getNotificationId(habitGroup)
systemTray.removeNotification(notificationId)
activeHabitGroups.remove(habitGroup)
}
override fun onCommandFinished(command: Command) {
@@ -56,6 +64,13 @@ class NotificationTray @Inject constructor(
val (_, deleted) = command
for (habit in deleted) cancel(habit)
}
if (command is DeleteHabitGroupsCommand) {
val (_, deletedGroups) = command
for (hgr in deletedGroups) {
for (h in hgr.habitList) cancel(h)
cancel(hgr)
}
}
}
override fun onNotificationsChanged() {
@@ -64,10 +79,16 @@ class NotificationTray @Inject constructor(
fun show(habit: Habit, timestamp: Timestamp, reminderTime: Long) {
val data = NotificationData(timestamp, reminderTime)
active[habit] = data
activeHabits[habit] = data
taskRunner.execute(ShowNotificationTask(habit, data))
}
fun show(habitGroup: HabitGroup, timestamp: Timestamp, reminderTime: Long) {
val data = NotificationData(timestamp, reminderTime)
activeHabitGroups[habitGroup] = data
taskRunner.execute(ShowNotificationTask(habitGroup, data))
}
fun startListening() {
commandRunner.addListener(this)
preferences.addListener(this)
@@ -83,14 +104,22 @@ class NotificationTray @Inject constructor(
return (id % Int.MAX_VALUE).toInt()
}
private fun getNotificationId(habitGroup: HabitGroup): Int {
val id = habitGroup.id ?: return 0
return (id % Int.MAX_VALUE).toInt()
}
private fun reshowAll() {
for ((habit, data) in active.entries) {
for ((habit, data) in activeHabits.entries) {
taskRunner.execute(ShowNotificationTask(habit, data))
}
for ((habitGroup, data) in activeHabitGroups.entries) {
taskRunner.execute(ShowNotificationTask(habitGroup, data))
}
}
fun reshow(habit: Habit) {
active[habit]?.let {
activeHabits[habit]?.let {
taskRunner.execute(ShowNotificationTask(habit, it))
}
}
@@ -104,48 +133,79 @@ class NotificationTray @Inject constructor(
reminderTime: Long
)
fun showNotification(
habitGroup: HabitGroup,
notificationId: Int,
timestamp: Timestamp,
reminderTime: Long
)
fun log(msg: String)
}
internal class NotificationData(val timestamp: Timestamp, val reminderTime: Long)
private inner class ShowNotificationTask(private val habit: Habit, data: NotificationData) :
Task {
private inner class ShowNotificationTask private constructor(
private val habit: Habit? = null,
private val habitGroup: HabitGroup? = null,
data: NotificationData
) : Task {
// Secondary constructor for Habit
constructor(habit: Habit, data: NotificationData) : this(habit, null, data)
// Secondary constructor for HabitGroup
constructor(habitGroup: HabitGroup, data: NotificationData) : this(null, habitGroup, data)
var isCompleted = false
private val timestamp: Timestamp = data.timestamp
private val reminderTime: Long = data.reminderTime
private val type = if (habit != null) "habit" else "habitgroup"
private val id = habit?.id ?: habitGroup?.id
private val hasReminder = habit?.hasReminder() ?: habitGroup!!.hasReminder()
private val isArchived = habit?.isArchived ?: habitGroup!!.isArchived
override fun doInBackground() {
isCompleted = habit.isCompletedToday()
isCompleted = habit?.isCompletedToday() ?: habitGroup!!.isCompletedToday()
}
override fun onPostExecute() {
systemTray.log("Showing notification for habit=" + habit.id)
systemTray.log(
String.format(
Locale.US,
"Showing notification for %s=%d",
type,
id
)
)
if (isCompleted) {
systemTray.log(
String.format(
Locale.US,
"Habit %d already checked. Skipping.",
habit.id
"%s %d already checked. Skipping.",
type,
id
)
)
return
}
if (!habit.hasReminder()) {
if (!hasReminder) {
systemTray.log(
String.format(
Locale.US,
"Habit %d does not have a reminder. Skipping.",
habit.id
"%s %d does not have a reminder. Skipping.",
type,
id
)
)
return
}
if (habit.isArchived) {
if (isArchived) {
systemTray.log(
String.format(
Locale.US,
"Habit %d is archived. Skipping.",
habit.id
"%s %d is archived. Skipping.",
type,
id
)
)
return
@@ -154,23 +214,33 @@ class NotificationTray @Inject constructor(
systemTray.log(
String.format(
Locale.US,
"Habit %d not supposed to run today. Skipping.",
habit.id
"%s %d not supposed to run today. Skipping.",
type,
id
)
)
return
}
systemTray.showNotification(
habit,
getNotificationId(habit),
timestamp,
reminderTime
)
if (habit != null) {
systemTray.showNotification(
habit,
getNotificationId(habit),
timestamp,
reminderTime
)
} else {
systemTray.showNotification(
habitGroup!!,
getNotificationId(habitGroup),
timestamp,
reminderTime
)
}
}
private fun shouldShowReminderToday(): Boolean {
if (!habit.hasReminder()) return false
val reminder = habit.reminder
if (!hasReminder) return false
val reminder = habit?.reminder ?: habitGroup!!.reminder
val reminderDays = Objects.requireNonNull(reminder)!!.days.toArray()
val weekday = timestamp.weekday
return reminderDays[weekday]