AlarmManager + service for notifications

pull/795/head
Alex Baker 6 years ago
parent eabdc75c8f
commit 1e86be28ee

@ -70,6 +70,26 @@ public class NotificationQueueTest {
});
}
@Test
public void alarmAndReminderSameTimeDifferentId() {
long now = currentTimeMillis();
queue.add(new AlarmEntry(1, 2, now));
queue.add(new ReminderEntry(1, now + 1000, TYPE_DUE));
verify(workManager).scheduleNotification(now);
Freeze.freezeAt(now)
.thawAfter(
new Snippet() {
{
assertEquals(
newHashSet(new AlarmEntry(1, 2, now), new ReminderEntry(1, now + 1000, TYPE_DUE)),
newHashSet(queue.getOverdueJobs()));
}
});
}
@Test
public void removeAlarmLeaveReminder() {
long now = currentTimeMillis();

@ -14,7 +14,7 @@ import org.tasks.Notifier;
import org.tasks.data.Location;
import org.tasks.data.LocationDao;
import org.tasks.injection.InjectingJobIntentService;
import org.tasks.injection.IntentServiceComponent;
import org.tasks.injection.ServiceComponent;
import timber.log.Timber;
public class GeofenceTransitionsIntentService extends InjectingJobIntentService {
@ -45,7 +45,7 @@ public class GeofenceTransitionsIntentService extends InjectingJobIntentService
}
@Override
protected void inject(IntentServiceComponent component) {
protected void inject(ServiceComponent component) {
component.inject(this);
}

@ -13,6 +13,7 @@
<!-- notifications -->
<!-- ************* -->
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- *************************** -->
<!-- google calendar integration -->
@ -432,6 +433,10 @@
android:value=".dashclock.DashClockSettings"/>
</service>
<service
android:exported="false"
android:name=".jobs.NotificationService"/>
<activity
android:exported="true"
android:label="@string/app_name"

@ -60,7 +60,10 @@ public final class ReminderService {
}
public void scheduleAlarm(Task task) {
jobs.add(getReminderEntry(task));
ReminderEntry reminder = getReminderEntry(task);
if (reminder != null) {
jobs.add(reminder);
}
}
public void cancelReminder(long taskId) {

@ -158,7 +158,7 @@ public class GoogleTaskSynchronizer {
PendingIntent resolve =
PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Builder builder =
new NotificationCompat.Builder(context, NotificationManager.NOTIFICATION_CHANNEL_DEFAULT)
new NotificationCompat.Builder(context, NotificationManager.NOTIFICATION_CHANNEL_MISCELLANEOUS)
.setAutoCancel(true)
.setContentIntent(resolve)
.setContentTitle(context.getString(R.string.sync_error_permissions))

@ -19,7 +19,7 @@ public interface ApplicationComponent {
BroadcastComponent plus(BroadcastModule module);
IntentServiceComponent plus(IntentServiceModule module);
ServiceComponent plus(ServiceModule module);
JobComponent plus(WorkModule module);
}

@ -18,7 +18,7 @@ public abstract class InjectingJobIntentService extends JobIntentService {
@Override
protected final void onHandleWork(@NonNull Intent intent) {
inject(
((InjectingApplication) getApplication()).getComponent().plus(new IntentServiceModule()));
((InjectingApplication) getApplication()).getComponent().plus(new ServiceModule()));
try {
doWork(intent);
@ -29,5 +29,5 @@ public abstract class InjectingJobIntentService extends JobIntentService {
protected abstract void doWork(@Nonnull Intent intent);
protected abstract void inject(IntentServiceComponent component);
protected abstract void inject(ServiceComponent component);
}

@ -0,0 +1,40 @@
package org.tasks.injection;
import android.app.Notification;
import android.app.Service;
import android.content.Intent;
import io.reactivex.Completable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import javax.annotation.Nonnull;
public abstract class InjectingService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
startForeground(getNotificationId(), getNotification());
inject(((InjectingApplication) getApplication()).getComponent().plus(new ServiceModule()));
Completable.fromAction(() -> doWork(intent))
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.doFinally(this::stop)
.subscribe();
return Service.START_NOT_STICKY;
}
private void stop() {
stopForeground(true);
stopSelf();
}
protected abstract int getNotificationId();
protected abstract Notification getNotification();
protected abstract void doWork(@Nonnull Intent intent);
protected abstract void inject(ServiceComponent component);
}

@ -6,7 +6,6 @@ import org.tasks.jobs.BackupWork;
import org.tasks.jobs.CleanupWork;
import org.tasks.jobs.DriveUploader;
import org.tasks.jobs.MidnightRefreshWork;
import org.tasks.jobs.NotificationWork;
import org.tasks.jobs.RefreshWork;
import org.tasks.jobs.SyncWork;
@ -15,8 +14,6 @@ public interface JobComponent {
void inject(SyncWork syncWork);
void inject(NotificationWork notificationWork);
void inject(BackupWork backupWork);
void inject(RefreshWork refreshWork);

@ -1,6 +1,7 @@
package org.tasks.injection;
import dagger.Subcomponent;
import org.tasks.jobs.NotificationService;
import org.tasks.locale.receiver.TaskerIntentService;
import org.tasks.location.GeofenceTransitionsIntentService;
import org.tasks.scheduling.BackgroundScheduler;
@ -8,8 +9,8 @@ import org.tasks.scheduling.CalendarNotificationIntentService;
import org.tasks.scheduling.GeofenceSchedulingIntentService;
import org.tasks.scheduling.NotificationSchedulerIntentService;
@Subcomponent(modules = IntentServiceModule.class)
public interface IntentServiceComponent {
@Subcomponent(modules = ServiceModule.class)
public interface ServiceComponent {
void inject(GeofenceSchedulingIntentService geofenceSchedulingIntentService);
@ -22,4 +23,6 @@ public interface IntentServiceComponent {
void inject(BackgroundScheduler backgroundScheduler);
void inject(TaskerIntentService taskerIntentService);
void inject(NotificationService notificationService);
}

@ -3,4 +3,4 @@ package org.tasks.injection;
import dagger.Module;
@Module
public class IntentServiceModule {}
public class ServiceModule {}

@ -11,8 +11,6 @@ import com.google.common.primitives.Ints;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.tasks.injection.ApplicationScope;
import org.tasks.preferences.Preferences;
import org.tasks.time.DateTime;
@ -36,12 +34,11 @@ public class NotificationQueue {
}
public synchronized <T extends NotificationQueueEntry> void add(Iterable<T> entries) {
boolean reschedule = false;
long originalFirstTime = firstTime();
for (T entry : filter(entries, notNull())) {
reschedule |= jobs.isEmpty() || entry.getTime() < firstTime();
jobs.put(entry.getTime(), entry);
}
if (reschedule) {
if (originalFirstTime != firstTime()) {
scheduleNext(true);
}
}
@ -59,16 +56,12 @@ public class NotificationQueue {
cancel(ReminderEntry.class, taskId);
}
private synchronized void cancel(Class<? extends NotificationQueueEntry> c, long id) {
boolean reschedule = false;
private void cancel(Class<? extends NotificationQueueEntry> c, long id) {
long firstTime = firstTime();
List<NotificationQueueEntry> existing =
newArrayList(filter(jobs.values(), r -> r.getClass().equals(c) && r.getId() == id));
for (NotificationQueueEntry entry : existing) {
reschedule |= entry.getTime() == firstTime;
jobs.remove(entry.getTime(), entry);
}
if (reschedule) {
remove(newArrayList(filter(jobs.values(), r -> r.getClass().equals(c) && r.getId() == id)));
if (firstTime != firstTime()) {
scheduleNext(true);
}
}

@ -0,0 +1,71 @@
package org.tasks.jobs;
import android.app.Notification;
import android.content.Intent;
import android.os.IBinder;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import java.util.List;
import javax.annotation.Nonnull;
import javax.inject.Inject;
import org.tasks.BuildConfig;
import org.tasks.Notifier;
import org.tasks.R;
import org.tasks.analytics.Tracker;
import org.tasks.injection.InjectingService;
import org.tasks.injection.ServiceComponent;
import org.tasks.notifications.NotificationManager;
import org.tasks.preferences.Preferences;
public class NotificationService extends InjectingService {
@Inject Preferences preferences;
@Inject Notifier notifier;
@Inject NotificationQueue notificationQueue;
@Inject Tracker tracker;
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
protected int getNotificationId() {
return -1;
}
@Override
protected Notification getNotification() {
return new NotificationCompat.Builder(
this, NotificationManager.NOTIFICATION_CHANNEL_MISCELLANEOUS)
.setSound(null)
.setSmallIcon(R.drawable.ic_check_white_24dp)
.setContentTitle(getString(R.string.app_name))
.setContentText(getString(R.string.building_notifications))
.build();
}
@Override
protected void doWork(@Nonnull Intent intent) {
try {
if (!preferences.isCurrentlyQuietHours()) {
List<? extends NotificationQueueEntry> overdueJobs = notificationQueue.getOverdueJobs();
notifier.triggerTaskNotifications(overdueJobs);
boolean success = notificationQueue.remove(overdueJobs);
if (BuildConfig.DEBUG && !success) {
throw new RuntimeException("Failed to remove jobs from queue");
}
}
} catch (Exception e) {
tracker.reportException(e);
} finally {
notificationQueue.scheduleNext();
}
}
@Override
protected void inject(ServiceComponent component) {
component.inject(this);
}
}

@ -1,46 +0,0 @@
package org.tasks.jobs;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.work.WorkerParameters;
import java.util.List;
import javax.inject.Inject;
import org.tasks.BuildConfig;
import org.tasks.Notifier;
import org.tasks.injection.JobComponent;
import org.tasks.preferences.Preferences;
public class NotificationWork extends RepeatingWorker {
@Inject Preferences preferences;
@Inject Notifier notifier;
@Inject NotificationQueue notificationQueue;
public NotificationWork(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
}
@NonNull
@Override
public Result run() {
if (!preferences.isCurrentlyQuietHours()) {
List<? extends NotificationQueueEntry> overdueJobs = notificationQueue.getOverdueJobs();
notifier.triggerTaskNotifications(overdueJobs);
boolean success = notificationQueue.remove(overdueJobs);
if (BuildConfig.DEBUG && !success) {
throw new RuntimeException("Failed to remove jobs from queue");
}
}
return Result.SUCCESS;
}
@Override
protected void inject(JobComponent component) {
component.inject(this);
}
@Override
protected void scheduleNext() {
notificationQueue.scheduleNext();
}
}

@ -1,10 +1,18 @@
package org.tasks.jobs;
import static com.todoroo.andlib.utility.AndroidUtilities.atLeastKitKat;
import static com.todoroo.andlib.utility.AndroidUtilities.atLeastMarshmallow;
import static com.todoroo.andlib.utility.AndroidUtilities.atLeastOreo;
import static com.todoroo.andlib.utility.DateUtilities.now;
import static org.tasks.date.DateTimeUtils.midnight;
import static org.tasks.date.DateTimeUtils.newDateTime;
import static org.tasks.time.DateTimeUtils.currentTimeMillis;
import static org.tasks.time.DateTimeUtils.printTimestamp;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import androidx.work.BackoffPolicy;
import androidx.work.Constraints;
@ -27,6 +35,7 @@ import org.tasks.R;
import org.tasks.data.CaldavDao;
import org.tasks.data.GoogleTaskListDao;
import org.tasks.injection.ApplicationScope;
import org.tasks.injection.ForApplication;
import org.tasks.preferences.Preferences;
import timber.log.Timber;
@ -36,21 +45,27 @@ public class WorkManager {
private static final String TAG_BACKUP = "tag_backup";
private static final String TAG_REFRESH = "tag_refresh";
private static final String TAG_MIDNIGHT_REFRESH = "tag_midnight_refresh";
private static final String TAG_NOTIFICATION = "tag_notification";
private static final String TAG_SYNC = "tag_sync";
private static final String TAG_BACKGROUND_SYNC = "tag_background_sync";
private final Context context;
private final Preferences preferences;
private final GoogleTaskListDao googleTaskListDao;
private final CaldavDao caldavDao;
private final AlarmManager alarmManager;
private androidx.work.WorkManager workManager;
@Inject
public WorkManager(
Preferences preferences, GoogleTaskListDao googleTaskListDao, CaldavDao caldavDao) {
@ForApplication Context context,
Preferences preferences,
GoogleTaskListDao googleTaskListDao,
CaldavDao caldavDao) {
this.context = context;
this.preferences = preferences;
this.googleTaskListDao = googleTaskListDao;
this.caldavDao = caldavDao;
alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
}
public void init() {
@ -138,7 +153,23 @@ public class WorkManager {
@SuppressWarnings("WeakerAccess")
public void scheduleNotification(long time) {
enqueueUnique(TAG_NOTIFICATION, NotificationWork.class, time);
if (time < currentTimeMillis()) {
Intent intent = getNotificationIntent();
if (atLeastOreo()) {
context.startForegroundService(intent);
} else {
context.startService(intent);
}
} else {
PendingIntent pendingIntent = getNotificationPendingIntent();
if (atLeastMarshmallow()) {
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, time, pendingIntent);
} else if (atLeastKitKat()) {
alarmManager.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent);
} else {
alarmManager.set(AlarmManager.RTC_WAKEUP, time, pendingIntent);
}
}
}
void scheduleBackup() {
@ -188,7 +219,7 @@ public class WorkManager {
@SuppressWarnings("WeakerAccess")
public void cancelNotifications() {
Timber.d("cancelNotifications");
workManager.cancelAllWorkByTag(TAG_NOTIFICATION);
alarmManager.cancel(getNotificationPendingIntent());
}
public void onStartup() {
@ -196,4 +227,15 @@ public class WorkManager {
scheduleMidnightRefresh();
scheduleBackup();
}
private Intent getNotificationIntent() {
return new Intent(context, NotificationService.class);
}
private PendingIntent getNotificationPendingIntent() {
Intent intent = getNotificationIntent();
return atLeastOreo()
? PendingIntent.getForegroundService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
: PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
}

@ -11,7 +11,7 @@ import org.tasks.analytics.Tracker;
import org.tasks.analytics.Tracking;
import org.tasks.injection.ForApplication;
import org.tasks.injection.InjectingJobIntentService;
import org.tasks.injection.IntentServiceComponent;
import org.tasks.injection.ServiceComponent;
import org.tasks.locale.bundle.ListNotificationBundle;
import org.tasks.locale.bundle.TaskCreationBundle;
import org.tasks.preferences.DefaultFilterProvider;
@ -49,7 +49,7 @@ public class TaskerIntentService extends InjectingJobIntentService {
}
@Override
protected void inject(IntentServiceComponent component) {
protected void inject(ServiceComponent component) {
component.inject(this);
}
}

@ -58,6 +58,7 @@ 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 = 40;
static final String EXTRA_NOTIFICATION_ID = "extra_notification_id";
static final int SUMMARY_NOTIFICATION_ID = 0;
@ -92,25 +93,30 @@ public class NotificationManager {
android.app.NotificationManager notificationManager =
(android.app.NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.createNotificationChannel(
createNotificationChannel(NOTIFICATION_CHANNEL_DEFAULT, R.string.notifications));
createNotificationChannel(NOTIFICATION_CHANNEL_DEFAULT, R.string.notifications, true));
notificationManager.createNotificationChannel(
createNotificationChannel(NOTIFICATION_CHANNEL_TASKER, R.string.tasker_locale));
createNotificationChannel(NOTIFICATION_CHANNEL_TASKER, R.string.tasker_locale, true));
notificationManager.createNotificationChannel(
createNotificationChannel(NOTIFICATION_CHANNEL_TIMERS, R.string.TEA_timer_controls));
createNotificationChannel(NOTIFICATION_CHANNEL_TIMERS, R.string.TEA_timer_controls, true));
notificationManager.createNotificationChannel(
createNotificationChannel(NOTIFICATION_CHANNEL_MISCELLANEOUS, R.string.miscellaneous, false));
}
}
@TargetApi(Build.VERSION_CODES.O)
private NotificationChannel createNotificationChannel(String channelId, int nameResId) {
private NotificationChannel createNotificationChannel(
String channelId, int nameResId, boolean alert) {
String channelName = context.getString(nameResId);
int importance =
alert
? android.app.NotificationManager.IMPORTANCE_HIGH
: android.app.NotificationManager.IMPORTANCE_LOW;
NotificationChannel notificationChannel =
new NotificationChannel(
channelId, channelName, android.app.NotificationManager.IMPORTANCE_HIGH);
notificationChannel.enableLights(true);
notificationChannel.enableVibration(true);
notificationChannel.setBypassDnd(true);
notificationChannel.setShowBadge(true);
notificationChannel.setImportance(android.app.NotificationManager.IMPORTANCE_HIGH);
new NotificationChannel(channelId, channelName, importance);
notificationChannel.enableLights(alert);
notificationChannel.enableVibration(alert);
notificationChannel.setBypassDnd(alert);
notificationChannel.setShowBadge(alert);
return notificationChannel;
}

@ -9,7 +9,7 @@ import javax.inject.Inject;
import org.tasks.files.FileHelper;
import org.tasks.injection.ForApplication;
import org.tasks.injection.InjectingJobIntentService;
import org.tasks.injection.IntentServiceComponent;
import org.tasks.injection.ServiceComponent;
import org.tasks.jobs.WorkManager;
import org.tasks.preferences.Preferences;
import timber.log.Timber;
@ -46,7 +46,7 @@ public class BackgroundScheduler extends InjectingJobIntentService {
}
@Override
protected void inject(IntentServiceComponent component) {
protected void inject(ServiceComponent component) {
component.inject(this);
}
}

@ -15,7 +15,7 @@ import org.tasks.calendars.AndroidCalendarEvent;
import org.tasks.calendars.CalendarEventProvider;
import org.tasks.injection.ForApplication;
import org.tasks.injection.InjectingJobIntentService;
import org.tasks.injection.IntentServiceComponent;
import org.tasks.injection.ServiceComponent;
import org.tasks.preferences.Preferences;
import timber.log.Timber;
@ -78,7 +78,7 @@ public class CalendarNotificationIntentService extends RecurringIntervalIntentSe
}
@Override
protected void inject(IntentServiceComponent component) {
protected void inject(ServiceComponent component) {
component.inject(this);
}

@ -5,7 +5,7 @@ import android.content.Intent;
import androidx.core.app.JobIntentService;
import javax.inject.Inject;
import org.tasks.injection.InjectingJobIntentService;
import org.tasks.injection.IntentServiceComponent;
import org.tasks.injection.ServiceComponent;
import org.tasks.location.GeofenceService;
import timber.log.Timber;
@ -30,7 +30,7 @@ public class GeofenceSchedulingIntentService extends InjectingJobIntentService {
}
@Override
protected void inject(IntentServiceComponent component) {
protected void inject(ServiceComponent component) {
component.inject(this);
}
}

@ -7,7 +7,7 @@ import com.todoroo.astrid.alarms.AlarmService;
import com.todoroo.astrid.reminders.ReminderService;
import javax.inject.Inject;
import org.tasks.injection.InjectingJobIntentService;
import org.tasks.injection.IntentServiceComponent;
import org.tasks.injection.ServiceComponent;
import org.tasks.jobs.NotificationQueue;
import org.tasks.notifications.NotificationManager;
import timber.log.Timber;
@ -46,7 +46,7 @@ public class NotificationSchedulerIntentService extends InjectingJobIntentServic
}
@Override
protected void inject(IntentServiceComponent component) {
protected void inject(ServiceComponent component) {
component.inject(this);
}
}

@ -876,4 +876,5 @@ File %1$s contained %2$s.\n\n
<string name="open_url">Open %s</string>
<string name="location_arrived">Arrived at %s</string>
<string name="location_departed">Departed %s</string>
<string name="building_notifications">Generating notifications</string>
</resources>

Loading…
Cancel
Save