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)) 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 intent = null
} }

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

@ -29,6 +29,7 @@ import android.os.Build
import android.util.Log import android.util.Log
import org.isoron.uhabits.core.AppScope import org.isoron.uhabits.core.AppScope
import org.isoron.uhabits.core.models.Habit 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.SchedulerResult
import org.isoron.uhabits.core.reminders.ReminderScheduler.SystemScheduler import org.isoron.uhabits.core.reminders.ReminderScheduler.SystemScheduler
import org.isoron.uhabits.core.utils.DateFormats import org.isoron.uhabits.core.utils.DateFormats
@ -75,6 +76,16 @@ class IntentScheduler
return schedule(reminderTime, intent, RTC_WAKEUP) 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 { override fun scheduleWidgetUpdate(updateTime: Long): SchedulerResult {
val intent = pendingIntents.updateWidgets() val intent = pendingIntents.updateWidgets()
return schedule(updateTime, intent, RTC) return schedule(updateTime, intent, RTC)
@ -94,4 +105,15 @@ class IntentScheduler
String.format("Setting alarm (%s): %s", time, name) 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.activities.habits.show.ShowHabitActivity
import org.isoron.uhabits.core.AppScope import org.isoron.uhabits.core.AppScope
import org.isoron.uhabits.core.models.Habit 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.models.Timestamp
import org.isoron.uhabits.inject.AppContext import org.isoron.uhabits.inject.AppContext
import org.isoron.uhabits.receivers.ReminderReceiver import org.isoron.uhabits.receivers.ReminderReceiver
@ -69,6 +70,17 @@ class PendingIntentFactory
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT 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 = fun removeRepetition(habit: Habit, timestamp: Timestamp?): PendingIntent =
getBroadcast( getBroadcast(
context, context,
@ -92,6 +104,17 @@ class PendingIntentFactory
) )
.getPendingIntent(0, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT)!! .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 { fun showHabitTemplate(): PendingIntent {
return getActivity( return getActivity(
context, context,
@ -123,6 +146,23 @@ class PendingIntentFactory
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT 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 = fun snoozeNotification(habit: Habit): PendingIntent =
getBroadcast( getBroadcast(
context, context,
@ -134,6 +174,17 @@ class PendingIntentFactory
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT 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 = fun toggleCheckmark(habit: Habit, timestamp: Long?): PendingIntent =
getBroadcast( getBroadcast(
context, context,
@ -185,6 +236,26 @@ class PendingIntentFactory
putExtra("timestamp", timestamp.unixTime) 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 { private fun getIntentTemplateFlags(): Int {
var flags = 0 var flags = 0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 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.R
import org.isoron.uhabits.core.AppScope import org.isoron.uhabits.core.AppScope
import org.isoron.uhabits.core.models.Habit 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.models.Timestamp
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.ui.NotificationTray import org.isoron.uhabits.core.ui.NotificationTray
@ -90,6 +91,34 @@ class AndroidNotificationTray
active.add(notificationId) 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( fun buildNotification(
habit: Habit, habit: Habit,
reminderTime: Long, reminderTime: Long,
@ -163,6 +192,58 @@ class AndroidNotificationTray
return builder.build() 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 { companion object {
private const val REMINDERS_CHANNEL_ID = "REMINDERS" private const val REMINDERS_CHANNEL_ID = "REMINDERS"
fun createAndroidNotificationChannel(context: Context) { fun createAndroidNotificationChannel(context: Context) {

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

@ -23,6 +23,7 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import org.isoron.uhabits.core.AppScope import org.isoron.uhabits.core.AppScope
import org.isoron.uhabits.core.models.Habit 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.models.Timestamp
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.reminders.ReminderScheduler import org.isoron.uhabits.core.reminders.ReminderScheduler
@ -50,21 +51,45 @@ class ReminderController @Inject constructor(
reminderScheduler.scheduleAll() reminderScheduler.scheduleAll()
} }
fun onShowReminder(
habitGroup: HabitGroup,
timestamp: Timestamp,
reminderTime: Long
) {
notificationTray.show(habitGroup, timestamp, reminderTime)
reminderScheduler.scheduleAll()
}
fun onSnoozePressed(habit: Habit, context: Context) { fun onSnoozePressed(habit: Habit, context: Context) {
showSnoozeDelayPicker(habit, context) showSnoozeDelayPicker(habit, context)
} }
fun onSnoozePressed(habitGroup: HabitGroup, context: Context) {
showSnoozeDelayPicker(habitGroup, context)
}
fun onSnoozeDelayPicked(habit: Habit, delayInMinutes: Int) { fun onSnoozeDelayPicked(habit: Habit, delayInMinutes: Int) {
reminderScheduler.snoozeReminder(habit, delayInMinutes.toLong()) reminderScheduler.snoozeReminder(habit, delayInMinutes.toLong())
notificationTray.cancel(habit) 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) { fun onSnoozeTimePicked(habit: Habit?, hour: Int, minute: Int) {
val time: Long = getUpcomingTimeInMillis(hour, minute) val time: Long = getUpcomingTimeInMillis(hour, minute)
reminderScheduler.scheduleAtTime(habit!!, time) reminderScheduler.scheduleAtTime(habit!!, time)
notificationTray.cancel(habit) 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) { fun onDismiss(habit: Habit) {
if (preferences.shouldMakeNotificationsSticky()) { if (preferences.shouldMakeNotificationsSticky()) {
// This is a workaround to keep sticky notifications non-dismissible in Android 14+. // 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) { private fun showSnoozeDelayPicker(habit: Habit, context: Context) {
context.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) context.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
val intent = Intent(context, SnoozeDelayPickerActivity::class.java) val intent = Intent(context, SnoozeDelayPickerActivity::class.java)
@ -82,4 +111,12 @@ class ReminderController @Inject constructor(
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent) 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 android.util.Log
import org.isoron.uhabits.HabitsApplication import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.core.models.Habit 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.models.Timestamp
import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayWithOffset import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayWithOffset
@ -44,52 +45,81 @@ class ReminderReceiver : BroadcastReceiver() {
val app = context.applicationContext as HabitsApplication val app = context.applicationContext as HabitsApplication
val appComponent = app.component val appComponent = app.component
val habits = appComponent.habitList val habits = appComponent.habitList
val habitGroups = appComponent.habitGroupList
val reminderController = appComponent.reminderController val reminderController = appComponent.reminderController
Log.i(TAG, String.format("Received intent: %s", intent.toString())) Log.i(TAG, String.format("Received intent: %s", intent.toString()))
var habit: Habit? = null var habit: Habit? = null
var habitGroup: HabitGroup? = null
var id: Long? = null
var type: String? = null
val today: Long = getStartOfTodayWithOffset() val today: Long = getStartOfTodayWithOffset()
val data = intent.data 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 timestamp = intent.getLongExtra("timestamp", today)
val reminderTime = intent.getLongExtra("reminderTime", today) val reminderTime = intent.getLongExtra("reminderTime", today)
try { try {
when (intent.action) { when (intent.action) {
ACTION_SHOW_REMINDER -> { ACTION_SHOW_REMINDER -> {
if (habit == null) return if (id == null) return
Log.d( Log.d(
"ReminderReceiver", "ReminderReceiver",
String.format( String.format(
"onShowReminder habit=%d timestamp=%d reminderTime=%d", "onShowReminder %s=%d timestamp=%d reminderTime=%d",
habit.id, type,
id,
timestamp, timestamp,
reminderTime reminderTime
) )
) )
reminderController.onShowReminder( if (habit != null) {
habit, reminderController.onShowReminder(
Timestamp(timestamp), habit,
reminderTime Timestamp(timestamp),
) reminderTime
)
} else {
reminderController.onShowReminder(
habitGroup!!,
Timestamp(timestamp),
reminderTime
)
}
} }
ACTION_DISMISS_REMINDER -> { ACTION_DISMISS_REMINDER -> {
if (habit == null) return if (id == null) return
Log.d("ReminderReceiver", String.format("onDismiss habit=%d", habit.id)) Log.d("ReminderReceiver", String.format("onDismiss %s=%d", type, id))
reminderController.onDismiss(habit) if (habit != null) {
reminderController.onDismiss(habit)
} else {
reminderController.onDismiss(habitGroup!!)
}
} }
ACTION_SNOOZE_REMINDER -> { ACTION_SNOOZE_REMINDER -> {
if (habit == null) return if (id == null) return
if (SDK_INT < Build.VERSION_CODES.S) { if (SDK_INT < Build.VERSION_CODES.S) {
Log.d( Log.d(
"ReminderReceiver", "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 { } else {
Log.w( Log.w(
"ReminderReceiver", "ReminderReceiver",
String.format( String.format(
"onSnoozePressed habit=%d, should be deactivated in recent versions.", "onSnoozePressed %s=%d, should be deactivated in recent versions.",
habit.id type,
id
) )
) )
} }

@ -43,7 +43,7 @@ data class HabitGroup(
var observable = ModelObservable() var observable = ModelObservable()
val uriString: String val uriString: String
get() = "content://org.isoron.uhabits/habit/$id" get() = "content://org.isoron.uhabits/habitgroup/$id"
fun hasReminder(): Boolean = reminder != null 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.CommandRunner
import org.isoron.uhabits.core.commands.CreateRepetitionCommand import org.isoron.uhabits.core.commands.CreateRepetitionCommand
import org.isoron.uhabits.core.models.Habit 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.HabitList
import org.isoron.uhabits.core.models.HabitMatcher import org.isoron.uhabits.core.models.HabitMatcher
import org.isoron.uhabits.core.preferences.WidgetPreferences import org.isoron.uhabits.core.preferences.WidgetPreferences
@ -39,6 +41,7 @@ import javax.inject.Inject
class ReminderScheduler @Inject constructor( class ReminderScheduler @Inject constructor(
private val commandRunner: CommandRunner, private val commandRunner: CommandRunner,
private val habitList: HabitList, private val habitList: HabitList,
private val habitGroupList: HabitGroupList,
private val sys: SystemScheduler, private val sys: SystemScheduler,
private val widgetPreferences: WidgetPreferences private val widgetPreferences: WidgetPreferences
) : CommandRunner.Listener { ) : CommandRunner.Listener {
@ -83,6 +86,40 @@ class ReminderScheduler @Inject constructor(
scheduleAtTime(habit, reminderTime) 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 @Synchronized
fun scheduleAtTime(habit: Habit, reminderTime: Long) { fun scheduleAtTime(habit: Habit, reminderTime: Long) {
sys.log("ReminderScheduler", "Scheduling alarm for habit=" + habit.id) sys.log("ReminderScheduler", "Scheduling alarm for habit=" + habit.id)
@ -108,16 +145,48 @@ class ReminderScheduler @Inject constructor(
sys.scheduleShowReminder(reminderTime, habit, timestamp) 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 @Synchronized
fun scheduleAll() { fun scheduleAll() {
sys.log("ReminderScheduler", "Scheduling all alarms") sys.log("ReminderScheduler", "Scheduling all alarms")
val reminderHabits = habitList.getFiltered(HabitMatcher.WITH_ALARM) 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 reminderHabits) schedule(habit)
for (habit in reminderSubHabits) schedule(habit)
for (hgr in reminderHabitGroups) schedule(hgr)
} }
@Synchronized @Synchronized
fun hasHabitsWithReminders(): Boolean { 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 @Synchronized
@ -138,6 +207,14 @@ class ReminderScheduler @Inject constructor(
schedule(habit) 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 { interface SystemScheduler {
fun scheduleShowReminder( fun scheduleShowReminder(
reminderTime: Long, reminderTime: Long,
@ -145,6 +222,12 @@ class ReminderScheduler @Inject constructor(
timestamp: Long timestamp: Long
): SchedulerResult ): SchedulerResult
fun scheduleShowReminder(
reminderTime: Long,
habitGroup: HabitGroup,
timestamp: Long
): SchedulerResult
fun scheduleWidgetUpdate(updateTime: Long): SchedulerResult? fun scheduleWidgetUpdate(updateTime: Long): SchedulerResult?
fun log(componentName: String, msg: String) 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.Command
import org.isoron.uhabits.core.commands.CommandRunner import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.commands.CreateRepetitionCommand 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.commands.DeleteHabitsCommand
import org.isoron.uhabits.core.models.Habit 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.models.Timestamp
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.tasks.Task import org.isoron.uhabits.core.tasks.Task
import org.isoron.uhabits.core.tasks.TaskRunner import org.isoron.uhabits.core.tasks.TaskRunner
import java.util.HashMap
import java.util.Locale import java.util.Locale
import java.util.Objects import java.util.Objects
import javax.inject.Inject import javax.inject.Inject
@ -40,11 +41,18 @@ class NotificationTray @Inject constructor(
private val preferences: Preferences, private val preferences: Preferences,
private val systemTray: SystemTray private val systemTray: SystemTray
) : CommandRunner.Listener, Preferences.Listener { ) : 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) { fun cancel(habit: Habit) {
val notificationId = getNotificationId(habit) val notificationId = getNotificationId(habit)
systemTray.removeNotification(notificationId) 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) { override fun onCommandFinished(command: Command) {
@ -56,6 +64,13 @@ class NotificationTray @Inject constructor(
val (_, deleted) = command val (_, deleted) = command
for (habit in deleted) cancel(habit) 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() { override fun onNotificationsChanged() {
@ -64,10 +79,16 @@ class NotificationTray @Inject constructor(
fun show(habit: Habit, timestamp: Timestamp, reminderTime: Long) { fun show(habit: Habit, timestamp: Timestamp, reminderTime: Long) {
val data = NotificationData(timestamp, reminderTime) val data = NotificationData(timestamp, reminderTime)
active[habit] = data activeHabits[habit] = data
taskRunner.execute(ShowNotificationTask(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() { fun startListening() {
commandRunner.addListener(this) commandRunner.addListener(this)
preferences.addListener(this) preferences.addListener(this)
@ -83,14 +104,22 @@ class NotificationTray @Inject constructor(
return (id % Int.MAX_VALUE).toInt() 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() { private fun reshowAll() {
for ((habit, data) in active.entries) { for ((habit, data) in activeHabits.entries) {
taskRunner.execute(ShowNotificationTask(habit, data)) taskRunner.execute(ShowNotificationTask(habit, data))
} }
for ((habitGroup, data) in activeHabitGroups.entries) {
taskRunner.execute(ShowNotificationTask(habitGroup, data))
}
} }
fun reshow(habit: Habit) { fun reshow(habit: Habit) {
active[habit]?.let { activeHabits[habit]?.let {
taskRunner.execute(ShowNotificationTask(habit, it)) taskRunner.execute(ShowNotificationTask(habit, it))
} }
} }
@ -104,48 +133,79 @@ class NotificationTray @Inject constructor(
reminderTime: Long reminderTime: Long
) )
fun showNotification(
habitGroup: HabitGroup,
notificationId: Int,
timestamp: Timestamp,
reminderTime: Long
)
fun log(msg: String) fun log(msg: String)
} }
internal class NotificationData(val timestamp: Timestamp, val reminderTime: Long) internal class NotificationData(val timestamp: Timestamp, val reminderTime: Long)
private inner class ShowNotificationTask(private val habit: Habit, data: NotificationData) : private inner class ShowNotificationTask private constructor(
Task { 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 var isCompleted = false
private val timestamp: Timestamp = data.timestamp private val timestamp: Timestamp = data.timestamp
private val reminderTime: Long = data.reminderTime 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() { override fun doInBackground() {
isCompleted = habit.isCompletedToday() isCompleted = habit?.isCompletedToday() ?: habitGroup!!.isCompletedToday()
} }
override fun onPostExecute() { 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) { if (isCompleted) {
systemTray.log( systemTray.log(
String.format( String.format(
Locale.US, Locale.US,
"Habit %d already checked. Skipping.", "%s %d already checked. Skipping.",
habit.id type,
id
) )
) )
return return
} }
if (!habit.hasReminder()) { if (!hasReminder) {
systemTray.log( systemTray.log(
String.format( String.format(
Locale.US, Locale.US,
"Habit %d does not have a reminder. Skipping.", "%s %d does not have a reminder. Skipping.",
habit.id type,
id
) )
) )
return return
} }
if (habit.isArchived) { if (isArchived) {
systemTray.log( systemTray.log(
String.format( String.format(
Locale.US, Locale.US,
"Habit %d is archived. Skipping.", "%s %d is archived. Skipping.",
habit.id type,
id
) )
) )
return return
@ -154,23 +214,33 @@ class NotificationTray @Inject constructor(
systemTray.log( systemTray.log(
String.format( String.format(
Locale.US, Locale.US,
"Habit %d not supposed to run today. Skipping.", "%s %d not supposed to run today. Skipping.",
habit.id type,
id
) )
) )
return return
} }
systemTray.showNotification( if (habit != null) {
habit, systemTray.showNotification(
getNotificationId(habit), habit,
timestamp, getNotificationId(habit),
reminderTime timestamp,
) reminderTime
)
} else {
systemTray.showNotification(
habitGroup!!,
getNotificationId(habitGroup),
timestamp,
reminderTime
)
}
} }
private fun shouldShowReminderToday(): Boolean { private fun shouldShowReminderToday(): Boolean {
if (!habit.hasReminder()) return false if (!hasReminder) return false
val reminder = habit.reminder val reminder = habit?.reminder ?: habitGroup!!.reminder
val reminderDays = Objects.requireNonNull(reminder)!!.days.toArray() val reminderDays = Objects.requireNonNull(reminder)!!.days.toArray()
val weekday = timestamp.weekday val weekday = timestamp.weekday
return reminderDays[weekday] return reminderDays[weekday]

Loading…
Cancel
Save