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