From e57adab036de63ce2e853ee47d9a97e1911999a4 Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Wed, 23 Aug 2017 17:18:47 -0500 Subject: [PATCH] Bundle and persist notifications --- app/build.gradle | 14 +- app/schemas/org.tasks.db.AppDatabase/1.json | 60 ++++ .../tasks/injection/BroadcastComponent.java | 3 + .../astrid/reminders/NotificationTests.java | 2 +- .../java/org/tasks/injection/TestModule.java | 8 + .../java/org/tasks/jobs/JobQueueTest.java | 9 +- .../tasks/preferences/PreferenceTests.java | 2 +- .../gtasks/GtasksMetadataServiceTest.java | 2 +- .../tasks/injection/BroadcastComponent.java | 3 + .../tasks/gtasks/GoogleTaskSyncAdapter.java | 2 +- .../tasks/injection/BroadcastComponent.java | 3 + app/src/main/AndroidManifest.xml | 4 +- .../andlib/utility/AndroidUtilities.java | 4 + .../calls/PhoneStateChangedReceiver.java | 84 +++++- .../java/com/todoroo/astrid/dao/TaskDao.java | 4 + .../astrid/reminders/ReminderPreferences.java | 13 +- .../astrid/service/StartupService.java | 18 +- .../todoroo/astrid/timers/TimerPlugin.java | 2 +- app/src/main/java/org/tasks/Notifier.java | 266 ++++++------------ .../main/java/org/tasks/db/AppDatabase.java | 12 + .../tasks/injection/ApplicationModule.java | 8 + .../injection/IntentServiceComponent.java | 6 +- app/src/main/java/org/tasks/jobs/Alarm.java | 14 + .../main/java/org/tasks/jobs/JobManager.java | 4 +- .../main/java/org/tasks/jobs/JobQueue.java | 12 +- .../java/org/tasks/jobs/JobQueueEntry.java | 4 + .../java/org/tasks/jobs/NotificationJob.java | 22 +- .../main/java/org/tasks/jobs/Reminder.java | 13 + .../org/tasks/notifications/Notification.java | 34 +++ .../NotificationClearedReceiver.java | 32 +++ .../tasks/notifications/NotificationDao.java | 25 ++ .../notifications/NotificationManager.java | 133 ++++++++- .../preferences/MiscellaneousPreferences.java | 5 +- .../org/tasks/preferences/Preferences.java | 37 +-- .../receivers/BootCompletedReceiver.java | 9 +- .../receivers/MyPackageReplacedReceiver.java | 9 +- .../tasks/scheduling/BackgroundScheduler.java | 53 +++- .../CalendarNotificationIntentService.java | 6 +- .../GeofenceSchedulingIntentService.java | 9 +- .../NotificationSchedulerIntentService.java | 12 +- .../scheduling/SchedulerIntentService.java | 45 --- .../drawable-hdpi/ic_done_all_white_24dp.png | Bin 0 -> 275 bytes .../drawable-xhdpi/ic_done_all_white_24dp.png | Bin 0 -> 300 bytes .../ic_done_all_white_24dp.png | Bin 0 -> 398 bytes .../res/drawable/ic_done_all_white_24dp.png | Bin 0 -> 213 bytes app/src/main/res/values-fr/strings.xml | 1 - app/src/main/res/values/keys.xml | 1 - gradle/wrapper/gradle-wrapper.properties | 4 +- 48 files changed, 655 insertions(+), 358 deletions(-) create mode 100644 app/schemas/org.tasks.db.AppDatabase/1.json create mode 100644 app/src/main/java/org/tasks/db/AppDatabase.java create mode 100644 app/src/main/java/org/tasks/notifications/Notification.java create mode 100644 app/src/main/java/org/tasks/notifications/NotificationClearedReceiver.java create mode 100644 app/src/main/java/org/tasks/notifications/NotificationDao.java delete mode 100644 app/src/main/java/org/tasks/scheduling/SchedulerIntentService.java create mode 100644 app/src/main/res/drawable-hdpi/ic_done_all_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_done_all_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_done_all_white_24dp.png create mode 100644 app/src/main/res/drawable/ic_done_all_white_24dp.png diff --git a/app/build.gradle b/app/build.gradle index 926ea65d8..444d8d7dd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,7 +1,7 @@ apply plugin: 'com.android.application' task wrapper(type: Wrapper) { - gradleVersion = '3.3' + gradleVersion = '4.1' } buildscript { @@ -37,6 +37,12 @@ android { targetSdkVersion 26 minSdkVersion 15 testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + + javaCompileOptions { + annotationProcessorOptions { + arguments = ["room.schemaLocation": "$projectDir/schemas".toString()] + } + } } signingConfigs { @@ -96,12 +102,17 @@ final BUTTERKNIFE_VERSION = '8.8.1' final GPS_VERSION = '11.2.0' final SUPPORT_VERSION = '26.0.1' final STETHO_VERSION = '1.5.0' +final ROOM_VERSION = '1.0.0-alpha9' final TESTING_SUPPORT_VERSION = '1.0.0' dependencies { annotationProcessor "com.google.dagger:dagger-compiler:${DAGGER_VERSION}" compile "com.google.dagger:dagger:${DAGGER_VERSION}" + compile "android.arch.persistence.room:rxjava2:${ROOM_VERSION}" + annotationProcessor "android.arch.persistence.room:compiler:${ROOM_VERSION}" + compile "io.reactivex.rxjava2:rxandroid:2.0.1" + annotationProcessor "com.jakewharton:butterknife-compiler:${BUTTERKNIFE_VERSION}" compile "com.jakewharton:butterknife:${BUTTERKNIFE_VERSION}" @@ -110,7 +121,6 @@ dependencies { } debugCompile "com.facebook.stetho:stetho-timber:${STETHO_VERSION}@aar" debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1' - //noinspection GradleCompatible debugCompile 'com.android.support:multidex:1.0.2' compile 'com.github.rey5137:material:1.2.4' diff --git a/app/schemas/org.tasks.db.AppDatabase/1.json b/app/schemas/org.tasks.db.AppDatabase/1.json new file mode 100644 index 000000000..35988ac97 --- /dev/null +++ b/app/schemas/org.tasks.db.AppDatabase/1.json @@ -0,0 +1,60 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "eab7679fcfaa5fd45ac7da7a4b205348", + "entities": [ + { + "tableName": "notification", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `task` INTEGER, `timestamp` INTEGER NOT NULL, `type` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "taskId", + "columnName": "task", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_notification_task", + "unique": true, + "columnNames": [ + "task" + ], + "createSql": "CREATE UNIQUE INDEX `index_notification_task` ON `${TABLE_NAME}` (`task`)" + } + ], + "foreignKeys": [] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"eab7679fcfaa5fd45ac7da7a4b205348\")" + ] + } +} \ No newline at end of file diff --git a/app/src/amazon/java/org/tasks/injection/BroadcastComponent.java b/app/src/amazon/java/org/tasks/injection/BroadcastComponent.java index 1dd209ead..c09ed3800 100644 --- a/app/src/amazon/java/org/tasks/injection/BroadcastComponent.java +++ b/app/src/amazon/java/org/tasks/injection/BroadcastComponent.java @@ -8,6 +8,7 @@ import com.todoroo.astrid.repeats.RepeatTaskCompleteListener; import com.todoroo.astrid.timers.TimerTaskCompleteListener; import org.tasks.locale.receiver.FireReceiver; +import org.tasks.notifications.NotificationClearedReceiver; import org.tasks.receivers.BootCompletedReceiver; import org.tasks.receivers.CompleteTaskReceiver; import org.tasks.receivers.ListNotificationReceiver; @@ -47,4 +48,6 @@ public interface BroadcastComponent { void inject(TeslaUnreadReceiver teslaUnreadReceiver); void inject(PushReceiver pushReceiver); + + void inject(NotificationClearedReceiver notificationClearedReceiver); } diff --git a/app/src/androidTest/java/com/todoroo/astrid/reminders/NotificationTests.java b/app/src/androidTest/java/com/todoroo/astrid/reminders/NotificationTests.java index a66aecd12..f678c9ebf 100644 --- a/app/src/androidTest/java/com/todoroo/astrid/reminders/NotificationTests.java +++ b/app/src/androidTest/java/com/todoroo/astrid/reminders/NotificationTests.java @@ -93,7 +93,7 @@ public class NotificationTests extends DatabaseTestCase { notifier.triggerTaskNotification(task.getId(), ReminderService.TYPE_DUE); - verify(notificationManager).notify(eq((int) task.getId()), any(Notification.class)); + verify(notificationManager).notify(eq((int) task.getId()), any(Notification.class), true, false, false); } @Test diff --git a/app/src/androidTest/java/org/tasks/injection/TestModule.java b/app/src/androidTest/java/org/tasks/injection/TestModule.java index 38c06f6d8..55e267ef6 100644 --- a/app/src/androidTest/java/org/tasks/injection/TestModule.java +++ b/app/src/androidTest/java/org/tasks/injection/TestModule.java @@ -1,10 +1,12 @@ package org.tasks.injection; +import android.arch.persistence.room.Room; import android.content.Context; import com.todoroo.astrid.dao.Database; import org.tasks.analytics.Tracker; +import org.tasks.db.AppDatabase; import org.tasks.preferences.PermissionChecker; import org.tasks.preferences.PermissivePermissionChecker; @@ -32,6 +34,12 @@ public class TestModule { }; } + @Provides + @ApplicationScope + public AppDatabase getAppDatabase() { + return Room.databaseBuilder(context, AppDatabase.class, "test-app-database").build(); + } + @ApplicationScope @Provides @ForApplication diff --git a/app/src/androidTest/java/org/tasks/jobs/JobQueueTest.java b/app/src/androidTest/java/org/tasks/jobs/JobQueueTest.java index e72febf0c..e16859b76 100644 --- a/app/src/androidTest/java/org/tasks/jobs/JobQueueTest.java +++ b/app/src/androidTest/java/org/tasks/jobs/JobQueueTest.java @@ -75,7 +75,7 @@ public class JobQueueTest { verify(jobManager).schedule(TAG, now); - queue.remove(new Alarm(1, 1, now)); + queue.remove(singletonList(new Alarm(1, 1, now))); Freeze.freezeAt(now).thawAfter(new Snippet() {{ assertEquals( @@ -93,7 +93,7 @@ public class JobQueueTest { verify(jobManager).schedule(TAG, now); - queue.remove(new Reminder(1, now, TYPE_DUE)); + queue.remove(singletonList(new Reminder(1, now, TYPE_DUE))); Freeze.freezeAt(now).thawAfter(new Snippet() {{ assertEquals( @@ -242,7 +242,7 @@ public class JobQueueTest { verify(jobManager).schedule(TAG, now); Freeze.freezeAt(now).thawAfter(new Snippet() {{ - queue.remove(new Reminder(1, now, TYPE_DUE)); + queue.remove(queue.getOverdueJobs()); }}); assertEquals( @@ -261,8 +261,7 @@ public class JobQueueTest { verify(jobManager).schedule(TAG, now); Freeze.freezeAt(now + ONE_MINUTE).thawAfter(new Snippet() {{ - queue.remove(new Reminder(1, now, TYPE_DUE)); - queue.remove(new Reminder(2, now + ONE_MINUTE, TYPE_DUE)); + queue.remove(queue.getOverdueJobs()); }}); assertEquals( diff --git a/app/src/androidTest/java/org/tasks/preferences/PreferenceTests.java b/app/src/androidTest/java/org/tasks/preferences/PreferenceTests.java index 29c9f9f08..8b68356f4 100644 --- a/app/src/androidTest/java/org/tasks/preferences/PreferenceTests.java +++ b/app/src/androidTest/java/org/tasks/preferences/PreferenceTests.java @@ -24,7 +24,7 @@ public class PreferenceTests { @Before public void setUp() { - preferences = new Preferences(getTargetContext(), null, null); + preferences = new Preferences(getTargetContext(), null); preferences.clear(); preferences.setBoolean(R.string.p_rmd_enable_quiet, true); } diff --git a/app/src/androidTestGoogleplay/java/com/todoroo/astrid/gtasks/GtasksMetadataServiceTest.java b/app/src/androidTestGoogleplay/java/com/todoroo/astrid/gtasks/GtasksMetadataServiceTest.java index d0d654061..ea17b83e7 100644 --- a/app/src/androidTestGoogleplay/java/com/todoroo/astrid/gtasks/GtasksMetadataServiceTest.java +++ b/app/src/androidTestGoogleplay/java/com/todoroo/astrid/gtasks/GtasksMetadataServiceTest.java @@ -38,7 +38,7 @@ public class GtasksMetadataServiceTest extends DatabaseTestCase { private final GtasksTestPreferenceService service; public GtasksMetadataServiceTestModule(Context context) { - service = new GtasksTestPreferenceService(new Preferences(context, null, null)); + service = new GtasksTestPreferenceService(new Preferences(context, null)); } @Provides diff --git a/app/src/generic/java/org/tasks/injection/BroadcastComponent.java b/app/src/generic/java/org/tasks/injection/BroadcastComponent.java index 1dd209ead..c09ed3800 100644 --- a/app/src/generic/java/org/tasks/injection/BroadcastComponent.java +++ b/app/src/generic/java/org/tasks/injection/BroadcastComponent.java @@ -8,6 +8,7 @@ import com.todoroo.astrid.repeats.RepeatTaskCompleteListener; import com.todoroo.astrid.timers.TimerTaskCompleteListener; import org.tasks.locale.receiver.FireReceiver; +import org.tasks.notifications.NotificationClearedReceiver; import org.tasks.receivers.BootCompletedReceiver; import org.tasks.receivers.CompleteTaskReceiver; import org.tasks.receivers.ListNotificationReceiver; @@ -47,4 +48,6 @@ public interface BroadcastComponent { void inject(TeslaUnreadReceiver teslaUnreadReceiver); void inject(PushReceiver pushReceiver); + + void inject(NotificationClearedReceiver notificationClearedReceiver); } diff --git a/app/src/googleplay/java/org/tasks/gtasks/GoogleTaskSyncAdapter.java b/app/src/googleplay/java/org/tasks/gtasks/GoogleTaskSyncAdapter.java index f2948fefc..9065fd934 100644 --- a/app/src/googleplay/java/org/tasks/gtasks/GoogleTaskSyncAdapter.java +++ b/app/src/googleplay/java/org/tasks/gtasks/GoogleTaskSyncAdapter.java @@ -160,7 +160,7 @@ public class GoogleTaskSyncAdapter extends InjectingAbstractThreadedSyncAdapter .setAutoCancel(true) .setSmallIcon(android.R.drawable.ic_dialog_alert) .setTicker(context.getString(R.string.common_google_play_services_notification_ticker)); - notificationManager.notify(Constants.NOTIFICATION_SYNC_ERROR, builder.build()); + notificationManager.notify(Constants.NOTIFICATION_SYNC_ERROR, builder.build(), true, false, false); } private void synchronize() throws IOException { diff --git a/app/src/googleplay/java/org/tasks/injection/BroadcastComponent.java b/app/src/googleplay/java/org/tasks/injection/BroadcastComponent.java index d82e3a865..350ec3485 100644 --- a/app/src/googleplay/java/org/tasks/injection/BroadcastComponent.java +++ b/app/src/googleplay/java/org/tasks/injection/BroadcastComponent.java @@ -8,6 +8,7 @@ import com.todoroo.astrid.repeats.RepeatTaskCompleteListener; import com.todoroo.astrid.timers.TimerTaskCompleteListener; import org.tasks.locale.receiver.FireReceiver; +import org.tasks.notifications.NotificationClearedReceiver; import org.tasks.receivers.BootCompletedReceiver; import org.tasks.receivers.CompleteTaskReceiver; import org.tasks.receivers.GoogleTaskPushReceiver; @@ -50,4 +51,6 @@ public interface BroadcastComponent { void inject(TeslaUnreadReceiver teslaUnreadReceiver); void inject(PushReceiver pushReceiver); + + void inject(NotificationClearedReceiver notificationClearedReceiver); } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3e6553f3a..c3c4313a5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -424,7 +424,7 @@ android:permission="android.permission.BIND_JOB_SERVICE" android:exported="false" /> + + = Build.VERSION_CODES.M; } + public static boolean atLeastNougat() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N; + } + public static boolean atLeastOreo() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; } diff --git a/app/src/main/java/com/todoroo/astrid/calls/PhoneStateChangedReceiver.java b/app/src/main/java/com/todoroo/astrid/calls/PhoneStateChangedReceiver.java index edb3c8f0c..ba40423ff 100644 --- a/app/src/main/java/com/todoroo/astrid/calls/PhoneStateChangedReceiver.java +++ b/app/src/main/java/com/todoroo/astrid/calls/PhoneStateChangedReceiver.java @@ -5,28 +5,41 @@ */ package com.todoroo.astrid.calls; +import android.annotation.SuppressLint; +import android.app.PendingIntent; +import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.net.Uri; import android.provider.CallLog.Calls; import android.provider.ContactsContract; +import android.support.v4.app.NotificationCompat; import android.telephony.TelephonyManager; import android.text.TextUtils; import com.todoroo.andlib.utility.AndroidUtilities; import com.todoroo.andlib.utility.DateUtilities; -import org.tasks.Notifier; +import org.tasks.R; import org.tasks.injection.BroadcastComponent; +import org.tasks.injection.ForApplication; import org.tasks.injection.InjectingBroadcastReceiver; +import org.tasks.notifications.NotificationManager; import org.tasks.preferences.PermissionChecker; import org.tasks.preferences.Preferences; +import org.tasks.reminders.MissedCallActivity; + +import java.io.InputStream; import javax.inject.Inject; import timber.log.Timber; +import static org.tasks.time.DateTimeUtils.currentTimeMillis; + public class PhoneStateChangedReceiver extends InjectingBroadcastReceiver { private static final String PREF_LAST_INCOMING_NUMBER = "last_incoming_number"; @@ -34,8 +47,9 @@ public class PhoneStateChangedReceiver extends InjectingBroadcastReceiver { private static final long WAIT_BEFORE_READ_LOG = 3000L; @Inject Preferences preferences; - @Inject Notifier notifier; + @Inject NotificationManager notificationManager; @Inject PermissionChecker permissionChecker; + @Inject @ForApplication Context context; @Override public void onReceive(final Context context, Intent intent) { @@ -73,12 +87,12 @@ public class PhoneStateChangedReceiver extends InjectingBroadcastReceiver { AndroidUtilities.sleepDeep(WAIT_BEFORE_READ_LOG); Cursor calls; try { - calls = getMissedCalls(context); + calls = getMissedCalls(); } catch (Exception e) { // Sometimes database is locked, retry once Timber.e(e, e.getMessage()); AndroidUtilities.sleepDeep(300L); try { - calls = getMissedCalls(context); + calls = getMissedCalls(); } catch (Exception e2) { Timber.e(e2, e2.getMessage()); calls = null; @@ -113,7 +127,7 @@ public class PhoneStateChangedReceiver extends InjectingBroadcastReceiver { long contactId = getContactIdFromNumber(context, number); - notifier.triggerMissedCallNotification(name, number, contactId); + triggerMissedCallNotification(name, number, contactId); } } catch (Exception e) { Timber.e(e, e.getMessage()); @@ -127,7 +141,8 @@ public class PhoneStateChangedReceiver extends InjectingBroadcastReceiver { } } - private Cursor getMissedCalls(Context context) { + @SuppressLint("MissingPermission") + private Cursor getMissedCalls() { if (permissionChecker.canAccessMissedCallPermissions()) { //noinspection MissingPermission return context.getContentResolver().query( @@ -174,4 +189,61 @@ public class PhoneStateChangedReceiver extends InjectingBroadcastReceiver { return -1; } + public void triggerMissedCallNotification(final String name, final String number, long contactId) { + final String title = context.getString(R.string.missed_call, TextUtils.isEmpty(name) ? number : name); + + Intent missedCallDialog = new Intent(context, MissedCallActivity.class); + missedCallDialog.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + missedCallDialog.putExtra(MissedCallActivity.EXTRA_NUMBER, number); + missedCallDialog.putExtra(MissedCallActivity.EXTRA_NAME, name); + missedCallDialog.putExtra(MissedCallActivity.EXTRA_TITLE, title); + + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NotificationManager.NOTIFICATION_CHANNEL_CALLS) + .setTicker(title) + .setContentTitle(title) + .setContentText(context.getString(R.string.app_name)) + .setWhen(currentTimeMillis()) + .setShowWhen(true) + .setSmallIcon(R.drawable.ic_check_white_24dp) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setContentIntent(PendingIntent.getActivity(context, missedCallDialog.hashCode(), missedCallDialog, PendingIntent.FLAG_UPDATE_CURRENT)); + + Bitmap contactImage = getContactImage(contactId); + if (contactImage != null) { + builder.setLargeIcon(contactImage); + } + + Intent callNow = new Intent(context, MissedCallActivity.class); + callNow.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + callNow.putExtra(MissedCallActivity.EXTRA_NUMBER, number); + callNow.putExtra(MissedCallActivity.EXTRA_NAME, name); + callNow.putExtra(MissedCallActivity.EXTRA_TITLE, title); + callNow.putExtra(MissedCallActivity.EXTRA_CALL_NOW, true); + + Intent callLater = new Intent(context, MissedCallActivity.class); + callLater.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + callLater.putExtra(MissedCallActivity.EXTRA_NUMBER, number); + callLater.putExtra(MissedCallActivity.EXTRA_NAME, name); + callLater.putExtra(MissedCallActivity.EXTRA_TITLE, title); + callLater.putExtra(MissedCallActivity.EXTRA_CALL_LATER, true); + builder + .addAction(R.drawable.ic_phone_white_24dp, context.getString(R.string.MCA_return_call), PendingIntent.getActivity(context, callNow.hashCode(), callNow, PendingIntent.FLAG_UPDATE_CURRENT)) + .addAction(R.drawable.ic_add_white_24dp, context.getString(R.string.MCA_add_task), PendingIntent.getActivity(context, callLater.hashCode(), callLater, PendingIntent.FLAG_UPDATE_CURRENT)); + + notificationManager.notify(number.hashCode(), builder.build(), true, false, false); + } + + private Bitmap getContactImage(long contactId) { + Bitmap b = null; + if (contactId >= 0) { + Uri uri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, contactId); + InputStream input = ContactsContract.Contacts.openContactPhotoInputStream(context.getContentResolver(), uri); + try { + b = BitmapFactory.decodeStream(input); + } catch (OutOfMemoryError e) { + Timber.e(e, e.getMessage()); + } + } + return b; + } } diff --git a/app/src/main/java/com/todoroo/astrid/dao/TaskDao.java b/app/src/main/java/com/todoroo/astrid/dao/TaskDao.java index 05be366c5..673d2f6ae 100644 --- a/app/src/main/java/com/todoroo/astrid/dao/TaskDao.java +++ b/app/src/main/java/com/todoroo/astrid/dao/TaskDao.java @@ -95,6 +95,10 @@ public class TaskDao { dao.query(Query.select(Task.PROPERTIES).where(Criterion.and(TaskCriteria.isActive(), criterion)), callback); } + public Task fetch(long id) { + return dao.fetch(id, Task.PROPERTIES); + } + public Task fetch(long id, Property... properties) { return dao.fetch(id, properties); } diff --git a/app/src/main/java/com/todoroo/astrid/reminders/ReminderPreferences.java b/app/src/main/java/com/todoroo/astrid/reminders/ReminderPreferences.java index 027ad8b66..e69a943cb 100644 --- a/app/src/main/java/com/todoroo/astrid/reminders/ReminderPreferences.java +++ b/app/src/main/java/com/todoroo/astrid/reminders/ReminderPreferences.java @@ -17,14 +17,11 @@ import android.preference.Preference; import android.preference.PreferenceManager; import android.provider.Settings; import android.support.annotation.NonNull; -import android.support.v4.app.JobIntentService; import org.tasks.R; import org.tasks.activities.TimePickerActivity; import org.tasks.injection.ActivityComponent; import org.tasks.injection.InjectingPreferenceActivity; -import org.tasks.jobs.JobManager; -import org.tasks.notifications.NotificationManager; import org.tasks.preferences.ActivityPermissionRequestor; import org.tasks.preferences.Device; import org.tasks.preferences.PermissionChecker; @@ -66,7 +63,8 @@ public class ReminderPreferences extends InjectingPreferenceActivity { R.string.p_rmd_time, R.string.p_rmd_enable_quiet, R.string.p_rmd_quietStart, - R.string.p_rmd_quietEnd); + R.string.p_rmd_quietEnd, + R.string.p_rmd_persistent); resetGeofencesOnChange( R.string.p_geofence_radius, R.string.p_geofence_responsiveness); @@ -89,8 +87,7 @@ public class ReminderPreferences extends InjectingPreferenceActivity { @TargetApi(Build.VERSION_CODES.O) private boolean openNotificationChannelSettings(Preference ignored) { - Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS); - intent.putExtra(Settings.EXTRA_CHANNEL_ID, NotificationManager.DEFAULT_NOTIFICATION_CHANNEL); + Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS); intent.putExtra(Settings.EXTRA_APP_PACKAGE, ReminderPreferences.this.getPackageName()); startActivity(intent); return true; @@ -99,7 +96,7 @@ public class ReminderPreferences extends InjectingPreferenceActivity { private void rescheduleNotificationsOnChange(int... resIds) { for (int resId : resIds) { findPreference(getString(resId)).setOnPreferenceChangeListener((preference, newValue) -> { - JobIntentService.enqueueWork(this, NotificationSchedulerIntentService.class, JobManager.JOB_ID_NOTIFICATION_SCHEDULER, new Intent()); + NotificationSchedulerIntentService.enqueueWork(this); return true; }); } @@ -108,7 +105,7 @@ public class ReminderPreferences extends InjectingPreferenceActivity { private void resetGeofencesOnChange(int... resIds) { for (int resId : resIds) { findPreference(getString(resId)).setOnPreferenceChangeListener((preference, newValue) -> { - JobIntentService.enqueueWork(this, GeofenceSchedulingIntentService.class, JobManager.JOB_ID_GEOFENCE_SCHEDULING, new Intent()); + GeofenceSchedulingIntentService.enqueueWork(this); return true; }); } diff --git a/app/src/main/java/com/todoroo/astrid/service/StartupService.java b/app/src/main/java/com/todoroo/astrid/service/StartupService.java index d739886c0..40d6aa105 100644 --- a/app/src/main/java/com/todoroo/astrid/service/StartupService.java +++ b/app/src/main/java/com/todoroo/astrid/service/StartupService.java @@ -6,7 +6,6 @@ package com.todoroo.astrid.service; import android.content.Context; -import android.content.Intent; import android.database.sqlite.SQLiteException; import android.os.Environment; @@ -15,7 +14,6 @@ import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ListMultimap; import com.google.common.collect.Multimaps; import com.todoroo.andlib.sql.Criterion; -import com.todoroo.astrid.api.AstridApiConstants; import com.todoroo.astrid.dao.Database; import com.todoroo.astrid.dao.MetadataDao; import com.todoroo.astrid.dao.TagDataDao; @@ -52,14 +50,14 @@ public class StartupService { private final TagDataDao tagDataDao; private final TagService tagService; private final MetadataDao metadataDao; - private final BackgroundScheduler backgroundScheduler; private final LocalBroadcastManager localBroadcastManager; + private final Context context; @Inject public StartupService(Database database, Preferences preferences, TaskDeleter taskDeleter, Tracker tracker, TagDataDao tagDataDao, TagService tagService, - MetadataDao metadataDao, BackgroundScheduler backgroundScheduler, - LocalBroadcastManager localBroadcastManager) { + MetadataDao metadataDao, LocalBroadcastManager localBroadcastManager, + @ForApplication Context context) { this.database = database; this.preferences = preferences; this.taskDeleter = taskDeleter; @@ -67,8 +65,8 @@ public class StartupService { this.tagDataDao = tagDataDao; this.tagService = tagService; this.metadataDao = metadataDao; - this.backgroundScheduler = backgroundScheduler; this.localBroadcastManager = localBroadcastManager; + this.context = context; } /** Called when this application is started up */ @@ -97,12 +95,10 @@ public class StartupService { preferences.setDefaults(); } - // perform startup activities in a background thread - new Thread(() -> { - taskDeleter.deleteTasksWithEmptyTitles(null); - }).start(); + BackgroundScheduler.enqueueWork(context); - backgroundScheduler.scheduleEverything(); + // perform startup activities in a background thread + new Thread(() -> taskDeleter.deleteTasksWithEmptyTitles(null)).start(); } private void upgrade(int from, int to) { diff --git a/app/src/main/java/com/todoroo/astrid/timers/TimerPlugin.java b/app/src/main/java/com/todoroo/astrid/timers/TimerPlugin.java index 460c6e074..02a37e036 100644 --- a/app/src/main/java/com/todoroo/astrid/timers/TimerPlugin.java +++ b/app/src/main/java/com/todoroo/astrid/timers/TimerPlugin.java @@ -115,7 +115,7 @@ public class TimerPlugin { .setAutoCancel(false) .setOngoing(true) .build(); - notificationManager.notify(Constants.NOTIFICATION_TIMER, notification); + notificationManager.notify(Constants.NOTIFICATION_TIMER, notification, false, false, false); } } } diff --git a/app/src/main/java/org/tasks/Notifier.java b/app/src/main/java/org/tasks/Notifier.java index 7ebfe07e8..6a9b873af 100644 --- a/app/src/main/java/org/tasks/Notifier.java +++ b/app/src/main/java/org/tasks/Notifier.java @@ -1,15 +1,9 @@ package org.tasks; -import android.annotation.SuppressLint; import android.app.Notification; import android.app.PendingIntent; -import android.content.ContentUris; import android.content.Context; import android.content.Intent; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.net.Uri; -import android.provider.ContactsContract; import android.support.v4.app.NotificationCompat; import android.text.TextUtils; @@ -23,19 +17,23 @@ import com.todoroo.astrid.data.Task; import com.todoroo.astrid.reminders.ReminderService; import com.todoroo.astrid.voice.VoiceOutputAssistant; +import org.tasks.db.AppDatabase; import org.tasks.injection.ForApplication; +import org.tasks.jobs.JobQueueEntry; import org.tasks.notifications.AudioManager; import org.tasks.notifications.NotificationManager; import org.tasks.notifications.TelephonyManager; import org.tasks.preferences.Preferences; import org.tasks.receivers.CompleteTaskReceiver; -import org.tasks.reminders.MissedCallActivity; import org.tasks.reminders.NotificationActivity; import org.tasks.reminders.SnoozeActivity; import org.tasks.reminders.SnoozeDialog; import org.tasks.reminders.SnoozeOption; -import java.io.InputStream; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import javax.inject.Inject; @@ -43,13 +41,13 @@ import timber.log.Timber; import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; -import static com.google.common.base.Strings.isNullOrEmpty; +import static com.google.common.collect.Lists.transform; +import static com.todoroo.andlib.utility.AndroidUtilities.atLeastNougat; +import static org.tasks.notifications.NotificationManager.GROUP_KEY; import static org.tasks.time.DateTimeUtils.currentTimeMillis; public class Notifier { - private static long lastNotificationSound = 0L; - private final Context context; private final TaskDao taskDao; private final NotificationManager notificationManager; @@ -57,12 +55,13 @@ public class Notifier { private final AudioManager audioManager; private final VoiceOutputAssistant voiceOutputAssistant; private final Preferences preferences; + private final AppDatabase appDatabase; @Inject public Notifier(@ForApplication Context context, TaskDao taskDao, NotificationManager notificationManager, TelephonyManager telephonyManager, AudioManager audioManager, VoiceOutputAssistant voiceOutputAssistant, - Preferences preferences) { + Preferences preferences, AppDatabase appDatabase) { this.context = context; this.taskDao = taskDao; this.notificationManager = notificationManager; @@ -70,65 +69,7 @@ public class Notifier { this.audioManager = audioManager; this.voiceOutputAssistant = voiceOutputAssistant; this.preferences = preferences; - } - - public void triggerMissedCallNotification(final String name, final String number, long contactId) { - final String title = context.getString(R.string.missed_call, TextUtils.isEmpty(name) ? number : name); - - Intent missedCallDialog = new Intent(context, MissedCallActivity.class); - missedCallDialog.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - missedCallDialog.putExtra(MissedCallActivity.EXTRA_NUMBER, number); - missedCallDialog.putExtra(MissedCallActivity.EXTRA_NAME, name); - missedCallDialog.putExtra(MissedCallActivity.EXTRA_TITLE, title); - - NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NotificationManager.DEFAULT_NOTIFICATION_CHANNEL) - .setSmallIcon(R.drawable.ic_check_white_24dp) - .setTicker(title) - .setContentTitle(title) - .setContentText(context.getString(R.string.app_name)) - .setWhen(currentTimeMillis()) - .setVibrate(preferences.isVibrationEnabled() ? preferences.getVibrationPattern() : null) - .setLights(preferences.getLEDColor(), preferences.isLEDNotificationEnabled() ? 700 : 0, 5000) - .setPriority(preferences.getNotificationPriority()) - .setContentIntent(PendingIntent.getActivity(context, missedCallDialog.hashCode(), missedCallDialog, PendingIntent.FLAG_UPDATE_CURRENT)); - - Bitmap contactImage = getContactImage(contactId); - if (contactImage != null) { - builder.setLargeIcon(contactImage); - } - - Intent callNow = new Intent(context, MissedCallActivity.class); - callNow.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - callNow.putExtra(MissedCallActivity.EXTRA_NUMBER, number); - callNow.putExtra(MissedCallActivity.EXTRA_NAME, name); - callNow.putExtra(MissedCallActivity.EXTRA_TITLE, title); - callNow.putExtra(MissedCallActivity.EXTRA_CALL_NOW, true); - - Intent callLater = new Intent(context, MissedCallActivity.class); - callLater.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - callLater.putExtra(MissedCallActivity.EXTRA_NUMBER, number); - callLater.putExtra(MissedCallActivity.EXTRA_NAME, name); - callLater.putExtra(MissedCallActivity.EXTRA_TITLE, title); - callLater.putExtra(MissedCallActivity.EXTRA_CALL_LATER, true); - builder - .addAction(R.drawable.ic_phone_white_24dp, context.getString(R.string.MCA_return_call), PendingIntent.getActivity(context, callNow.hashCode(), callNow, PendingIntent.FLAG_UPDATE_CURRENT)) - .addAction(R.drawable.ic_add_white_24dp, context.getString(R.string.MCA_add_task), PendingIntent.getActivity(context, callLater.hashCode(), callLater, PendingIntent.FLAG_UPDATE_CURRENT)); - - activateNotification(1, number.hashCode(), builder.build(), null); - } - - private Bitmap getContactImage(long contactId) { - Bitmap b = null; - if (contactId >= 0) { - Uri uri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, contactId); - InputStream input = ContactsContract.Contacts.openContactPhotoInputStream(context.getContentResolver(), uri); - try { - b = BitmapFactory.decodeStream(input); - } catch (OutOfMemoryError e) { - Timber.e(e, e.getMessage()); - } - } - return b; + this.appDatabase = appDatabase; } public void triggerFilterNotification(final Filter filter) { @@ -146,56 +87,56 @@ public class Notifier { intent.putExtra(TaskListActivity.OPEN_FILTER, filter); PendingIntent pendingIntent = PendingIntent.getActivity(context, (title + query).hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT); - Notification notification = new NotificationCompat.Builder(context, NotificationManager.DEFAULT_NOTIFICATION_CHANNEL) - .setSmallIcon(R.drawable.ic_check_white_24dp) + NotificationCompat.Builder notification = new NotificationCompat.Builder(context, NotificationManager.NOTIFICATION_CHANNEL_TASKER) .setCategory(NotificationCompat.CATEGORY_REMINDER) .setTicker(title) - .setWhen(currentTimeMillis()) .setContentTitle(title) .setContentText(subtitle) .setContentIntent(pendingIntent) .setAutoCancel(true) - .setVibrate(preferences.isVibrationEnabled() ? preferences.getVibrationPattern() : null) - .setLights(preferences.getLEDColor(), preferences.isLEDNotificationEnabled() ? 700 : 0, 5000) - .setPriority(preferences.getNotificationPriority()) - .build(); - - activateNotification(1, (title + query).hashCode(), notification, null); + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setWhen(currentTimeMillis()) + .setShowWhen(true); + + notificationManager.notify( + (title + query).hashCode(), + notification.build(), + true, + false, + false); } public void triggerTaskNotification(long id, int type) { - if (!showNotification(id, type)) { - notificationManager.cancel(id); - } + org.tasks.notifications.Notification notification = new org.tasks.notifications.Notification(); + notification.taskId = id; + notification.type = type; + notification.timestamp = currentTimeMillis(); + triggerNotifications(Collections.singletonList(notification), true); } - private boolean showNotification(final long id, final int type) { - Task task; - try { - task = taskDao.fetch(id, Task.ID, Task.TITLE, Task.HIDE_UNTIL, Task.COMPLETION_DATE, - Task.DUE_DATE, Task.DELETION_DATE, Task.REMINDER_FLAGS, Task.NOTES); - if (task == null) { - throw new IllegalArgumentException("cound not find item with id"); //$NON-NLS-1$ - } - - } catch (Exception e) { - Timber.e(e, e.getMessage()); - return false; + private NotificationCompat.Builder getTaskNotification(org.tasks.notifications.Notification notification) { + long id = notification.taskId; + int type = notification.type; + long when = notification.timestamp; + Task task = taskDao.fetch(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 false; + return null; } // new task edit in progress if (TextUtils.isEmpty(task.getTitle())) { - return false; + return null; } // it's hidden - don't sound, don't delete if (task.isHidden() && type == ReminderService.TYPE_RANDOM) { - return false; + return null; } // task due date was changed, but alarm wasn't rescheduled @@ -203,21 +144,18 @@ public class Notifier { !task.hasDueTime() && task.getDueDate() - DateUtilities.now() > DateUtilities.ONE_DAY; if ((type == ReminderService.TYPE_DUE || type == ReminderService.TYPE_OVERDUE) && (!task.hasDueDate() || dueInFuture)) { - return false; + return null; } // read properties final String taskTitle = task.getTitle(); final String taskDescription = task.getNotes(); - boolean nonstopMode = task.isNotifyModeNonstop(); - boolean ringFiveMode = task.isNotifyModeFive(); - int ringTimes = nonstopMode ? -1 : (ringFiveMode ? 5 : 1); // update last reminder time - task.setReminderLast(DateUtilities.now()); + task.setReminderLast(when); taskDao.saveExisting(task); - final String text = context.getString(R.string.app_name); + final String appName = context.getString(R.string.app_name); final Intent intent = new Intent(context, NotificationActivity.class); intent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK); @@ -225,22 +163,18 @@ public class Notifier { intent.putExtra(NotificationActivity.EXTRA_TASK_ID, id); intent.putExtra(NotificationActivity.EXTRA_TITLE, taskTitle); - // don't ring multiple times if random reminder - if (type == ReminderService.TYPE_RANDOM) { - ringTimes = 1; - } - - NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NotificationManager.DEFAULT_NOTIFICATION_CHANNEL) - .setSmallIcon(R.drawable.ic_check_white_24dp) + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NotificationManager.NOTIFICATION_CHANNEL_DEFAULT) .setCategory(NotificationCompat.CATEGORY_REMINDER) .setTicker(taskTitle) - .setWhen(currentTimeMillis()) .setContentTitle(taskTitle) - .setContentText(text) - .setVibrate(preferences.isVibrationEnabled() ? preferences.getVibrationPattern() : null) - .setLights(preferences.getLEDColor(), preferences.isLEDNotificationEnabled() ? 700 : 0, 5000) - .setPriority(preferences.getNotificationPriority()) + .setContentText(appName) + .setGroup(GROUP_KEY) + .setSmallIcon(R.drawable.ic_check_white_24dp) + .setWhen(when) + .setShowWhen(true) + .setPriority(NotificationCompat.PRIORITY_HIGH) .setContentIntent(PendingIntent.getActivity(context, (int) id, intent, PendingIntent.FLAG_UPDATE_CURRENT)); + if (!Strings.isNullOrEmpty(taskDescription)) { builder.setStyle(new NotificationCompat.BigTextStyle().bigText(taskDescription)); } @@ -271,79 +205,59 @@ public class Notifier { .build()); } - builder.addAction(completeAction) + return builder.addAction(completeAction) .addAction(R.drawable.ic_snooze_white_24dp, context.getResources().getString(R.string.rmd_NoA_snooze), snoozePendingIntent) .extend(wearableExtender); - - activateNotification(ringTimes, (int) id, builder.build(), taskTitle); - - return true; } - @SuppressLint("NewApi") - private void activateNotification(int ringTimes, int notificationId, Notification notification, String text) { - if (preferences.getBoolean(R.string.p_rmd_persistent, true)) { - notification.flags |= Notification.FLAG_NO_CLEAR; - } - - boolean voiceReminder = preferences.getBoolean(R.string.p_voiceRemindersEnabled, false) && !isNullOrEmpty(text); + public void restoreNotifications() { + triggerNotifications(appDatabase.notificationDao().getAll(), false); + } - if (ringTimes != 1) { - notification.audioStreamType = android.media.AudioManager.STREAM_ALARM; + public void triggerTaskNotifications(List entries) { + triggerNotifications(transform(entries, JobQueueEntry::toNotification), true); + } - // insistent rings until notification is disabled - if (ringTimes < 0) { - notification.flags |= Notification.FLAG_INSISTENT; - voiceReminder = false; + public void triggerNotifications(List entries, boolean alert) { + Map notifications = new LinkedHashMap<>(); + boolean ringFiveTimes = false; + boolean ringNonstop = false; + for (int i = 0 ; i < entries.size() ; i++) { + org.tasks.notifications.Notification entry = entries.get(i); + Task task = taskDao.fetch(entry.taskId); + if (task == null) { + continue; } - - } else { - notification.audioStreamType = android.media.AudioManager.STREAM_NOTIFICATION; - } - - boolean soundIntervalOk = checkLastNotificationSound(); - - if (telephonyManager.callStateIdle()) { - String notificationPreference = preferences.getStringValue(R.string.p_rmd_ringtone); - if (audioManager.notificationsMuted()) { - notification.sound = null; - voiceReminder = false; - } else if (notificationPreference != null) { - if (notificationPreference.length() > 0 && soundIntervalOk) { - notification.sound = Uri.parse(notificationPreference); - } else { - notification.sound = null; - } - } else if (soundIntervalOk) { - notification.defaults |= Notification.DEFAULT_SOUND; + if (entry.type != ReminderService.TYPE_RANDOM) { + ringFiveTimes |= task.isNotifyModeFive(); + ringNonstop |= task.isNotifyModeNonstop(); + } + NotificationCompat.Builder notification = getTaskNotification(entry); + if (notification != null) { + notification.setGroupAlertBehavior(alert && (atLeastNougat() ? entries.size() == 1 : i == entries.size() - 1) + ? NotificationCompat.GROUP_ALERT_CHILDREN + : NotificationCompat.GROUP_ALERT_SUMMARY); + notifications.put(entry, notification.build()); } } - if (!telephonyManager.callStateIdle()) { - notification.sound = null; - notification.vibrate = null; - voiceReminder = false; + if (notifications.isEmpty()) { + return; + } else { + Timber.d("Triggering %s", notifications.keySet()); } - for (int i = 0; i < Math.max(ringTimes, 1); i++) { - notificationManager.notify(notificationId, notification); - AndroidUtilities.sleepDeep(500); - } - if (voiceReminder) { - AndroidUtilities.sleepDeep(2000); - voiceOutputAssistant.speak(text); - } - } + notificationManager.notifyTasks(notifications, alert, ringNonstop, ringFiveTimes); - /** - * @return true if notification should sound - */ - private static boolean checkLastNotificationSound() { - long now = DateUtilities.now(); - if (now - lastNotificationSound > 10000) { - lastNotificationSound = now; - return true; + if (alert && + preferences.getBoolean(R.string.p_voiceRemindersEnabled, false) && + !ringNonstop && + !audioManager.notificationsMuted() && + telephonyManager.callStateIdle()) { + for (Notification notification : notifications.values()) { + AndroidUtilities.sleepDeep(2000); + voiceOutputAssistant.speak(notification.tickerText.toString()); + } } - return false; } } diff --git a/app/src/main/java/org/tasks/db/AppDatabase.java b/app/src/main/java/org/tasks/db/AppDatabase.java new file mode 100644 index 000000000..3ceb02354 --- /dev/null +++ b/app/src/main/java/org/tasks/db/AppDatabase.java @@ -0,0 +1,12 @@ +package org.tasks.db; + +import android.arch.persistence.room.Database; +import android.arch.persistence.room.RoomDatabase; + +import org.tasks.notifications.Notification; +import org.tasks.notifications.NotificationDao; + +@Database(entities = {Notification.class}, version = 1) +public abstract class AppDatabase extends RoomDatabase { + public abstract NotificationDao notificationDao(); +} diff --git a/app/src/main/java/org/tasks/injection/ApplicationModule.java b/app/src/main/java/org/tasks/injection/ApplicationModule.java index 3ddcc017e..d51f48fe2 100644 --- a/app/src/main/java/org/tasks/injection/ApplicationModule.java +++ b/app/src/main/java/org/tasks/injection/ApplicationModule.java @@ -1,9 +1,11 @@ package org.tasks.injection; +import android.arch.persistence.room.Room; import android.content.Context; import org.tasks.ErrorReportingSingleThreadExecutor; import org.tasks.analytics.Tracker; +import org.tasks.db.AppDatabase; import org.tasks.locale.Locale; import org.tasks.themes.ThemeCache; import org.tasks.ui.CheckBoxes; @@ -56,4 +58,10 @@ public class ApplicationModule { public WidgetCheckBoxes getWidgetCheckBoxes(CheckBoxes checkBoxes) { return newWidgetCheckBoxes(checkBoxes); } + + @Provides + @ApplicationScope + public AppDatabase getAppDatabase() { + return Room.databaseBuilder(context, AppDatabase.class, "app-database").build(); + } } diff --git a/app/src/main/java/org/tasks/injection/IntentServiceComponent.java b/app/src/main/java/org/tasks/injection/IntentServiceComponent.java index ba60ade50..88ddf8df0 100644 --- a/app/src/main/java/org/tasks/injection/IntentServiceComponent.java +++ b/app/src/main/java/org/tasks/injection/IntentServiceComponent.java @@ -5,17 +5,15 @@ import org.tasks.jobs.BackupJob; import org.tasks.jobs.MidnightRefreshJob; import org.tasks.jobs.RefreshJob; import org.tasks.location.GeofenceTransitionsIntentService; +import org.tasks.scheduling.BackgroundScheduler; import org.tasks.scheduling.CalendarNotificationIntentService; import org.tasks.scheduling.GeofenceSchedulingIntentService; import org.tasks.scheduling.NotificationSchedulerIntentService; -import org.tasks.scheduling.SchedulerIntentService; import dagger.Subcomponent; @Subcomponent(modules = IntentServiceModule.class) public interface IntentServiceComponent { - void inject(SchedulerIntentService schedulerIntentService); - void inject(GeofenceSchedulingIntentService geofenceSchedulingIntentService); void inject(CalendarNotificationIntentService calendarNotificationIntentService); @@ -31,4 +29,6 @@ public interface IntentServiceComponent { void inject(MidnightRefreshJob midnightRefreshJob); void inject(RefreshJob refreshJob); + + void inject(BackgroundScheduler backgroundScheduler); } diff --git a/app/src/main/java/org/tasks/jobs/Alarm.java b/app/src/main/java/org/tasks/jobs/Alarm.java index c48139bb6..9f3b81886 100644 --- a/app/src/main/java/org/tasks/jobs/Alarm.java +++ b/app/src/main/java/org/tasks/jobs/Alarm.java @@ -2,6 +2,11 @@ package org.tasks.jobs; import com.todoroo.astrid.alarms.AlarmFields; import com.todoroo.astrid.data.Metadata; +import com.todoroo.astrid.reminders.ReminderService; + +import org.tasks.notifications.Notification; + +import static org.tasks.time.DateTimeUtils.currentTimeMillis; public class Alarm implements JobQueueEntry { private final long alarmId; @@ -32,6 +37,15 @@ public class Alarm implements JobQueueEntry { return time; } + @Override + public Notification toNotification() { + Notification notification = new Notification(); + notification.taskId = taskId; + notification.type = ReminderService.TYPE_ALARM; + notification.timestamp = currentTimeMillis(); + return notification; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/app/src/main/java/org/tasks/jobs/JobManager.java b/app/src/main/java/org/tasks/jobs/JobManager.java index e4914c12b..1e1904083 100644 --- a/app/src/main/java/org/tasks/jobs/JobManager.java +++ b/app/src/main/java/org/tasks/jobs/JobManager.java @@ -20,12 +20,12 @@ import static org.tasks.time.DateTimeUtils.printTimestamp; public class JobManager { static final int JOB_ID_REFRESH = 1; + public static final int JOB_ID_BACKGROUND_SCHEDULER = 2; static final int JOB_ID_NOTIFICATION = 3; public static final int JOB_ID_GEOFENCE_TRANSITION = 4; public static final int JOB_ID_GEOFENCE_SCHEDULING = 5; static final int JOB_ID_MIDNIGHT_REFRESH = 6; static final int JOB_ID_BACKUP = 7; - public static final int JOB_ID_SCHEDULER = 8; public static final int JOB_ID_NOTIFICATION_SCHEDULER = 9; public static final int JOB_ID_CALENDAR_NOTIFICATION = 10; @@ -38,7 +38,7 @@ public class JobManager { this.alarmManager = alarmManager; } - public void schedule(String tag, long time) { + void schedule(String tag, long time) { Timber.d("%s: %s", tag, printTimestamp(time)); alarmManager.wakeup(adjust(time), getPendingIntent(tag)); } diff --git a/app/src/main/java/org/tasks/jobs/JobQueue.java b/app/src/main/java/org/tasks/jobs/JobQueue.java index f3ca4e645..7f6db5f09 100644 --- a/app/src/main/java/org/tasks/jobs/JobQueue.java +++ b/app/src/main/java/org/tasks/jobs/JobQueue.java @@ -72,10 +72,6 @@ public class JobQueue { return result; } - public synchronized boolean remove(JobQueueEntry entry) { - return jobs.remove(entry.getTime(), entry); - } - synchronized void scheduleNext() { scheduleNext(false); } @@ -106,4 +102,12 @@ public class JobQueue { List getJobs() { return ImmutableList.copyOf(jobs.values()); } + + public synchronized boolean remove(List entries) { + boolean success = true; + for (JobQueueEntry entry : entries) { + success &= jobs.remove(entry.getTime(), entry); + } + return success; + } } diff --git a/app/src/main/java/org/tasks/jobs/JobQueueEntry.java b/app/src/main/java/org/tasks/jobs/JobQueueEntry.java index a52845d05..6ece579a9 100644 --- a/app/src/main/java/org/tasks/jobs/JobQueueEntry.java +++ b/app/src/main/java/org/tasks/jobs/JobQueueEntry.java @@ -1,7 +1,11 @@ package org.tasks.jobs; +import org.tasks.notifications.Notification; + public interface JobQueueEntry { long getId(); long getTime(); + + Notification toNotification(); } diff --git a/app/src/main/java/org/tasks/jobs/NotificationJob.java b/app/src/main/java/org/tasks/jobs/NotificationJob.java index c8b311328..87885cbbc 100644 --- a/app/src/main/java/org/tasks/jobs/NotificationJob.java +++ b/app/src/main/java/org/tasks/jobs/NotificationJob.java @@ -6,13 +6,14 @@ import android.content.Intent; import android.support.v4.app.JobIntentService; import com.todoroo.astrid.dao.TaskDao; -import com.todoroo.astrid.data.Task; -import com.todoroo.astrid.reminders.ReminderService; +import org.tasks.BuildConfig; import org.tasks.Notifier; import org.tasks.injection.IntentServiceComponent; import org.tasks.preferences.Preferences; +import java.util.List; + import javax.inject.Inject; public class NotificationJob extends Job { @@ -34,18 +35,11 @@ public class NotificationJob extends Job { @Override protected void run() { if (!preferences.isCurrentlyQuietHours()) { - for (JobQueueEntry entry : jobQueue.getOverdueJobs()) { - if (entry instanceof Alarm) { - Alarm alarm = (Alarm) entry; - Task task = taskDao.fetch(alarm.getTaskId(), Task.REMINDER_LAST); - if (task != null && task.getReminderLast() < alarm.getTime()) { - notifier.triggerTaskNotification(alarm.getTaskId(), ReminderService.TYPE_ALARM); - } - } else if (entry instanceof Reminder) { - Reminder reminder = (Reminder) entry; - notifier.triggerTaskNotification(reminder.getId(), reminder.getType()); - } - jobQueue.remove(entry); + List overdueJobs = jobQueue.getOverdueJobs(); + notifier.triggerTaskNotifications(overdueJobs); + boolean success = jobQueue.remove(overdueJobs); + if (BuildConfig.DEBUG && !success) { + throw new RuntimeException("Failed to remove jobs from queue"); } } } diff --git a/app/src/main/java/org/tasks/jobs/Reminder.java b/app/src/main/java/org/tasks/jobs/Reminder.java index 06bc1f54b..43fa55a83 100644 --- a/app/src/main/java/org/tasks/jobs/Reminder.java +++ b/app/src/main/java/org/tasks/jobs/Reminder.java @@ -1,5 +1,9 @@ package org.tasks.jobs; +import org.tasks.notifications.Notification; + +import static org.tasks.time.DateTimeUtils.currentTimeMillis; + public class Reminder implements JobQueueEntry { private final long taskId; private final long time; @@ -21,6 +25,15 @@ public class Reminder implements JobQueueEntry { return time; } + @Override + public Notification toNotification() { + Notification notification = new Notification(); + notification.taskId = taskId; + notification.type = type; + notification.timestamp = currentTimeMillis(); + return notification; + } + public int getType() { return type; } diff --git a/app/src/main/java/org/tasks/notifications/Notification.java b/app/src/main/java/org/tasks/notifications/Notification.java new file mode 100644 index 000000000..bcdd5b2b3 --- /dev/null +++ b/app/src/main/java/org/tasks/notifications/Notification.java @@ -0,0 +1,34 @@ +package org.tasks.notifications; + +import android.arch.persistence.room.ColumnInfo; +import android.arch.persistence.room.Entity; +import android.arch.persistence.room.Index; +import android.arch.persistence.room.PrimaryKey; + +@Entity(tableName = "notification", + indices = {@Index(value = "task", unique = true)}) +public class Notification { + + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = "uid") + public int uid; + + @ColumnInfo(name = "task") + public Long taskId; + + @ColumnInfo(name = "timestamp") + public long timestamp; + + @ColumnInfo(name = "type") + public int type; + + @Override + public String toString() { + return "Notification{" + + "uid=" + uid + + ", taskId=" + taskId + + ", timestamp=" + timestamp + + ", type=" + type + + '}'; + } +} diff --git a/app/src/main/java/org/tasks/notifications/NotificationClearedReceiver.java b/app/src/main/java/org/tasks/notifications/NotificationClearedReceiver.java new file mode 100644 index 000000000..0d83dd400 --- /dev/null +++ b/app/src/main/java/org/tasks/notifications/NotificationClearedReceiver.java @@ -0,0 +1,32 @@ +package org.tasks.notifications; + +import android.content.Context; +import android.content.Intent; + +import org.tasks.db.AppDatabase; +import org.tasks.injection.BroadcastComponent; +import org.tasks.injection.InjectingBroadcastReceiver; + +import javax.inject.Inject; + +import timber.log.Timber; + +public class NotificationClearedReceiver extends InjectingBroadcastReceiver { + + @Inject NotificationManager notificationManager; + @Inject AppDatabase appDatabase; + + @Override + public void onReceive(Context context, Intent intent) { + super.onReceive(context, intent); + + long notificationId = intent.getLongExtra(NotificationManager.EXTRA_NOTIFICATION_ID, -1L); + Timber.d("cleared %s", notificationId); + notificationManager.cancel(notificationId); + } + + @Override + protected void inject(BroadcastComponent component) { + component.inject(this); + } +} diff --git a/app/src/main/java/org/tasks/notifications/NotificationDao.java b/app/src/main/java/org/tasks/notifications/NotificationDao.java new file mode 100644 index 000000000..c4675f80d --- /dev/null +++ b/app/src/main/java/org/tasks/notifications/NotificationDao.java @@ -0,0 +1,25 @@ +package org.tasks.notifications; + +import android.arch.persistence.room.Dao; +import android.arch.persistence.room.Insert; +import android.arch.persistence.room.OnConflictStrategy; +import android.arch.persistence.room.Query; + +import java.util.List; + +import io.reactivex.Single; + +@Dao +public interface NotificationDao { + @Query("SELECT * FROM notification") + List getAll(); + + @Insert(onConflict = OnConflictStrategy.REPLACE) + void insertAll(List notifications); + + @Query("SELECT COUNT(*) FROM notification") + int count(); + + @Query("DELETE FROM notification WHERE task = :taskId") + void delete(long taskId); +} diff --git a/app/src/main/java/org/tasks/notifications/NotificationManager.java b/app/src/main/java/org/tasks/notifications/NotificationManager.java index b91ca77fe..208f6047b 100644 --- a/app/src/main/java/org/tasks/notifications/NotificationManager.java +++ b/app/src/main/java/org/tasks/notifications/NotificationManager.java @@ -1,49 +1,154 @@ package org.tasks.notifications; +import android.annotation.TargetApi; import android.app.Notification; import android.app.NotificationChannel; +import android.app.PendingIntent; import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.support.v4.app.NotificationCompat; import org.tasks.R; +import org.tasks.db.AppDatabase; +import org.tasks.injection.ApplicationScope; import org.tasks.injection.ForApplication; import org.tasks.preferences.Preferences; +import java.util.ArrayList; +import java.util.Map; + import javax.inject.Inject; +import io.reactivex.Completable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; + +import static com.google.common.collect.Lists.newArrayList; +import static com.todoroo.andlib.utility.AndroidUtilities.atLeastNougat; import static com.todoroo.andlib.utility.AndroidUtilities.atLeastOreo; +@ApplicationScope 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_CALLS = "notifications_calls"; + public static final String GROUP_KEY = "tasks"; + private static final int SUMMARY_NOTIFICATION_ID = 0; + static final String EXTRA_NOTIFICATION_ID = "extra_notification_id"; + private final android.app.NotificationManager notificationManager; + private final AppDatabase appDatabase; + private final Context context; private final Preferences preferences; - public static final String DEFAULT_NOTIFICATION_CHANNEL = "notifications"; @Inject - public NotificationManager(@ForApplication Context context, Preferences preferences) { + public NotificationManager(@ForApplication Context context, Preferences preferences, + AppDatabase appDatabase) { + this.context = context; this.preferences = preferences; notificationManager = (android.app.NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + this.appDatabase = appDatabase; if (atLeastOreo()) { - String channelName = context.getString(R.string.notifications); - NotificationChannel notificationChannel = new NotificationChannel(DEFAULT_NOTIFICATION_CHANNEL, 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); - notificationChannel.setLightColor(preferences.getLEDColor()); - notificationChannel.setVibrationPattern(preferences.getVibrationPattern()); - notificationManager.createNotificationChannel(notificationChannel); + notificationManager.createNotificationChannel(createNotificationChannel(NOTIFICATION_CHANNEL_DEFAULT, R.string.notifications)); + notificationManager.createNotificationChannel(createNotificationChannel(NOTIFICATION_CHANNEL_CALLS, R.string.missed_calls)); + notificationManager.createNotificationChannel(createNotificationChannel(NOTIFICATION_CHANNEL_TASKER, R.string.tasker_locale)); } } + @TargetApi(Build.VERSION_CODES.O) + private NotificationChannel createNotificationChannel(String channelId, int nameResId) { + String channelName = context.getString(nameResId); + 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); + return notificationChannel; + } + public void cancel(long id) { notificationManager.cancel((int) id); + Completable.fromAction(() -> { + appDatabase.notificationDao().delete(id); + updateSummary(false, false, false); + }) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe(); } - public void notify(int notificationId, Notification notification) { + public void notifyTasks(Map notifications, boolean alert, boolean nonstop, boolean fiveTimes) { + appDatabase.notificationDao().insertAll(newArrayList(notifications.keySet())); + updateSummary(alert && notifications.size() > 1, nonstop, fiveTimes); + ArrayList> entries = newArrayList(notifications.entrySet()); + + int last = entries.size() - 1; + for (int i = 0; i < last; i++) { + Map.Entry entry = entries.get(i); + notify(entry.getKey().taskId, entry.getValue(), false, false, false); + } + Map.Entry entry = entries.get(last); + notify(entry.getKey().taskId, entry.getValue(), alert, nonstop, fiveTimes); + } + + public void notify(long notificationId, Notification notification, boolean alert, boolean nonstop, boolean fiveTimes) { if (preferences.getBoolean(R.string.p_rmd_enabled, true)) { - notificationManager.notify(notificationId, notification); + int ringTimes = 1; + if (preferences.getBoolean(R.string.p_rmd_persistent, true)) { + notification.flags |= Notification.FLAG_NO_CLEAR; + } + if (preferences.isLEDNotificationEnabled()) { + notification.defaults |= Notification.DEFAULT_LIGHTS; + } + if (alert) { + if (nonstop) { + notification.flags |= Notification.FLAG_INSISTENT; + ringTimes = 1; + } else if (fiveTimes) { + ringTimes = 5; + } + if (preferences.isVibrationEnabled()) { + notification.defaults |= Notification.DEFAULT_VIBRATE; + } + notification.sound = preferences.getRingtone(); + notification.audioStreamType = Notification.STREAM_DEFAULT; + } + 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); + for (int i = 0 ; i < ringTimes ; i++) { + notificationManager.notify((int) notificationId, notification); + } + } + } + + private void updateSummary(boolean notify, boolean nonStop, boolean fiveTimes) { + if (atLeastNougat()) { + if (appDatabase.notificationDao().count() == 0) { + notificationManager.cancel(SUMMARY_NOTIFICATION_ID); + } else { + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NotificationManager.NOTIFICATION_CHANNEL_DEFAULT) + .setGroupSummary(true) + .setGroup(GROUP_KEY) + .setShowWhen(false) + .setSmallIcon(R.drawable.ic_done_all_white_24dp); + + if (notify) { + builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setSound(preferences.getRingtone()); + + } else { + builder.setOnlyAlertOnce(true) + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN); + } + + notify(NotificationManager.SUMMARY_NOTIFICATION_ID, builder.build(), notify, nonStop, fiveTimes); + } } } } diff --git a/app/src/main/java/org/tasks/preferences/MiscellaneousPreferences.java b/app/src/main/java/org/tasks/preferences/MiscellaneousPreferences.java index eff8a3ac2..d7c3de6de 100644 --- a/app/src/main/java/org/tasks/preferences/MiscellaneousPreferences.java +++ b/app/src/main/java/org/tasks/preferences/MiscellaneousPreferences.java @@ -12,7 +12,7 @@ import org.tasks.R; import org.tasks.files.FileExplore; import org.tasks.injection.ActivityComponent; import org.tasks.injection.InjectingPreferenceActivity; -import org.tasks.scheduling.BackgroundScheduler; +import org.tasks.scheduling.CalendarNotificationIntentService; import java.io.File; @@ -31,7 +31,6 @@ public class MiscellaneousPreferences extends InjectingPreferenceActivity { @Inject VoiceOutputAssistant voiceOutputAssistant; @Inject ActivityPermissionRequestor permissionRequestor; @Inject PermissionChecker permissionChecker; - @Inject BackgroundScheduler backgroundScheduler; private CheckBoxPreference calendarReminderPreference; @@ -113,7 +112,7 @@ public class MiscellaneousPreferences extends InjectingPreferenceActivity { return true; } if (permissionRequestor.requestCalendarPermissions()) { - backgroundScheduler.scheduleCalendarNotifications(); + CalendarNotificationIntentService.enqueueWork(this); return true; } return false; diff --git a/app/src/main/java/org/tasks/preferences/Preferences.java b/app/src/main/java/org/tasks/preferences/Preferences.java index a29445980..5eab6a2bb 100644 --- a/app/src/main/java/org/tasks/preferences/Preferences.java +++ b/app/src/main/java/org/tasks/preferences/Preferences.java @@ -3,10 +3,12 @@ package org.tasks.preferences; import android.content.Context; import android.content.SharedPreferences; import android.content.res.Resources; +import android.media.RingtoneManager; +import android.net.Uri; import android.preference.PreferenceManager; -import android.support.v4.app.NotificationCompat; import android.text.TextUtils; +import com.google.common.base.Strings; import com.todoroo.astrid.activity.BeastModePreferences; import com.todoroo.astrid.api.AstridApiConstants; import com.todoroo.astrid.core.SortHelper; @@ -15,7 +17,6 @@ import com.todoroo.astrid.data.TaskAttachment; import org.tasks.R; import org.tasks.injection.ForApplication; -import org.tasks.themes.ThemeCache; import org.tasks.time.DateTime; import java.io.File; @@ -26,7 +27,6 @@ import javax.inject.Inject; import timber.log.Timber; import static android.content.SharedPreferences.Editor; -import static com.todoroo.andlib.utility.AndroidUtilities.atLeastOreo; public class Preferences { @@ -38,14 +38,11 @@ public class Preferences { private final PermissionChecker permissionChecker; private final SharedPreferences prefs; private final SharedPreferences publicPrefs; - private final ThemeCache themeCache; @Inject - public Preferences(@ForApplication Context context, PermissionChecker permissionChecker, - ThemeCache themeCache) { + public Preferences(@ForApplication Context context, PermissionChecker permissionChecker) { this.context = context; this.permissionChecker = permissionChecker; - this.themeCache = themeCache; prefs = PreferenceManager.getDefaultSharedPreferences(context); publicPrefs = context.getSharedPreferences(AstridApiConstants.PUBLIC_PREFS, Context.MODE_PRIVATE); } @@ -88,16 +85,8 @@ public class Preferences { return time; } - public boolean isVibrationEnabled() { - return atLeastOreo() || getBoolean(R.string.p_rmd_vibrate, true); - } - public boolean isLEDNotificationEnabled() { - return atLeastOreo() || getBoolean(R.string.p_led_notification, true); - } - - public int getLEDColor() { - return R.color.led_green; + return getBoolean(R.string.p_led_notification, true); } public boolean quietHoursEnabled() { @@ -141,13 +130,15 @@ public class Preferences { return defaultCalendar != null && !defaultCalendar.equals("-1") && !defaultCalendar.equals("0"); } - public boolean isTrackingEnabled() { - return getBoolean(R.string.p_collect_statistics, true); + public Uri getRingtone() { + String ringtone = getStringValue(R.string.p_rmd_ringtone); + return Strings.isNullOrEmpty(ringtone) + ? RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) + : Uri.parse(ringtone); } - public int getNotificationPriority() { - return Math.max(NotificationCompat.PRIORITY_MIN, Math.min(NotificationCompat.PRIORITY_MAX, - getIntegerFromString(R.string.p_notification_priority, NotificationCompat.PRIORITY_HIGH))); + public boolean isTrackingEnabled() { + return getBoolean(R.string.p_collect_statistics, true); } public String getDefaultCalendar() { @@ -411,8 +402,8 @@ public class Preferences { return directory; } - public long[] getVibrationPattern() { - return new long[] {0, 333, 333, 333}; + public boolean isVibrationEnabled() { + return getBoolean(R.string.p_rmd_vibrate, true); } public void remove(int resId) { diff --git a/app/src/main/java/org/tasks/receivers/BootCompletedReceiver.java b/app/src/main/java/org/tasks/receivers/BootCompletedReceiver.java index abfccd1c6..8593b15c6 100644 --- a/app/src/main/java/org/tasks/receivers/BootCompletedReceiver.java +++ b/app/src/main/java/org/tasks/receivers/BootCompletedReceiver.java @@ -5,27 +5,20 @@ import android.content.Intent; import org.tasks.injection.BroadcastComponent; import org.tasks.injection.InjectingBroadcastReceiver; -import org.tasks.scheduling.BackgroundScheduler; - -import javax.inject.Inject; import timber.log.Timber; public class BootCompletedReceiver extends InjectingBroadcastReceiver { - @Inject BackgroundScheduler backgroundScheduler; - @Override public void onReceive(Context context, Intent intent) { super.onReceive(context, intent); - if (!intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) { + if (!Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) { return; } Timber.d("onReceive(context, %s)", intent); - - backgroundScheduler.scheduleEverything(); } @Override diff --git a/app/src/main/java/org/tasks/receivers/MyPackageReplacedReceiver.java b/app/src/main/java/org/tasks/receivers/MyPackageReplacedReceiver.java index 8a99ae5bc..0c5814fdf 100644 --- a/app/src/main/java/org/tasks/receivers/MyPackageReplacedReceiver.java +++ b/app/src/main/java/org/tasks/receivers/MyPackageReplacedReceiver.java @@ -5,27 +5,20 @@ import android.content.Intent; import org.tasks.injection.BroadcastComponent; import org.tasks.injection.InjectingBroadcastReceiver; -import org.tasks.scheduling.BackgroundScheduler; - -import javax.inject.Inject; import timber.log.Timber; public class MyPackageReplacedReceiver extends InjectingBroadcastReceiver { - @Inject BackgroundScheduler backgroundScheduler; - @Override public void onReceive(Context context, Intent intent) { super.onReceive(context, intent); - if (!intent.getAction().equals(Intent.ACTION_MY_PACKAGE_REPLACED)) { + if (!Intent.ACTION_MY_PACKAGE_REPLACED.equals(intent.getAction())) { return; } Timber.d("onReceive(context, %s)", intent); - - backgroundScheduler.scheduleEverything(); } @Override diff --git a/app/src/main/java/org/tasks/scheduling/BackgroundScheduler.java b/app/src/main/java/org/tasks/scheduling/BackgroundScheduler.java index 02d18031f..0204b5022 100644 --- a/app/src/main/java/org/tasks/scheduling/BackgroundScheduler.java +++ b/app/src/main/java/org/tasks/scheduling/BackgroundScheduler.java @@ -2,29 +2,56 @@ package org.tasks.scheduling; import android.content.Context; import android.content.Intent; -import android.support.v4.app.JobIntentService; +import android.support.annotation.NonNull; + +import com.todoroo.andlib.sql.Criterion; +import com.todoroo.astrid.dao.TaskDao; +import com.todoroo.astrid.data.Task; import org.tasks.injection.ForApplication; +import org.tasks.injection.InjectingJobIntentService; +import org.tasks.injection.IntentServiceComponent; import org.tasks.jobs.JobManager; import javax.inject.Inject; -public class BackgroundScheduler { - private final Context context; +import timber.log.Timber; + +import static java.lang.System.currentTimeMillis; - @Inject - public BackgroundScheduler(@ForApplication Context context) { - this.context = context; +public class BackgroundScheduler extends InjectingJobIntentService { + + public static void enqueueWork(Context context) { + BackgroundScheduler.enqueueWork(context, BackgroundScheduler.class, JobManager.JOB_ID_BACKGROUND_SCHEDULER, new Intent()); } - public void scheduleEverything() { - JobIntentService.enqueueWork(context, GeofenceSchedulingIntentService.class, JobManager.JOB_ID_GEOFENCE_SCHEDULING, new Intent()); - JobIntentService.enqueueWork(context, SchedulerIntentService.class, JobManager.JOB_ID_SCHEDULER, new Intent()); - JobIntentService.enqueueWork(context, NotificationSchedulerIntentService.class, JobManager.JOB_ID_NOTIFICATION_SCHEDULER, new Intent()); - scheduleCalendarNotifications(); + @Inject @ForApplication Context context; + @Inject TaskDao taskDao; + @Inject JobManager jobManager; + @Inject RefreshScheduler refreshScheduler; + + @Override + protected void onHandleWork(@NonNull Intent intent) { + super.onHandleWork(intent); + + Timber.d("onHandleWork(%s)", intent); + + NotificationSchedulerIntentService.enqueueWork(context); + CalendarNotificationIntentService.enqueueWork(context); + GeofenceSchedulingIntentService.enqueueWork(context); + + jobManager.scheduleMidnightBackup(); + jobManager.scheduleMidnightRefresh(); + + refreshScheduler.clear(); + long now = currentTimeMillis(); + taskDao.selectActive( + Criterion.or(Task.HIDE_UNTIL.gt(now), Task.DUE_DATE.gt(now)), + refreshScheduler::scheduleRefresh); } - public void scheduleCalendarNotifications() { - JobIntentService.enqueueWork(context, CalendarNotificationIntentService.class, JobManager.JOB_ID_CALENDAR_NOTIFICATION, new Intent()); + @Override + protected void inject(IntentServiceComponent component) { + component.inject(this); } } diff --git a/app/src/main/java/org/tasks/scheduling/CalendarNotificationIntentService.java b/app/src/main/java/org/tasks/scheduling/CalendarNotificationIntentService.java index 112697ec8..bc982f924 100644 --- a/app/src/main/java/org/tasks/scheduling/CalendarNotificationIntentService.java +++ b/app/src/main/java/org/tasks/scheduling/CalendarNotificationIntentService.java @@ -26,10 +26,14 @@ import timber.log.Timber; public class CalendarNotificationIntentService extends RecurringIntervalIntentService { + public static void enqueueWork(Context context) { + JobIntentService.enqueueWork(context, CalendarNotificationIntentService.class, JobManager.JOB_ID_CALENDAR_NOTIFICATION, new Intent()); + } + public static class Broadcast extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - JobIntentService.enqueueWork(context, CalendarNotificationIntentService.class, JobManager.JOB_ID_CALENDAR_NOTIFICATION, new Intent()); + enqueueWork(context); } } diff --git a/app/src/main/java/org/tasks/scheduling/GeofenceSchedulingIntentService.java b/app/src/main/java/org/tasks/scheduling/GeofenceSchedulingIntentService.java index d8bdeed57..48227fe0c 100644 --- a/app/src/main/java/org/tasks/scheduling/GeofenceSchedulingIntentService.java +++ b/app/src/main/java/org/tasks/scheduling/GeofenceSchedulingIntentService.java @@ -1,9 +1,12 @@ package org.tasks.scheduling; +import android.content.Context; import android.content.Intent; +import android.support.v4.app.JobIntentService; import org.tasks.injection.InjectingJobIntentService; import org.tasks.injection.IntentServiceComponent; +import org.tasks.jobs.JobManager; import org.tasks.location.GeofenceService; import javax.inject.Inject; @@ -12,13 +15,17 @@ import timber.log.Timber; public class GeofenceSchedulingIntentService extends InjectingJobIntentService { + public static void enqueueWork(Context context) { + JobIntentService.enqueueWork(context, GeofenceSchedulingIntentService.class, JobManager.JOB_ID_GEOFENCE_SCHEDULING, new Intent()); + } + @Inject GeofenceService geofenceService; @Override protected void onHandleWork(Intent intent) { super.onHandleWork(intent); - Timber.d("onHandleIntent(%s)", intent); + Timber.d("onHandleWork(%s)", intent); geofenceService.cancelGeofences(); geofenceService.setupGeofences(); diff --git a/app/src/main/java/org/tasks/scheduling/NotificationSchedulerIntentService.java b/app/src/main/java/org/tasks/scheduling/NotificationSchedulerIntentService.java index 631c83428..1f9232773 100644 --- a/app/src/main/java/org/tasks/scheduling/NotificationSchedulerIntentService.java +++ b/app/src/main/java/org/tasks/scheduling/NotificationSchedulerIntentService.java @@ -1,13 +1,17 @@ package org.tasks.scheduling; +import android.content.Context; import android.content.Intent; +import android.support.v4.app.JobIntentService; import com.todoroo.astrid.alarms.AlarmService; import com.todoroo.astrid.dao.TaskDao; import com.todoroo.astrid.reminders.ReminderService; +import org.tasks.Notifier; import org.tasks.injection.InjectingJobIntentService; import org.tasks.injection.IntentServiceComponent; +import org.tasks.jobs.JobManager; import org.tasks.jobs.JobQueue; import javax.inject.Inject; @@ -16,19 +20,25 @@ import timber.log.Timber; public class NotificationSchedulerIntentService extends InjectingJobIntentService { + public static void enqueueWork(Context context) { + JobIntentService.enqueueWork(context, NotificationSchedulerIntentService.class, JobManager.JOB_ID_NOTIFICATION_SCHEDULER, new Intent()); + } + @Inject AlarmService alarmService; @Inject ReminderService reminderService; @Inject TaskDao taskDao; @Inject JobQueue jobQueue; + @Inject Notifier notifier; @Override protected void onHandleWork(Intent intent) { super.onHandleWork(intent); - Timber.d("onHandleIntent(%s)", intent); + Timber.d("onHandleWork(%s)", intent); jobQueue.clear(); + notifier.restoreNotifications(); reminderService.scheduleAllAlarms(taskDao); alarmService.scheduleAllAlarms(); } diff --git a/app/src/main/java/org/tasks/scheduling/SchedulerIntentService.java b/app/src/main/java/org/tasks/scheduling/SchedulerIntentService.java deleted file mode 100644 index faa0e69b5..000000000 --- a/app/src/main/java/org/tasks/scheduling/SchedulerIntentService.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.tasks.scheduling; - -import android.content.Intent; - -import com.todoroo.andlib.sql.Criterion; -import com.todoroo.astrid.dao.TaskDao; -import com.todoroo.astrid.data.Task; - -import org.tasks.injection.InjectingJobIntentService; -import org.tasks.injection.IntentServiceComponent; -import org.tasks.jobs.JobManager; - -import javax.inject.Inject; - -import timber.log.Timber; - -import static java.lang.System.currentTimeMillis; - -public class SchedulerIntentService extends InjectingJobIntentService { - - @Inject TaskDao taskDao; - @Inject JobManager jobManager; - @Inject RefreshScheduler refreshScheduler; - - @Override - protected void onHandleWork(Intent intent) { - super.onHandleWork(intent); - - Timber.d("onHandleIntent(%s)", intent); - - jobManager.scheduleMidnightBackup(); - jobManager.scheduleMidnightRefresh(); - - refreshScheduler.clear(); - long now = currentTimeMillis(); - taskDao.selectActive( - Criterion.or(Task.HIDE_UNTIL.gt(now), Task.DUE_DATE.gt(now)), - refreshScheduler::scheduleRefresh); - } - - @Override - protected void inject(IntentServiceComponent component) { - component.inject(this); - } -} diff --git a/app/src/main/res/drawable-hdpi/ic_done_all_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_done_all_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..8c83ff91c01a8c8298df10577d710cce90f99929 GIT binary patch literal 275 zcmV+u0qp*XP)8{Ak@VRh%KH&5X7pR&Ys0V@HlSONsykxQLQI%)8g`XNk4_QZ!0L+XG=(a zByT>Hs#E6+VT6@v8!XB&Q+&LL!e9?9rOX5$05`z^^kvKgfD2}dk*p|W&efRlk22jV zjQtsAsF_m%1_K8>(KBO1Qz@nBaV^TUH~{yEV$KtALdq?)DLQx%r&#IXnhMvQR~xK1 zYOSYi1FB$&^C-bdWl%fXq65#MZo+$$L5Z(lKpknMDDl^2;RTN&MoAe>nW>&Z38v0} Z zI}QO+6o>KKDG-mPXgm|ku>uK&P$R~MCQ&JWRm-@Ey4#B$VF)TMF8In=6kA!p{S2$fuJRfXKsI0np;|`+OxM z`DUi`4bK0_?=z9FDe{z9QIELK7zCfI9Fpff;Pg%~2M7_?`5d1A&{+~pgD*>>XPGy~ z7$(7kf}qPSObE*6)!Gu`eZ>9cybz7gM1s5xkuV>ZP~<~IGmhdV05a26EH^Do>3VzkAd1j7p1$9mlKEvS8<$lOjC~rtLv7=+%qJ9CEuV( zSEHe6nx<)*rU}Pk{Y6Ypiv{D#MVl4ZDLH_jM4J@=Hz_$@(l-a-HYLYN+TsB8Q*vOm zgChx2PEAhdcR54lDCd802Fm#nXRVyS!8x)xSu_qEUO1~9dvU}=qQ|CKOfAw=Y|bSgrDIE*M32wXV#(VVG#2;9V!lm^$_XYl#W;baVj;qDlIHu6 z#5f+3ad~;LNU~q_0Fv|rqZUc#y4`@?Vp%^63A6{x?9Cum1nxzd3&8Q9$MY|JOs6|0kf~eemY)InMN|928pK9T?c3fParamètres du Widget Paramètres de l\'en-tête Paramètres de la ligne - Fréquence de notification Permissions requises de Tasks. Création d\'une nouvelle liste Suppression de la liste diff --git a/app/src/main/res/values/keys.xml b/app/src/main/res/values/keys.xml index d29d7cd99..becf3564c 100644 --- a/app/src/main/res/values/keys.xml +++ b/app/src/main/res/values/keys.xml @@ -195,7 +195,6 @@ show_completed_tasks reverse_sort manual_sort - notification_priority @string/TEA_ctrl_when_pref diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2bb867551..153a339de 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Aug 10 09:35:33 CDT 2017 +#Wed Aug 30 13:47:01 CDT 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-rc-1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip