Implement notifications for both habit groups and sub habits

pull/2020/head
Dharanish 1 year ago
parent 32c69772ae
commit 8fac8afadf

@ -181,6 +181,16 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
component.listHabitsBehavior.onEdit(habit, Timestamp(timestamp))
}
}
intent.getLongExtra("CLEAR_NOTIFICATION_HABIT_ID", -1).takeIf { it != -1L }?.let { id ->
val dismissHabit = appComponent.habitList.getById(id) ?: appComponent.habitGroupList.getHabitByID(id)
if (dismissHabit != null) {
appComponent.reminderController.onDismiss(dismissHabit)
} else {
val dismissHabitGroup = appComponent.habitGroupList.getById(id)!!
appComponent.reminderController.onDismiss(dismissHabitGroup)
}
}
intent = null
}

@ -63,9 +63,10 @@ class HabitsModule(dbFile: File) {
sys: IntentScheduler,
commandRunner: CommandRunner,
habitList: HabitList,
habitGroupList: HabitGroupList,
widgetPreferences: WidgetPreferences
): ReminderScheduler {
return ReminderScheduler(commandRunner, habitList, sys, widgetPreferences)
return ReminderScheduler(commandRunner, habitList, habitGroupList, sys, widgetPreferences)
}
@Provides

@ -29,6 +29,7 @@ import android.os.Build
import android.util.Log
import org.isoron.uhabits.core.AppScope
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitGroup
import org.isoron.uhabits.core.reminders.ReminderScheduler.SchedulerResult
import org.isoron.uhabits.core.reminders.ReminderScheduler.SystemScheduler
import org.isoron.uhabits.core.utils.DateFormats
@ -75,6 +76,16 @@ class IntentScheduler
return schedule(reminderTime, intent, RTC_WAKEUP)
}
override fun scheduleShowReminder(
reminderTime: Long,
habitGroup: HabitGroup,
timestamp: Long
): SchedulerResult {
val intent = pendingIntents.showReminder(habitGroup, reminderTime, timestamp)
logReminderScheduled(habitGroup, reminderTime)
return schedule(reminderTime, intent, RTC_WAKEUP)
}
override fun scheduleWidgetUpdate(updateTime: Long): SchedulerResult {
val intent = pendingIntents.updateWidgets()
return schedule(updateTime, intent, RTC)
@ -94,4 +105,15 @@ class IntentScheduler
String.format("Setting alarm (%s): %s", time, name)
)
}
private fun logReminderScheduled(habitGroup: HabitGroup, reminderTime: Long) {
val min = min(5, habitGroup.name.length)
val name = habitGroup.name.substring(0, min)
val df = DateFormats.getBackupDateFormat()
val time = df.format(Date(reminderTime))
Log.i(
"ReminderHelper",
String.format("Setting alarm (%s): %s", time, name)
)
}
}

