mirror of https://github.com/tasks/tasks
Convert NotificationManager to Kotlin
parent
00bfd053c6
commit
8a30fde2f2
@ -1,441 +0,0 @@
|
|||||||
package org.tasks.notifications;
|
|
||||||
|
|
||||||
import static androidx.core.app.NotificationCompat.FLAG_INSISTENT;
|
|
||||||
import static androidx.core.app.NotificationCompat.FLAG_NO_CLEAR;
|
|
||||||
import static com.google.common.collect.Iterables.concat;
|
|
||||||
import static com.google.common.collect.Iterables.tryFind;
|
|
||||||
import static com.google.common.collect.Lists.newArrayList;
|
|
||||||
import static com.google.common.collect.Lists.transform;
|
|
||||||
import static com.todoroo.andlib.utility.AndroidUtilities.atLeastNougat;
|
|
||||||
import static com.todoroo.andlib.utility.AndroidUtilities.preOreo;
|
|
||||||
import static com.todoroo.astrid.reminders.ReminderService.TYPE_GEOFENCE_ENTER;
|
|
||||||
import static com.todoroo.astrid.reminders.ReminderService.TYPE_GEOFENCE_EXIT;
|
|
||||||
import static java.util.Collections.emptyList;
|
|
||||||
import static java.util.Collections.singletonList;
|
|
||||||
import static org.tasks.Strings.isNullOrEmpty;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import androidx.core.app.NotificationCompat;
|
|
||||||
import androidx.core.app.NotificationManagerCompat;
|
|
||||||
import com.google.common.base.Joiner;
|
|
||||||
import com.todoroo.andlib.sql.Join;
|
|
||||||
import com.todoroo.andlib.sql.QueryTemplate;
|
|
||||||
import com.todoroo.andlib.utility.DateUtilities;
|
|
||||||
import com.todoroo.astrid.api.Filter;
|
|
||||||
import com.todoroo.astrid.dao.TaskDaoBlocking;
|
|
||||||
import com.todoroo.astrid.data.Task;
|
|
||||||
import com.todoroo.astrid.reminders.ReminderService;
|
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext;
|
|
||||||
import io.reactivex.Completable;
|
|
||||||
import io.reactivex.Single;
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
|
||||||
import io.reactivex.schedulers.Schedulers;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Singleton;
|
|
||||||
import org.tasks.LocalBroadcastManager;
|
|
||||||
import org.tasks.R;
|
|
||||||
import org.tasks.data.LocationDaoBlocking;
|
|
||||||
import org.tasks.data.Place;
|
|
||||||
import org.tasks.intents.TaskIntents;
|
|
||||||
import org.tasks.preferences.Preferences;
|
|
||||||
import org.tasks.receivers.CompleteTaskReceiver;
|
|
||||||
import org.tasks.reminders.NotificationActivity;
|
|
||||||
import org.tasks.reminders.SnoozeActivity;
|
|
||||||
import org.tasks.reminders.SnoozeDialog;
|
|
||||||
import org.tasks.reminders.SnoozeOption;
|
|
||||||
import org.tasks.themes.ColorProvider;
|
|
||||||
import org.tasks.time.DateTime;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
public class NotificationManager {
|
|
||||||
|
|
||||||
public static final String NOTIFICATION_CHANNEL_DEFAULT = "notifications";
|
|
||||||
public static final String NOTIFICATION_CHANNEL_TASKER = "notifications_tasker";
|
|
||||||
public static final String NOTIFICATION_CHANNEL_TIMERS = "notifications_timers";
|
|
||||||
public static final String NOTIFICATION_CHANNEL_MISCELLANEOUS = "notifications_miscellaneous";
|
|
||||||
public static final int MAX_NOTIFICATIONS = 21;
|
|
||||||
static final String EXTRA_NOTIFICATION_ID = "extra_notification_id";
|
|
||||||
static final int SUMMARY_NOTIFICATION_ID = 0;
|
|
||||||
private static final String GROUP_KEY = "tasks";
|
|
||||||
private static final int NOTIFICATIONS_PER_SECOND = 4;
|
|
||||||
private final NotificationManagerCompat notificationManagerCompat;
|
|
||||||
private final ColorProvider colorProvider;
|
|
||||||
private final LocalBroadcastManager localBroadcastManager;
|
|
||||||
private final LocationDaoBlocking locationDao;
|
|
||||||
private final NotificationDaoBlocking notificationDao;
|
|
||||||
private final TaskDaoBlocking taskDao;
|
|
||||||
private final Context context;
|
|
||||||
private final Preferences preferences;
|
|
||||||
private final Throttle throttle = new Throttle(NOTIFICATIONS_PER_SECOND);
|
|
||||||
private final NotificationLimiter queue = new NotificationLimiter(MAX_NOTIFICATIONS);
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public NotificationManager(
|
|
||||||
@ApplicationContext Context context,
|
|
||||||
Preferences preferences,
|
|
||||||
NotificationDaoBlocking notificationDao,
|
|
||||||
TaskDaoBlocking taskDao,
|
|
||||||
LocationDaoBlocking locationDao,
|
|
||||||
LocalBroadcastManager localBroadcastManager) {
|
|
||||||
this.context = context;
|
|
||||||
this.preferences = preferences;
|
|
||||||
this.notificationDao = notificationDao;
|
|
||||||
this.taskDao = taskDao;
|
|
||||||
this.locationDao = locationDao;
|
|
||||||
this.localBroadcastManager = localBroadcastManager;
|
|
||||||
this.colorProvider = new ColorProvider(context, preferences);
|
|
||||||
notificationManagerCompat = NotificationManagerCompat.from(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("CheckResult")
|
|
||||||
public void cancel(long id) {
|
|
||||||
if (id == SUMMARY_NOTIFICATION_ID) {
|
|
||||||
//noinspection ResultOfMethodCallIgnored
|
|
||||||
Single.fromCallable(() -> concat(notificationDao.getAll(), singletonList(id)))
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(this::cancel);
|
|
||||||
} else {
|
|
||||||
cancel(singletonList(id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("CheckResult")
|
|
||||||
public void cancel(Iterable<Long> ids) {
|
|
||||||
for (Long id : ids) {
|
|
||||||
notificationManagerCompat.cancel(id.intValue());
|
|
||||||
queue.remove(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
//noinspection ResultOfMethodCallIgnored
|
|
||||||
Completable.fromAction(() -> notificationDao.deleteAll(newArrayList(ids)))
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.subscribe(() -> notifyTasks(emptyList(), false, false, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void restoreNotifications(boolean cancelExisting) {
|
|
||||||
List<Notification> notifications = notificationDao.getAllOrdered();
|
|
||||||
if (cancelExisting) {
|
|
||||||
for (Notification notification : notifications) {
|
|
||||||
notificationManagerCompat.cancel((int) notification.getTaskId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preferences.bundleNotifications() && notifications.size() > 1) {
|
|
||||||
updateSummary(false, false, false, Collections.emptyList());
|
|
||||||
createNotifications(notifications, false, false, false, true);
|
|
||||||
} else {
|
|
||||||
createNotifications(notifications, false, false, false, false);
|
|
||||||
cancelSummaryNotification();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void notifyTasks(
|
|
||||||
List<Notification> newNotifications, boolean alert, boolean nonstop, boolean fiveTimes) {
|
|
||||||
List<Notification> existingNotifications = notificationDao.getAllOrdered();
|
|
||||||
notificationDao.insertAll(newNotifications);
|
|
||||||
int totalCount = existingNotifications.size() + newNotifications.size();
|
|
||||||
if (totalCount == 0) {
|
|
||||||
cancelSummaryNotification();
|
|
||||||
} else if (totalCount == 1) {
|
|
||||||
List<Notification> notifications =
|
|
||||||
newArrayList(concat(existingNotifications, newNotifications));
|
|
||||||
createNotifications(notifications, alert, nonstop, fiveTimes, false);
|
|
||||||
cancelSummaryNotification();
|
|
||||||
} else if (preferences.bundleNotifications()) {
|
|
||||||
updateSummary(false, false, false, Collections.emptyList());
|
|
||||||
|
|
||||||
if (existingNotifications.size() == 1) {
|
|
||||||
createNotifications(existingNotifications, false, false, false, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (atLeastNougat() && newNotifications.size() == 1) {
|
|
||||||
createNotifications(newNotifications, alert, nonstop, fiveTimes, true);
|
|
||||||
} else {
|
|
||||||
createNotifications(newNotifications, false, false, false, true);
|
|
||||||
updateSummary(alert, nonstop, fiveTimes, newNotifications);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
createNotifications(newNotifications, alert, nonstop, fiveTimes, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
localBroadcastManager.broadcastRefresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createNotifications(
|
|
||||||
List<Notification> notifications,
|
|
||||||
boolean alert,
|
|
||||||
boolean nonstop,
|
|
||||||
boolean fiveTimes,
|
|
||||||
boolean useGroupKey) {
|
|
||||||
for (Notification notification : notifications) {
|
|
||||||
NotificationCompat.Builder builder = getTaskNotification(notification);
|
|
||||||
if (builder == null) {
|
|
||||||
notificationManagerCompat.cancel((int) notification.getTaskId());
|
|
||||||
notificationDao.delete(notification.getTaskId());
|
|
||||||
} else {
|
|
||||||
builder
|
|
||||||
.setGroup(
|
|
||||||
useGroupKey
|
|
||||||
? GROUP_KEY
|
|
||||||
: (atLeastNougat() ? Long.toString(notification.getTaskId()) : null))
|
|
||||||
.setGroupAlertBehavior(
|
|
||||||
alert
|
|
||||||
? NotificationCompat.GROUP_ALERT_CHILDREN
|
|
||||||
: NotificationCompat.GROUP_ALERT_SUMMARY);
|
|
||||||
notify(notification.getTaskId(), builder, alert, nonstop, fiveTimes);
|
|
||||||
alert = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void notify(
|
|
||||||
long notificationId,
|
|
||||||
NotificationCompat.Builder builder,
|
|
||||||
boolean alert,
|
|
||||||
boolean nonstop,
|
|
||||||
boolean fiveTimes) {
|
|
||||||
if (!preferences.getBoolean(R.string.p_rmd_enabled, true)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
builder.setLocalOnly(!preferences.getBoolean(R.string.p_wearable_notifications, true));
|
|
||||||
if (preOreo()) {
|
|
||||||
if (alert) {
|
|
||||||
builder
|
|
||||||
.setSound(preferences.getRingtone())
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
||||||
.setDefaults(preferences.getNotificationDefaults());
|
|
||||||
} else {
|
|
||||||
builder.setDefaults(0).setTicker(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
android.app.Notification notification = builder.build();
|
|
||||||
int ringTimes = fiveTimes ? 5 : 1;
|
|
||||||
if (alert && nonstop) {
|
|
||||||
notification.flags |= FLAG_INSISTENT;
|
|
||||||
ringTimes = 1;
|
|
||||||
}
|
|
||||||
if (preferences.usePersistentReminders()) {
|
|
||||||
notification.flags |= FLAG_NO_CLEAR;
|
|
||||||
}
|
|
||||||
Intent deleteIntent = new Intent(context, NotificationClearedReceiver.class);
|
|
||||||
deleteIntent.putExtra(EXTRA_NOTIFICATION_ID, notificationId);
|
|
||||||
notification.deleteIntent =
|
|
||||||
PendingIntent.getBroadcast(
|
|
||||||
context, (int) notificationId, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
|
||||||
|
|
||||||
List<Long> evicted = queue.add(notificationId);
|
|
||||||
if (evicted.size() > 0) {
|
|
||||||
cancel(evicted);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < ringTimes; i++) {
|
|
||||||
throttle.run(() -> notificationManagerCompat.notify((int) notificationId, notification));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateSummary(
|
|
||||||
boolean notify, boolean nonStop, boolean fiveTimes, List<Notification> newNotifications) {
|
|
||||||
List<Task> tasks = taskDao.activeNotifications();
|
|
||||||
int taskCount = tasks.size();
|
|
||||||
if (taskCount == 0) {
|
|
||||||
cancelSummaryNotification();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ArrayList<Long> taskIds = new ArrayList<>(transform(tasks, Task::getId));
|
|
||||||
Filter filter =
|
|
||||||
new Filter(
|
|
||||||
context.getString(R.string.notifications),
|
|
||||||
new QueryTemplate()
|
|
||||||
.join(Join.inner(Notification.TABLE, Task.ID.eq(Notification.TASK))));
|
|
||||||
long when = notificationDao.latestTimestamp();
|
|
||||||
int maxPriority = 3;
|
|
||||||
String summaryTitle =
|
|
||||||
context.getResources().getQuantityString(R.plurals.task_count, taskCount, taskCount);
|
|
||||||
NotificationCompat.InboxStyle style =
|
|
||||||
new NotificationCompat.InboxStyle().setBigContentTitle(summaryTitle);
|
|
||||||
List<String> titles = new ArrayList<>();
|
|
||||||
List<String> ticker = new ArrayList<>();
|
|
||||||
for (Task task : tasks) {
|
|
||||||
String title = task.getTitle();
|
|
||||||
style.addLine(title);
|
|
||||||
titles.add(title);
|
|
||||||
maxPriority = Math.min(maxPriority, task.getPriority());
|
|
||||||
}
|
|
||||||
for (Notification notification : newNotifications) {
|
|
||||||
Task task = tryFind(tasks, t -> t.getId() == notification.getTaskId()).orNull();
|
|
||||||
if (task == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
ticker.add(task.getTitle());
|
|
||||||
}
|
|
||||||
NotificationCompat.Builder builder =
|
|
||||||
new NotificationCompat.Builder(context, NotificationManager.NOTIFICATION_CHANNEL_DEFAULT)
|
|
||||||
.setContentTitle(summaryTitle)
|
|
||||||
.setContentText(
|
|
||||||
Joiner.on(context.getString(R.string.list_separator_with_space)).join(titles))
|
|
||||||
.setShowWhen(true)
|
|
||||||
.setWhen(when)
|
|
||||||
.setSmallIcon(R.drawable.ic_done_all_white_24dp)
|
|
||||||
.setStyle(style)
|
|
||||||
.setColor(colorProvider.getPriorityColor(maxPriority, true))
|
|
||||||
.setOnlyAlertOnce(false)
|
|
||||||
.setContentIntent(
|
|
||||||
PendingIntent.getActivity(
|
|
||||||
context,
|
|
||||||
0,
|
|
||||||
TaskIntents.getTaskListIntent(context, filter),
|
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT))
|
|
||||||
.setGroupSummary(true)
|
|
||||||
.setGroup(GROUP_KEY)
|
|
||||||
.setTicker(
|
|
||||||
Joiner.on(context.getString(R.string.list_separator_with_space)).join(ticker))
|
|
||||||
.setGroupAlertBehavior(
|
|
||||||
notify
|
|
||||||
? NotificationCompat.GROUP_ALERT_SUMMARY
|
|
||||||
: NotificationCompat.GROUP_ALERT_CHILDREN);
|
|
||||||
|
|
||||||
Intent snoozeIntent = SnoozeActivity.newIntent(context, taskIds);
|
|
||||||
builder.addAction(
|
|
||||||
R.drawable.ic_snooze_white_24dp,
|
|
||||||
context.getString(R.string.snooze_all),
|
|
||||||
PendingIntent.getActivity(context, 0, snoozeIntent, PendingIntent.FLAG_CANCEL_CURRENT));
|
|
||||||
|
|
||||||
notify(NotificationManager.SUMMARY_NOTIFICATION_ID, builder, notify, nonStop, fiveTimes);
|
|
||||||
}
|
|
||||||
|
|
||||||
public NotificationCompat.Builder getTaskNotification(Notification notification) {
|
|
||||||
long id = notification.getTaskId();
|
|
||||||
int type = notification.getType();
|
|
||||||
long when = notification.getTimestamp();
|
|
||||||
Task task = taskDao.fetchBlocking(id);
|
|
||||||
if (task == null) {
|
|
||||||
Timber.e("Could not find %s", id);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// you're done, or not yours - don't sound, do delete
|
|
||||||
if (task.isCompleted() || task.isDeleted()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// new task edit in progress
|
|
||||||
if (isNullOrEmpty(task.getTitle())) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// it's hidden - don't sound, don't delete
|
|
||||||
if (task.isHidden() && type == ReminderService.TYPE_RANDOM) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// task due date was changed, but alarm wasn't rescheduled
|
|
||||||
boolean dueInFuture =
|
|
||||||
task.hasDueTime()
|
|
||||||
&& new DateTime(task.getDueDate()).startOfMinute().getMillis() > DateUtilities.now()
|
|
||||||
|| !task.hasDueTime()
|
|
||||||
&& task.getDueDate() - DateUtilities.now() > DateUtilities.ONE_DAY;
|
|
||||||
if ((type == ReminderService.TYPE_DUE || type == ReminderService.TYPE_OVERDUE)
|
|
||||||
&& (!task.hasDueDate() || dueInFuture)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// read properties
|
|
||||||
final String taskTitle = task.getTitle();
|
|
||||||
final String taskDescription = task.getNotes();
|
|
||||||
|
|
||||||
// update last reminder time
|
|
||||||
long reminderTime = new DateTime(when).endOfMinute().getMillis();
|
|
||||||
if (reminderTime != task.getReminderLast()) {
|
|
||||||
task.setReminderLast(reminderTime);
|
|
||||||
taskDao.save(task);
|
|
||||||
}
|
|
||||||
|
|
||||||
NotificationCompat.Builder builder =
|
|
||||||
new NotificationCompat.Builder(context, NotificationManager.NOTIFICATION_CHANNEL_DEFAULT)
|
|
||||||
.setCategory(NotificationCompat.CATEGORY_REMINDER)
|
|
||||||
.setContentTitle(taskTitle)
|
|
||||||
.setColor(colorProvider.getPriorityColor(task.getPriority(), true))
|
|
||||||
.setSmallIcon(R.drawable.ic_check_white_24dp)
|
|
||||||
.setWhen(when)
|
|
||||||
.setOnlyAlertOnce(false)
|
|
||||||
.setShowWhen(true)
|
|
||||||
.setTicker(taskTitle);
|
|
||||||
|
|
||||||
Intent intent = NotificationActivity.newIntent(context, taskTitle, id);
|
|
||||||
builder.setContentIntent(
|
|
||||||
PendingIntent.getActivity(context, (int) id, intent, PendingIntent.FLAG_UPDATE_CURRENT));
|
|
||||||
|
|
||||||
if (type == TYPE_GEOFENCE_ENTER || type == TYPE_GEOFENCE_EXIT) {
|
|
||||||
Place place = locationDao.getPlace(notification.getLocation());
|
|
||||||
if (place != null) {
|
|
||||||
builder.setContentText(
|
|
||||||
context.getString(
|
|
||||||
type == TYPE_GEOFENCE_ENTER
|
|
||||||
? R.string.location_arrived
|
|
||||||
: R.string.location_departed,
|
|
||||||
place.getDisplayName()));
|
|
||||||
}
|
|
||||||
} else if (!isNullOrEmpty(taskDescription)) {
|
|
||||||
builder
|
|
||||||
.setContentText(taskDescription)
|
|
||||||
.setStyle(new NotificationCompat.BigTextStyle().bigText(taskDescription));
|
|
||||||
}
|
|
||||||
|
|
||||||
Intent completeIntent = new Intent(context, CompleteTaskReceiver.class);
|
|
||||||
completeIntent.putExtra(CompleteTaskReceiver.TASK_ID, id);
|
|
||||||
PendingIntent completePendingIntent =
|
|
||||||
PendingIntent.getBroadcast(
|
|
||||||
context, (int) id, completeIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
|
||||||
|
|
||||||
NotificationCompat.Action completeAction =
|
|
||||||
new NotificationCompat.Action.Builder(
|
|
||||||
R.drawable.ic_check_white_24dp,
|
|
||||||
context.getString(R.string.rmd_NoA_done),
|
|
||||||
completePendingIntent)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
Intent snoozeIntent = SnoozeActivity.newIntent(context, id);
|
|
||||||
PendingIntent snoozePendingIntent =
|
|
||||||
PendingIntent.getActivity(
|
|
||||||
context, (int) id, snoozeIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
|
||||||
|
|
||||||
NotificationCompat.WearableExtender wearableExtender =
|
|
||||||
new NotificationCompat.WearableExtender();
|
|
||||||
wearableExtender.addAction(completeAction);
|
|
||||||
for (final SnoozeOption snoozeOption : SnoozeDialog.getSnoozeOptions(preferences)) {
|
|
||||||
final long timestamp = snoozeOption.getDateTime().getMillis();
|
|
||||||
Intent wearableIntent = SnoozeActivity.newIntent(context, id);
|
|
||||||
wearableIntent.setAction(String.format("snooze-%s-%s", id, timestamp));
|
|
||||||
wearableIntent.putExtra(SnoozeActivity.EXTRA_SNOOZE_TIME, timestamp);
|
|
||||||
PendingIntent wearablePendingIntent =
|
|
||||||
PendingIntent.getActivity(
|
|
||||||
context, (int) id, wearableIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
|
||||||
wearableExtender.addAction(
|
|
||||||
new NotificationCompat.Action.Builder(
|
|
||||||
R.drawable.ic_snooze_white_24dp,
|
|
||||||
context.getString(snoozeOption.getResId()),
|
|
||||||
wearablePendingIntent)
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder
|
|
||||||
.addAction(completeAction)
|
|
||||||
.addAction(
|
|
||||||
R.drawable.ic_snooze_white_24dp,
|
|
||||||
context.getString(R.string.rmd_NoA_snooze),
|
|
||||||
snoozePendingIntent)
|
|
||||||
.extend(wearableExtender);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void cancelSummaryNotification() {
|
|
||||||
notificationManagerCompat.cancel(SUMMARY_NOTIFICATION_ID);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,401 @@
|
|||||||
|
package org.tasks.notifications
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import com.todoroo.andlib.sql.Join.Companion.inner
|
||||||
|
import com.todoroo.andlib.sql.QueryTemplate
|
||||||
|
import com.todoroo.andlib.utility.AndroidUtilities
|
||||||
|
import com.todoroo.andlib.utility.DateUtilities
|
||||||
|
import com.todoroo.astrid.api.Filter
|
||||||
|
import com.todoroo.astrid.dao.TaskDaoBlocking
|
||||||
|
import com.todoroo.astrid.data.Task
|
||||||
|
import com.todoroo.astrid.reminders.ReminderService
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import io.reactivex.Completable
|
||||||
|
import io.reactivex.Single
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
import org.tasks.LocalBroadcastManager
|
||||||
|
import org.tasks.R
|
||||||
|
import org.tasks.Strings.isNullOrEmpty
|
||||||
|
import org.tasks.data.LocationDaoBlocking
|
||||||
|
import org.tasks.intents.TaskIntents
|
||||||
|
import org.tasks.preferences.Preferences
|
||||||
|
import org.tasks.receivers.CompleteTaskReceiver
|
||||||
|
import org.tasks.reminders.NotificationActivity
|
||||||
|
import org.tasks.reminders.SnoozeActivity
|
||||||
|
import org.tasks.reminders.SnoozeDialog
|
||||||
|
import org.tasks.themes.ColorProvider
|
||||||
|
import org.tasks.time.DateTime
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.*
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class NotificationManager @Inject constructor(
|
||||||
|
@param:ApplicationContext private val context: Context,
|
||||||
|
private val preferences: Preferences,
|
||||||
|
private val notificationDao: NotificationDaoBlocking,
|
||||||
|
private val taskDao: TaskDaoBlocking,
|
||||||
|
private val locationDao: LocationDaoBlocking,
|
||||||
|
private val localBroadcastManager: LocalBroadcastManager) {
|
||||||
|
private val notificationManagerCompat = NotificationManagerCompat.from(context)
|
||||||
|
private val colorProvider = ColorProvider(context, preferences)
|
||||||
|
private val throttle = Throttle(NOTIFICATIONS_PER_SECOND)
|
||||||
|
private val queue = NotificationLimiter(MAX_NOTIFICATIONS)
|
||||||
|
|
||||||
|
@SuppressLint("CheckResult")
|
||||||
|
fun cancel(id: Long) {
|
||||||
|
if (id == SUMMARY_NOTIFICATION_ID.toLong()) {
|
||||||
|
Single.fromCallable { notificationDao.getAll() + id }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe { ids: Iterable<Long> -> this.cancel(ids) }
|
||||||
|
} else {
|
||||||
|
cancel(listOf(id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("CheckResult")
|
||||||
|
fun cancel(ids: Iterable<Long>) {
|
||||||
|
for (id in ids) {
|
||||||
|
notificationManagerCompat.cancel(id.toInt())
|
||||||
|
queue.remove(id)
|
||||||
|
}
|
||||||
|
Completable.fromAction { notificationDao.deleteAll(ids.toList()) }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.subscribe {
|
||||||
|
notifyTasks(
|
||||||
|
emptyList(),
|
||||||
|
alert = false,
|
||||||
|
nonstop = false,
|
||||||
|
fiveTimes = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restoreNotifications(cancelExisting: Boolean) {
|
||||||
|
val notifications = notificationDao.getAllOrdered()
|
||||||
|
if (cancelExisting) {
|
||||||
|
for (notification in notifications) {
|
||||||
|
notificationManagerCompat.cancel(notification.taskId.toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (preferences.bundleNotifications() && notifications.size > 1) {
|
||||||
|
updateSummary(
|
||||||
|
notify = false,
|
||||||
|
nonStop = false,
|
||||||
|
fiveTimes = false,
|
||||||
|
newNotifications = emptyList())
|
||||||
|
createNotifications(
|
||||||
|
notifications,
|
||||||
|
alert = false,
|
||||||
|
nonstop = false,
|
||||||
|
fiveTimes = false,
|
||||||
|
useGroupKey = true)
|
||||||
|
} else {
|
||||||
|
createNotifications(
|
||||||
|
notifications,
|
||||||
|
alert = false,
|
||||||
|
nonstop = false,
|
||||||
|
fiveTimes = false,
|
||||||
|
useGroupKey = false)
|
||||||
|
cancelSummaryNotification()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun notifyTasks(
|
||||||
|
newNotifications: List<Notification>, alert: Boolean, nonstop: Boolean, fiveTimes: Boolean) {
|
||||||
|
val existingNotifications = notificationDao.getAllOrdered()
|
||||||
|
notificationDao.insertAll(newNotifications)
|
||||||
|
val totalCount = existingNotifications.size + newNotifications.size
|
||||||
|
if (totalCount == 0) {
|
||||||
|
cancelSummaryNotification()
|
||||||
|
} else if (totalCount == 1) {
|
||||||
|
val notifications = existingNotifications + newNotifications
|
||||||
|
createNotifications(notifications, alert, nonstop, fiveTimes, false)
|
||||||
|
cancelSummaryNotification()
|
||||||
|
} else if (preferences.bundleNotifications()) {
|
||||||
|
updateSummary(
|
||||||
|
notify = false,
|
||||||
|
nonStop = false,
|
||||||
|
fiveTimes = false,
|
||||||
|
newNotifications = emptyList())
|
||||||
|
if (existingNotifications.size == 1) {
|
||||||
|
createNotifications(
|
||||||
|
existingNotifications,
|
||||||
|
alert = false,
|
||||||
|
nonstop = false,
|
||||||
|
fiveTimes = false,
|
||||||
|
useGroupKey = true)
|
||||||
|
}
|
||||||
|
if (AndroidUtilities.atLeastNougat() && newNotifications.size == 1) {
|
||||||
|
createNotifications(newNotifications, alert, nonstop, fiveTimes, true)
|
||||||
|
} else {
|
||||||
|
createNotifications(
|
||||||
|
newNotifications,
|
||||||
|
alert = false,
|
||||||
|
nonstop = false,
|
||||||
|
fiveTimes = false,
|
||||||
|
useGroupKey = true)
|
||||||
|
updateSummary(alert, nonstop, fiveTimes, newNotifications)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
createNotifications(newNotifications, alert, nonstop, fiveTimes, false)
|
||||||
|
}
|
||||||
|
localBroadcastManager.broadcastRefresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createNotifications(
|
||||||
|
notifications: List<Notification>,
|
||||||
|
alert: Boolean,
|
||||||
|
nonstop: Boolean,
|
||||||
|
fiveTimes: Boolean,
|
||||||
|
useGroupKey: Boolean) {
|
||||||
|
var alert = alert
|
||||||
|
for (notification in notifications) {
|
||||||
|
val builder = getTaskNotification(notification)
|
||||||
|
if (builder == null) {
|
||||||
|
notificationManagerCompat.cancel(notification.taskId.toInt())
|
||||||
|
notificationDao.delete(notification.taskId)
|
||||||
|
} else {
|
||||||
|
builder
|
||||||
|
.setGroup(
|
||||||
|
if (useGroupKey) GROUP_KEY else if (AndroidUtilities.atLeastNougat()) java.lang.Long.toString(notification.taskId) else null)
|
||||||
|
.setGroupAlertBehavior(
|
||||||
|
if (alert) NotificationCompat.GROUP_ALERT_CHILDREN else NotificationCompat.GROUP_ALERT_SUMMARY)
|
||||||
|
notify(notification.taskId, builder, alert, nonstop, fiveTimes)
|
||||||
|
alert = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun notify(
|
||||||
|
notificationId: Long,
|
||||||
|
builder: NotificationCompat.Builder,
|
||||||
|
alert: Boolean,
|
||||||
|
nonstop: Boolean,
|
||||||
|
fiveTimes: Boolean) {
|
||||||
|
if (!preferences.getBoolean(R.string.p_rmd_enabled, true)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
builder.setLocalOnly(!preferences.getBoolean(R.string.p_wearable_notifications, true))
|
||||||
|
if (AndroidUtilities.preOreo()) {
|
||||||
|
if (alert) {
|
||||||
|
builder
|
||||||
|
.setSound(preferences.ringtone)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
.setDefaults(preferences.notificationDefaults)
|
||||||
|
} else {
|
||||||
|
builder.setDefaults(0).setTicker(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val notification = builder.build()
|
||||||
|
var ringTimes = if (fiveTimes) 5 else 1
|
||||||
|
if (alert && nonstop) {
|
||||||
|
notification.flags = notification.flags or NotificationCompat.FLAG_INSISTENT
|
||||||
|
ringTimes = 1
|
||||||
|
}
|
||||||
|
if (preferences.usePersistentReminders()) {
|
||||||
|
notification.flags = notification.flags or NotificationCompat.FLAG_NO_CLEAR
|
||||||
|
}
|
||||||
|
val deleteIntent = Intent(context, NotificationClearedReceiver::class.java)
|
||||||
|
deleteIntent.putExtra(EXTRA_NOTIFICATION_ID, notificationId)
|
||||||
|
notification.deleteIntent = PendingIntent.getBroadcast(
|
||||||
|
context, notificationId.toInt(), deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
|
val evicted = queue.add(notificationId)
|
||||||
|
if (evicted.size > 0) {
|
||||||
|
cancel(evicted)
|
||||||
|
}
|
||||||
|
for (i in 0 until ringTimes) {
|
||||||
|
throttle.run { notificationManagerCompat.notify(notificationId.toInt(), notification) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateSummary(
|
||||||
|
notify: Boolean, nonStop: Boolean, fiveTimes: Boolean, newNotifications: List<Notification>) {
|
||||||
|
val tasks = taskDao.activeNotifications()
|
||||||
|
val taskCount = tasks.size
|
||||||
|
if (taskCount == 0) {
|
||||||
|
cancelSummaryNotification()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val taskIds = tasks.map { it.id }
|
||||||
|
val filter = Filter(
|
||||||
|
context.getString(R.string.notifications),
|
||||||
|
QueryTemplate()
|
||||||
|
.join(inner(Notification.TABLE, Task.ID.eq(Notification.TASK))))
|
||||||
|
val `when` = notificationDao.latestTimestamp()
|
||||||
|
var maxPriority = 3
|
||||||
|
val summaryTitle = context.resources.getQuantityString(R.plurals.task_count, taskCount, taskCount)
|
||||||
|
val style = NotificationCompat.InboxStyle().setBigContentTitle(summaryTitle)
|
||||||
|
val titles: MutableList<String?> = ArrayList()
|
||||||
|
val ticker: MutableList<String?> = ArrayList()
|
||||||
|
for (task in tasks) {
|
||||||
|
val title = task.title
|
||||||
|
style.addLine(title)
|
||||||
|
titles.add(title)
|
||||||
|
maxPriority = min(maxPriority, task.priority)
|
||||||
|
}
|
||||||
|
for (notification in newNotifications) {
|
||||||
|
tasks.find { it.id == notification.taskId }?.let { ticker.add(it.title) }
|
||||||
|
}
|
||||||
|
val builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_DEFAULT)
|
||||||
|
.setContentTitle(summaryTitle)
|
||||||
|
.setContentText(
|
||||||
|
titles.joinToString(context.getString(R.string.list_separator_with_space)))
|
||||||
|
.setShowWhen(true)
|
||||||
|
.setWhen(`when`)
|
||||||
|
.setSmallIcon(R.drawable.ic_done_all_white_24dp)
|
||||||
|
.setStyle(style)
|
||||||
|
.setColor(colorProvider.getPriorityColor(maxPriority, true))
|
||||||
|
.setOnlyAlertOnce(false)
|
||||||
|
.setContentIntent(
|
||||||
|
PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
0,
|
||||||
|
TaskIntents.getTaskListIntent(context, filter),
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT))
|
||||||
|
.setGroupSummary(true)
|
||||||
|
.setGroup(GROUP_KEY)
|
||||||
|
.setTicker(
|
||||||
|
ticker.joinToString(context.getString(R.string.list_separator_with_space)))
|
||||||
|
.setGroupAlertBehavior(
|
||||||
|
if (notify) NotificationCompat.GROUP_ALERT_SUMMARY else NotificationCompat.GROUP_ALERT_CHILDREN)
|
||||||
|
val snoozeIntent = SnoozeActivity.newIntent(context, taskIds)
|
||||||
|
builder.addAction(
|
||||||
|
R.drawable.ic_snooze_white_24dp,
|
||||||
|
context.getString(R.string.snooze_all),
|
||||||
|
PendingIntent.getActivity(context, 0, snoozeIntent, PendingIntent.FLAG_CANCEL_CURRENT))
|
||||||
|
notify(SUMMARY_NOTIFICATION_ID.toLong(), builder, notify, nonStop, fiveTimes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTaskNotification(notification: Notification): NotificationCompat.Builder? {
|
||||||
|
val id = notification.taskId
|
||||||
|
val type = notification.type
|
||||||
|
val `when` = notification.timestamp
|
||||||
|
val task = taskDao.fetchBlocking(id)
|
||||||
|
if (task == null) {
|
||||||
|
Timber.e("Could not find %s", id)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// you're done, or not yours - don't sound, do delete
|
||||||
|
if (task.isCompleted || task.isDeleted) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// new task edit in progress
|
||||||
|
if (isNullOrEmpty(task.title)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// it's hidden - don't sound, don't delete
|
||||||
|
if (task.isHidden && type == ReminderService.TYPE_RANDOM) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// task due date was changed, but alarm wasn't rescheduled
|
||||||
|
val dueInFuture = (task.hasDueTime()
|
||||||
|
&& DateTime(task.dueDate).startOfMinute().millis > DateUtilities.now()
|
||||||
|
|| !task.hasDueTime()
|
||||||
|
&& task.dueDate - DateUtilities.now() > DateUtilities.ONE_DAY)
|
||||||
|
if ((type == ReminderService.TYPE_DUE || type == ReminderService.TYPE_OVERDUE)
|
||||||
|
&& (!task.hasDueDate() || dueInFuture)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// read properties
|
||||||
|
val taskTitle = task.title
|
||||||
|
val taskDescription = task.notes
|
||||||
|
|
||||||
|
// update last reminder time
|
||||||
|
val reminderTime = DateTime(`when`).endOfMinute().millis
|
||||||
|
if (reminderTime != task.reminderLast) {
|
||||||
|
task.reminderLast = reminderTime
|
||||||
|
taskDao.save(task)
|
||||||
|
}
|
||||||
|
val builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_DEFAULT)
|
||||||
|
.setCategory(NotificationCompat.CATEGORY_REMINDER)
|
||||||
|
.setContentTitle(taskTitle)
|
||||||
|
.setColor(colorProvider.getPriorityColor(task.priority, true))
|
||||||
|
.setSmallIcon(R.drawable.ic_check_white_24dp)
|
||||||
|
.setWhen(`when`)
|
||||||
|
.setOnlyAlertOnce(false)
|
||||||
|
.setShowWhen(true)
|
||||||
|
.setTicker(taskTitle)
|
||||||
|
val intent = NotificationActivity.newIntent(context, taskTitle, id)
|
||||||
|
builder.setContentIntent(
|
||||||
|
PendingIntent.getActivity(context, id.toInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT))
|
||||||
|
if (type == ReminderService.TYPE_GEOFENCE_ENTER || type == ReminderService.TYPE_GEOFENCE_EXIT) {
|
||||||
|
val place = locationDao.getPlace(notification.location!!)
|
||||||
|
if (place != null) {
|
||||||
|
builder.setContentText(
|
||||||
|
context.getString(
|
||||||
|
if (type == ReminderService.TYPE_GEOFENCE_ENTER) R.string.location_arrived else R.string.location_departed,
|
||||||
|
place.displayName))
|
||||||
|
}
|
||||||
|
} else if (!isNullOrEmpty(taskDescription)) {
|
||||||
|
builder
|
||||||
|
.setContentText(taskDescription)
|
||||||
|
.setStyle(NotificationCompat.BigTextStyle().bigText(taskDescription))
|
||||||
|
}
|
||||||
|
val completeIntent = Intent(context, CompleteTaskReceiver::class.java)
|
||||||
|
completeIntent.putExtra(CompleteTaskReceiver.TASK_ID, id)
|
||||||
|
val completePendingIntent = PendingIntent.getBroadcast(
|
||||||
|
context, id.toInt(), completeIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
|
val completeAction = NotificationCompat.Action.Builder(
|
||||||
|
R.drawable.ic_check_white_24dp,
|
||||||
|
context.getString(R.string.rmd_NoA_done),
|
||||||
|
completePendingIntent)
|
||||||
|
.build()
|
||||||
|
val snoozeIntent = SnoozeActivity.newIntent(context, id)
|
||||||
|
val snoozePendingIntent = PendingIntent.getActivity(
|
||||||
|
context, id.toInt(), snoozeIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
|
val wearableExtender = NotificationCompat.WearableExtender()
|
||||||
|
wearableExtender.addAction(completeAction)
|
||||||
|
for (snoozeOption in SnoozeDialog.getSnoozeOptions(preferences)) {
|
||||||
|
val timestamp = snoozeOption.dateTime.millis
|
||||||
|
val wearableIntent = SnoozeActivity.newIntent(context, id)
|
||||||
|
wearableIntent.action = String.format("snooze-%s-%s", id, timestamp)
|
||||||
|
wearableIntent.putExtra(SnoozeActivity.EXTRA_SNOOZE_TIME, timestamp)
|
||||||
|
val wearablePendingIntent = PendingIntent.getActivity(
|
||||||
|
context, id.toInt(), wearableIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
|
wearableExtender.addAction(
|
||||||
|
NotificationCompat.Action.Builder(
|
||||||
|
R.drawable.ic_snooze_white_24dp,
|
||||||
|
context.getString(snoozeOption.resId),
|
||||||
|
wearablePendingIntent)
|
||||||
|
.build())
|
||||||
|
}
|
||||||
|
return builder
|
||||||
|
.addAction(completeAction)
|
||||||
|
.addAction(
|
||||||
|
R.drawable.ic_snooze_white_24dp,
|
||||||
|
context.getString(R.string.rmd_NoA_snooze),
|
||||||
|
snoozePendingIntent)
|
||||||
|
.extend(wearableExtender)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cancelSummaryNotification() {
|
||||||
|
notificationManagerCompat.cancel(SUMMARY_NOTIFICATION_ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val NOTIFICATION_CHANNEL_DEFAULT = "notifications"
|
||||||
|
const val NOTIFICATION_CHANNEL_TASKER = "notifications_tasker"
|
||||||
|
const val NOTIFICATION_CHANNEL_TIMERS = "notifications_timers"
|
||||||
|
const val NOTIFICATION_CHANNEL_MISCELLANEOUS = "notifications_miscellaneous"
|
||||||
|
const val MAX_NOTIFICATIONS = 21
|
||||||
|
const val EXTRA_NOTIFICATION_ID = "extra_notification_id"
|
||||||
|
const val SUMMARY_NOTIFICATION_ID = 0
|
||||||
|
private const val GROUP_KEY = "tasks"
|
||||||
|
private const val NOTIFICATIONS_PER_SECOND = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue