mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-15 21:48:50 -06:00
Implement notifications for both habit groups and sub habits
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user