@ -33,6 +33,7 @@ import org.isoron.uhabits.activities.habits.list.ListHabitsActivity
import org.isoron.uhabits.activities.habits.show.ShowHabitActivity
import org.isoron.uhabits.core.AppScope
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.inject.AppContext
import org.isoron.uhabits.receivers.ReminderReceiver
@ -69,6 +70,17 @@ class PendingIntentFactory
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
)
fun dismissNotification(habitGroup: HabitGroup): PendingIntent =
getBroadcast(
context,
0,
Intent(context, ReminderReceiver::class.java).apply {
action = WidgetReceiver.ACTION_DISMISS_REMINDER
data = Uri.parse(habitGroup.uriString)
},
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
)
fun removeRepetition(habit: Habit, timestamp: Timestamp?): PendingIntent =
getBroadcast(
context,
@ -92,6 +104,17 @@ class PendingIntentFactory
)
.getPendingIntent(0, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT)!!
fun showHabitGroup(habitGroup: HabitGroup): PendingIntent =
androidx.core.app.TaskStackBuilder
.create(context)
.addNextIntentWithParentStack(
intentFactory.startShowHabitGroupActivity(
context,
habitGroup
)
)
.getPendingIntent(0, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT)!!
fun showHabitTemplate(): PendingIntent {
return getActivity(
context,
@ -123,6 +146,23 @@ class PendingIntentFactory
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
)
fun showReminder(
habitGroup: HabitGroup,
reminderTime: Long?,
timestamp: Long
): PendingIntent =
getBroadcast(
context,
(habitGroup.id!! % Integer.MAX_VALUE).toInt() + 1,
Intent(context, ReminderReceiver::class.java).apply {
action = ReminderReceiver.ACTION_SHOW_REMINDER
data = Uri.parse(habitGroup.uriString)
putExtra("timestamp", timestamp)
putExtra("reminderTime", reminderTime)
},
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
)
fun snoozeNotification(habit: Habit): PendingIntent =
getBroadcast(
context,
@ -134,6 +174,17 @@ class PendingIntentFactory
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
)
fun snoozeNotification(habitGroup: HabitGroup): PendingIntent =
getBroadcast(
context,
0,
Intent(context, ReminderReceiver::class.java).apply {
data = Uri.parse(habitGroup.uriString)
action = ReminderReceiver.ACTION_SNOOZE_REMINDER
},
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
)
fun toggleCheckmark(habit: Habit, timestamp: Long?): PendingIntent =
getBroadcast(
context,
@ -185,6 +236,26 @@ class PendingIntentFactory
putExtra("timestamp", timestamp.unixTime)
}
fun showHabitList(): PendingIntent {
return getActivity(
context,
1,
Intent(context, ListHabitsActivity::class.java),
getIntentTemplateFlags()
)
}
fun showHabitListWithNotificationClear(id: Long): PendingIntent {
return getActivity(
context,
0,
Intent(context, ListHabitsActivity::class.java).apply {
putExtra("CLEAR_NOTIFICATION_HABIT_ID", id)
},
getIntentTemplateFlags()
)
}
private fun getIntentTemplateFlags(): Int {
var flags = 0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {

@ -35,6 +35,7 @@ import androidx.core.app.NotificationManagerCompat
import org.isoron.uhabits.R
import org.isoron.uhabits.core.AppScope
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.ui.NotificationTray
@ -90,6 +91,34 @@ class AndroidNotificationTray
active.add(notificationId)
}
override fun showNotification(
habitGroup: HabitGroup,
notificationId: Int,
timestamp: Timestamp,
reminderTime: Long
) {
val notificationManager = NotificationManagerCompat.from(context)
val notification = buildNotification(habitGroup, reminderTime, timestamp)
createAndroidNotificationChannel(context)
try {
notificationManager.notify(notificationId, notification)
} catch (e: RuntimeException) {
// Some Xiaomi phones produce a RuntimeException if custom notification sounds are used.
Log.i(
"AndroidNotificationTray",
"Failed to show notification. Retrying without sound."
)
val n = buildNotification(
habitGroup,
reminderTime,
timestamp,
disableSound = true
)
notificationManager.notify(notificationId, n)
}
active.add(notificationId)
}
fun buildNotification(
habit: Habit,
reminderTime: Long,
@ -163,6 +192,58 @@ class AndroidNotificationTray
return builder.build()
}
fun buildNotification(
habitGroup: HabitGroup,
reminderTime: Long,
timestamp: Timestamp,
disableSound: Boolean = false
): Notification {
val enterAction = Action(
R.drawable.ic_action_check,
context.getString(R.string.enter),
pendingIntents.showHabitListWithNotificationClear(habitGroup.id!!)
)
val wearableBg = decodeResource(context.resources, R.drawable.stripe)
// Even though the set of actions is the same on the phone and
// on the watch, Pebble requires us to add them to the
// WearableExtender.
val wearableExtender = WearableExtender().setBackground(wearableBg)
val defaultText = context.getString(R.string.default_reminder_question)
val builder = Builder(context, REMINDERS_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(habitGroup.name)
.setContentText(if (habitGroup.question.isBlank()) defaultText else habitGroup.question)
.setContentIntent(pendingIntents.showHabitGroup(habitGroup))
.setDeleteIntent(pendingIntents.dismissNotification(habitGroup))
.setSound(null)
.setWhen(reminderTime)
.setShowWhen(true)
.setOngoing(preferences.shouldMakeNotificationsSticky())
wearableExtender.addAction(enterAction)
builder.addAction(enterAction)
if (!disableSound) {
builder.setSound(ringtoneManager.getURI())
}
if (SDK_INT < Build.VERSION_CODES.S) {
val snoozeAction = Action(
R.drawable.ic_action_snooze,
context.getString(R.string.snooze),
pendingIntents.snoozeNotification(habitGroup)
)
wearableExtender.addAction(snoozeAction)
builder.addAction(snoozeAction)
}
builder.extend(wearableExtender)
return builder.build()
}
companion object {
private const val REMINDERS_CHANNEL_ID = "REMINDERS"
fun createAndroidNotificationChannel(context: Context) {

@ -33,6 +33,7 @@ import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.AndroidThemeSwitcher
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitGroup
import org.isoron.uhabits.core.ui.views.DarkTheme
import org.isoron.uhabits.core.ui.views.LightTheme
import org.isoron.uhabits.receivers.ReminderController
@ -41,6 +42,7 @@ import java.util.Calendar
class SnoozeDelayPickerActivity : FragmentActivity(), OnItemClickListener {
private var habit: Habit? = null
private var habitGroup: HabitGroup? = null
private var reminderController: ReminderController? = null
private var dialog: AlertDialog? = null
private var androidColor: Int = 0
@ -58,10 +60,13 @@ class SnoozeDelayPickerActivity : FragmentActivity(), OnItemClickListener {
if (data == null) {
finish()
} else {
habit = appComponent.habitList.getById(ContentUris.parseId(data))
val id = ContentUris.parseId(data)
habit = appComponent.habitList.getById(id) ?: appComponent.habitGroupList.getHabitByID(id)
habitGroup = appComponent.habitGroupList.getById(id)
}
if (habit == null) finish()
androidColor = themeSwitcher.currentTheme.color(habit!!.color).toInt()
if (habit == null && habitGroup == null) finish()
val color = habit?.color ?: habitGroup!!.color
androidColor = themeSwitcher.currentTheme.color(color).toInt()
reminderController = appComponent.reminderController
dialog = AlertDialog.Builder(this)
.setTitle(R.string.select_snooze_delay)
@ -87,7 +92,11 @@ class SnoozeDelayPickerActivity : FragmentActivity(), OnItemClickListener {
val calendar = Calendar.getInstance()
val dialog = TimePickerDialog.newInstance(
{ view: RadialPickerLayout?, hour: Int, minute: Int ->
reminderController!!.onSnoozeTimePicked(habit, hour, minute)
if (habit != null) {
reminderController!!.onSnoozeTimePicked(habit, hour, minute)
} else {
reminderController!!.onSnoozeTimePicked(habitGroup, hour, minute)
}
finish()
},
calendar[Calendar.HOUR_OF_DAY],
@ -101,7 +110,11 @@ class SnoozeDelayPickerActivity : FragmentActivity(), OnItemClickListener {
override fun onItemClick(parent: AdapterView<*>?, view: View, position: Int, id: Long) {
val snoozeValues = resources.getIntArray(R.array.snooze_picker_values)
if (snoozeValues[position] >= 0) {
reminderController!!.onSnoozeDelayPicked(habit!!, snoozeValues[position])
if (habit != null) {
reminderController!!.onSnoozeDelayPicked(habit!!, snoozeValues[position])
} else {
reminderController!!.onSnoozeDelayPicked(habitGroup!!, snoozeValues[position])
}
finish()
} else {
showTimePicker()

@ -23,6 +23,7 @@ import android.content.Intent
import android.net.Uri
import org.isoron.uhabits.core.AppScope
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.reminders.ReminderScheduler
@ -50,21 +51,45 @@ class ReminderController @Inject constructor(
reminderScheduler.scheduleAll()
}
fun onShowReminder(
habitGroup: HabitGroup,
timestamp: Timestamp,
reminderTime: Long
) {
notificationTray.show(habitGroup, timestamp, reminderTime)
reminderScheduler.scheduleAll()
}
fun onSnoozePressed(habit: Habit, context: Context) {
showSnoozeDelayPicker(habit, context)
}
fun onSnoozePressed(habitGroup: HabitGroup, context: Context) {
showSnoozeDelayPicker(habitGroup, context)
}
fun onSnoozeDelayPicked(habit: Habit, delayInMinutes: Int) {
reminderScheduler.snoozeReminder(habit, delayInMinutes.toLong())
notificationTray.cancel(habit)
}
fun onSnoozeDelayPicked(habitGroup: HabitGroup, delayInMinutes: Int) {
reminderScheduler.snoozeReminder(habitGroup, delayInMinutes.toLong())
notificationTray.cancel(habitGroup)
}
fun onSnoozeTimePicked(habit: Habit?, hour: Int, minute: Int) {
val time: Long = getUpcomingTimeInMillis(hour, minute)
reminderScheduler.scheduleAtTime(habit!!, time)
notificationTray.cancel(habit)
}
fun onSnoozeTimePicked(habitGroup: HabitGroup?, hour: Int, minute: Int) {
val time: Long = getUpcomingTimeInMillis(hour, minute)
reminderScheduler.scheduleAtTime(habitGroup!!, time)
notificationTray.cancel(habitGroup)
}
fun onDismiss(habit: Habit) {
if (preferences.shouldMakeNotificationsSticky()) {
// This is a workaround to keep sticky notifications non-dismissible in Android 14+.
@ -75,6 +100,10 @@ class ReminderController @Inject constructor(
}
}
fun onDismiss(habitGroup: HabitGroup) {
notificationTray.cancel(habitGroup)
}
private fun showSnoozeDelayPicker(habit: Habit, context: Context) {
context.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
val intent = Intent(context, SnoozeDelayPickerActivity::class.java)
@ -82,4 +111,12 @@ class ReminderController @Inject constructor(
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
private fun showSnoozeDelayPicker(habitGroup: HabitGroup, context: Context) {
context.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
val intent = Intent(context, SnoozeDelayPickerActivity::class.java)
intent.data = Uri.parse(habitGroup.uriString)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
}

@ -27,6 +27,7 @@ import android.os.Build.VERSION.SDK_INT
import android.util.Log
import org.isoron.uhabits.HabitsApplication
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.utils.DateUtils.Companion.getStartOfTodayWithOffset
@ -44,52 +45,81 @@ class ReminderReceiver : BroadcastReceiver() {
val app = context.applicationContext as HabitsApplication
val appComponent = app.component
val habits = appComponent.habitList
val habitGroups = appComponent.habitGroupList
val reminderController = appComponent.reminderController
Log.i(TAG, String.format("Received intent: %s", intent.toString()))
var habit: Habit? = null
var habitGroup: HabitGroup? = null
var id: Long? = null
var type: String? = null
val today: Long = getStartOfTodayWithOffset()
val data = intent.data
if (data != null) habit = habits.getById(ContentUris.parseId(data))
if (data != null) {
type = data.pathSegments[0]
id = ContentUris.parseId(data)
when (type) {
"habit" -> habit = habits.getById(id) ?: habitGroups.getHabitByID(id)
"habitgroup" -> habitGroup = habitGroups.getById(id)
}
}
val timestamp = intent.getLongExtra("timestamp", today)
val reminderTime = intent.getLongExtra("reminderTime", today)
try {
when (intent.action) {
ACTION_SHOW_REMINDER -> {
if (habit == null) return
if (id == null) return
Log.d(
"ReminderReceiver",
String.format(
"onShowReminder habit=%d timestamp=%d reminderTime=%d",
habit.id,
"onShowReminder %s=%d timestamp=%d reminderTime=%d",
type,
id,
timestamp,
reminderTime
)
)
reminderController.onShowReminder(
habit,
Timestamp(timestamp),
reminderTime
)
if (habit != null) {
reminderController.onShowReminder(
habit,
Timestamp(timestamp),
reminderTime
)
} else {
reminderController.onShowReminder(
habitGroup!!,
Timestamp(timestamp),
reminderTime
)
}
}
ACTION_DISMISS_REMINDER -> {
if (habit == null) return
Log.d("ReminderReceiver", String.format("onDismiss habit=%d", habit.id))
reminderController.onDismiss(habit)
if (id == null) return
Log.d("ReminderReceiver", String.format("onDismiss %s=%d", type, id))
if (habit != null) {
reminderController.onDismiss(habit)
} else {
reminderController.onDismiss(habitGroup!!)
}
}
ACTION_SNOOZE_REMINDER -> {
if (habit == null) return
if (id == null) return
if (SDK_INT < Build.VERSION_CODES.S) {
Log.d(
"ReminderReceiver",
String.format("onSnoozePressed habit=%d", habit.id)
String.format("onSnoozePressed %s=%d", type, id)
)
reminderController.onSnoozePressed(habit, context)
if (habit != null) {
reminderController.onSnoozePressed(habit, context)
} else {
reminderController.onSnoozePressed(habitGroup!!, context)
}
} else {
Log.w(
"ReminderReceiver",
String.format(
"onSnoozePressed habit=%d, should be deactivated in recent versions.",
habit.id
"onSnoozePressed %s=%d, should be deactivated in recent versions.",
type,
id
)
)
}

@ -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]

Loading…
Cancel
Save