Bundle and persist notifications

pull/574/head
Alex Baker 8 years ago
parent 805caeb434
commit e57adab036

@ -1,7 +1,7 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
task wrapper(type: Wrapper) { task wrapper(type: Wrapper) {
gradleVersion = '3.3' gradleVersion = '4.1'
} }
buildscript { buildscript {
@ -37,6 +37,12 @@ android {
targetSdkVersion 26 targetSdkVersion 26
minSdkVersion 15 minSdkVersion 15
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
} }
signingConfigs { signingConfigs {
@ -96,12 +102,17 @@ final BUTTERKNIFE_VERSION = '8.8.1'
final GPS_VERSION = '11.2.0' final GPS_VERSION = '11.2.0'
final SUPPORT_VERSION = '26.0.1' final SUPPORT_VERSION = '26.0.1'
final STETHO_VERSION = '1.5.0' final STETHO_VERSION = '1.5.0'
final ROOM_VERSION = '1.0.0-alpha9'
final TESTING_SUPPORT_VERSION = '1.0.0' final TESTING_SUPPORT_VERSION = '1.0.0'
dependencies { dependencies {
annotationProcessor "com.google.dagger:dagger-compiler:${DAGGER_VERSION}" annotationProcessor "com.google.dagger:dagger-compiler:${DAGGER_VERSION}"
compile "com.google.dagger:dagger:${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}" annotationProcessor "com.jakewharton:butterknife-compiler:${BUTTERKNIFE_VERSION}"
compile "com.jakewharton:butterknife:${BUTTERKNIFE_VERSION}" compile "com.jakewharton:butterknife:${BUTTERKNIFE_VERSION}"
@ -110,7 +121,6 @@ dependencies {
} }
debugCompile "com.facebook.stetho:stetho-timber:${STETHO_VERSION}@aar" debugCompile "com.facebook.stetho:stetho-timber:${STETHO_VERSION}@aar"
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1' debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1'
//noinspection GradleCompatible
debugCompile 'com.android.support:multidex:1.0.2' debugCompile 'com.android.support:multidex:1.0.2'
compile 'com.github.rey5137:material:1.2.4' compile 'com.github.rey5137:material:1.2.4'

@ -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\")"
]
}
}

@ -8,6 +8,7 @@ import com.todoroo.astrid.repeats.RepeatTaskCompleteListener;
import com.todoroo.astrid.timers.TimerTaskCompleteListener; import com.todoroo.astrid.timers.TimerTaskCompleteListener;
import org.tasks.locale.receiver.FireReceiver; import org.tasks.locale.receiver.FireReceiver;
import org.tasks.notifications.NotificationClearedReceiver;
import org.tasks.receivers.BootCompletedReceiver; import org.tasks.receivers.BootCompletedReceiver;
import org.tasks.receivers.CompleteTaskReceiver; import org.tasks.receivers.CompleteTaskReceiver;
import org.tasks.receivers.ListNotificationReceiver; import org.tasks.receivers.ListNotificationReceiver;
@ -47,4 +48,6 @@ public interface BroadcastComponent {
void inject(TeslaUnreadReceiver teslaUnreadReceiver); void inject(TeslaUnreadReceiver teslaUnreadReceiver);
void inject(PushReceiver pushReceiver); void inject(PushReceiver pushReceiver);
void inject(NotificationClearedReceiver notificationClearedReceiver);
} }

@ -93,7 +93,7 @@ public class NotificationTests extends DatabaseTestCase {
notifier.triggerTaskNotification(task.getId(), ReminderService.TYPE_DUE); 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 @Test

@ -1,10 +1,12 @@
package org.tasks.injection; package org.tasks.injection;
import android.arch.persistence.room.Room;
import android.content.Context; import android.content.Context;
import com.todoroo.astrid.dao.Database; import com.todoroo.astrid.dao.Database;
import org.tasks.analytics.Tracker; import org.tasks.analytics.Tracker;
import org.tasks.db.AppDatabase;
import org.tasks.preferences.PermissionChecker; import org.tasks.preferences.PermissionChecker;
import org.tasks.preferences.PermissivePermissionChecker; 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 @ApplicationScope
@Provides @Provides
@ForApplication @ForApplication

@ -75,7 +75,7 @@ public class JobQueueTest {
verify(jobManager).schedule(TAG, now); 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() {{ Freeze.freezeAt(now).thawAfter(new Snippet() {{
assertEquals( assertEquals(
@ -93,7 +93,7 @@ public class JobQueueTest {
verify(jobManager).schedule(TAG, now); 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() {{ Freeze.freezeAt(now).thawAfter(new Snippet() {{
assertEquals( assertEquals(
@ -242,7 +242,7 @@ public class JobQueueTest {
verify(jobManager).schedule(TAG, now); verify(jobManager).schedule(TAG, now);
Freeze.freezeAt(now).thawAfter(new Snippet() {{ Freeze.freezeAt(now).thawAfter(new Snippet() {{
queue.remove(new Reminder(1, now, TYPE_DUE)); queue.remove(queue.getOverdueJobs());
}}); }});
assertEquals( assertEquals(
@ -261,8 +261,7 @@ public class JobQueueTest {
verify(jobManager).schedule(TAG, now); verify(jobManager).schedule(TAG, now);
Freeze.freezeAt(now + ONE_MINUTE).thawAfter(new Snippet() {{ Freeze.freezeAt(now + ONE_MINUTE).thawAfter(new Snippet() {{
queue.remove(new Reminder(1, now, TYPE_DUE)); queue.remove(queue.getOverdueJobs());
queue.remove(new Reminder(2, now + ONE_MINUTE, TYPE_DUE));
}}); }});
assertEquals( assertEquals(

@ -24,7 +24,7 @@ public class PreferenceTests {
@Before @Before
public void setUp() { public void setUp() {
preferences = new Preferences(getTargetContext(), null, null); preferences = new Preferences(getTargetContext(), null);
preferences.clear(); preferences.clear();
preferences.setBoolean(R.string.p_rmd_enable_quiet, true); preferences.setBoolean(R.string.p_rmd_enable_quiet, true);
} }

@ -38,7 +38,7 @@ public class GtasksMetadataServiceTest extends DatabaseTestCase {
private final GtasksTestPreferenceService service; private final GtasksTestPreferenceService service;
public GtasksMetadataServiceTestModule(Context context) { public GtasksMetadataServiceTestModule(Context context) {
service = new GtasksTestPreferenceService(new Preferences(context, null, null)); service = new GtasksTestPreferenceService(new Preferences(context, null));
} }
@Provides @Provides

@ -8,6 +8,7 @@ import com.todoroo.astrid.repeats.RepeatTaskCompleteListener;
import com.todoroo.astrid.timers.TimerTaskCompleteListener; import com.todoroo.astrid.timers.TimerTaskCompleteListener;
import org.tasks.locale.receiver.FireReceiver; import org.tasks.locale.receiver.FireReceiver;
import org.tasks.notifications.NotificationClearedReceiver;
import org.tasks.receivers.BootCompletedReceiver; import org.tasks.receivers.BootCompletedReceiver;
import org.tasks.receivers.CompleteTaskReceiver; import org.tasks.receivers.CompleteTaskReceiver;
import org.tasks.receivers.ListNotificationReceiver; import org.tasks.receivers.ListNotificationReceiver;
@ -47,4 +48,6 @@ public interface BroadcastComponent {
void inject(TeslaUnreadReceiver teslaUnreadReceiver); void inject(TeslaUnreadReceiver teslaUnreadReceiver);
void inject(PushReceiver pushReceiver); void inject(PushReceiver pushReceiver);
void inject(NotificationClearedReceiver notificationClearedReceiver);
} }

@ -160,7 +160,7 @@ public class GoogleTaskSyncAdapter extends InjectingAbstractThreadedSyncAdapter
.setAutoCancel(true) .setAutoCancel(true)
.setSmallIcon(android.R.drawable.ic_dialog_alert) .setSmallIcon(android.R.drawable.ic_dialog_alert)
.setTicker(context.getString(R.string.common_google_play_services_notification_ticker)); .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 { private void synchronize() throws IOException {

@ -8,6 +8,7 @@ import com.todoroo.astrid.repeats.RepeatTaskCompleteListener;
import com.todoroo.astrid.timers.TimerTaskCompleteListener; import com.todoroo.astrid.timers.TimerTaskCompleteListener;
import org.tasks.locale.receiver.FireReceiver; import org.tasks.locale.receiver.FireReceiver;
import org.tasks.notifications.NotificationClearedReceiver;
import org.tasks.receivers.BootCompletedReceiver; import org.tasks.receivers.BootCompletedReceiver;
import org.tasks.receivers.CompleteTaskReceiver; import org.tasks.receivers.CompleteTaskReceiver;
import org.tasks.receivers.GoogleTaskPushReceiver; import org.tasks.receivers.GoogleTaskPushReceiver;
@ -50,4 +51,6 @@ public interface BroadcastComponent {
void inject(TeslaUnreadReceiver teslaUnreadReceiver); void inject(TeslaUnreadReceiver teslaUnreadReceiver);
void inject(PushReceiver pushReceiver); void inject(PushReceiver pushReceiver);
void inject(NotificationClearedReceiver notificationClearedReceiver);
} }

@ -424,7 +424,7 @@
android:permission="android.permission.BIND_JOB_SERVICE" android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false" /> android:exported="false" />
<service <service
android:name=".scheduling.SchedulerIntentService" android:name=".scheduling.BackgroundScheduler"
android:permission="android.permission.BIND_JOB_SERVICE" android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false" /> android:exported="false" />
<service <service
@ -468,6 +468,8 @@
android:permission="android.permission.BIND_JOB_SERVICE" android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false" /> android:exported="false" />
<receiver android:name=".notifications.NotificationClearedReceiver" />
<!-- Uses Library --> <!-- Uses Library -->
<uses-library <uses-library
android:name="com.google.android.maps" android:name="com.google.android.maps"

@ -240,6 +240,10 @@ public class AndroidUtilities {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
} }
public static boolean atLeastNougat() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
}
public static boolean atLeastOreo() { public static boolean atLeastOreo() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
} }

@ -5,28 +5,41 @@
*/ */
package com.todoroo.astrid.calls; package com.todoroo.astrid.calls;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.ContentUris;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri; import android.net.Uri;
import android.provider.CallLog.Calls; import android.provider.CallLog.Calls;
import android.provider.ContactsContract; import android.provider.ContactsContract;
import android.support.v4.app.NotificationCompat;
import android.telephony.TelephonyManager; import android.telephony.TelephonyManager;
import android.text.TextUtils; import android.text.TextUtils;
import com.todoroo.andlib.utility.AndroidUtilities; import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.andlib.utility.DateUtilities; import com.todoroo.andlib.utility.DateUtilities;
import org.tasks.Notifier; import org.tasks.R;
import org.tasks.injection.BroadcastComponent; import org.tasks.injection.BroadcastComponent;
import org.tasks.injection.ForApplication;
import org.tasks.injection.InjectingBroadcastReceiver; import org.tasks.injection.InjectingBroadcastReceiver;
import org.tasks.notifications.NotificationManager;
import org.tasks.preferences.PermissionChecker; import org.tasks.preferences.PermissionChecker;
import org.tasks.preferences.Preferences; import org.tasks.preferences.Preferences;
import org.tasks.reminders.MissedCallActivity;
import java.io.InputStream;
import javax.inject.Inject; import javax.inject.Inject;
import timber.log.Timber; import timber.log.Timber;
import static org.tasks.time.DateTimeUtils.currentTimeMillis;
public class PhoneStateChangedReceiver extends InjectingBroadcastReceiver { public class PhoneStateChangedReceiver extends InjectingBroadcastReceiver {
private static final String PREF_LAST_INCOMING_NUMBER = "last_incoming_number"; 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; private static final long WAIT_BEFORE_READ_LOG = 3000L;
@Inject Preferences preferences; @Inject Preferences preferences;
@Inject Notifier notifier; @Inject NotificationManager notificationManager;
@Inject PermissionChecker permissionChecker; @Inject PermissionChecker permissionChecker;
@Inject @ForApplication Context context;
@Override @Override
public void onReceive(final Context context, Intent intent) { public void onReceive(final Context context, Intent intent) {
@ -73,12 +87,12 @@ public class PhoneStateChangedReceiver extends InjectingBroadcastReceiver {
AndroidUtilities.sleepDeep(WAIT_BEFORE_READ_LOG); AndroidUtilities.sleepDeep(WAIT_BEFORE_READ_LOG);
Cursor calls; Cursor calls;
try { try {
calls = getMissedCalls(context); calls = getMissedCalls();
} catch (Exception e) { // Sometimes database is locked, retry once } catch (Exception e) { // Sometimes database is locked, retry once
Timber.e(e, e.getMessage()); Timber.e(e, e.getMessage());
AndroidUtilities.sleepDeep(300L); AndroidUtilities.sleepDeep(300L);
try { try {
calls = getMissedCalls(context); calls = getMissedCalls();
} catch (Exception e2) { } catch (Exception e2) {
Timber.e(e2, e2.getMessage()); Timber.e(e2, e2.getMessage());
calls = null; calls = null;
@ -113,7 +127,7 @@ public class PhoneStateChangedReceiver extends InjectingBroadcastReceiver {
long contactId = getContactIdFromNumber(context, number); long contactId = getContactIdFromNumber(context, number);
notifier.triggerMissedCallNotification(name, number, contactId); triggerMissedCallNotification(name, number, contactId);
} }
} catch (Exception e) { } catch (Exception e) {
Timber.e(e, e.getMessage()); 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()) { if (permissionChecker.canAccessMissedCallPermissions()) {
//noinspection MissingPermission //noinspection MissingPermission
return context.getContentResolver().query( return context.getContentResolver().query(
@ -174,4 +189,61 @@ public class PhoneStateChangedReceiver extends InjectingBroadcastReceiver {
return -1; 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;
}
} }

@ -95,6 +95,10 @@ public class TaskDao {
dao.query(Query.select(Task.PROPERTIES).where(Criterion.and(TaskCriteria.isActive(), criterion)), callback); 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) { public Task fetch(long id, Property<?>... properties) {
return dao.fetch(id, properties); return dao.fetch(id, properties);
} }

@ -17,14 +17,11 @@ import android.preference.Preference;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.provider.Settings; import android.provider.Settings;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.v4.app.JobIntentService;
import org.tasks.R; import org.tasks.R;
import org.tasks.activities.TimePickerActivity; import org.tasks.activities.TimePickerActivity;
import org.tasks.injection.ActivityComponent; import org.tasks.injection.ActivityComponent;
import org.tasks.injection.InjectingPreferenceActivity; import org.tasks.injection.InjectingPreferenceActivity;
import org.tasks.jobs.JobManager;
import org.tasks.notifications.NotificationManager;
import org.tasks.preferences.ActivityPermissionRequestor; import org.tasks.preferences.ActivityPermissionRequestor;
import org.tasks.preferences.Device; import org.tasks.preferences.Device;
import org.tasks.preferences.PermissionChecker; import org.tasks.preferences.PermissionChecker;
@ -66,7 +63,8 @@ public class ReminderPreferences extends InjectingPreferenceActivity {
R.string.p_rmd_time, R.string.p_rmd_time,
R.string.p_rmd_enable_quiet, R.string.p_rmd_enable_quiet,
R.string.p_rmd_quietStart, R.string.p_rmd_quietStart,
R.string.p_rmd_quietEnd); R.string.p_rmd_quietEnd,
R.string.p_rmd_persistent);
resetGeofencesOnChange( resetGeofencesOnChange(
R.string.p_geofence_radius, R.string.p_geofence_radius,
R.string.p_geofence_responsiveness); R.string.p_geofence_responsiveness);
@ -89,8 +87,7 @@ public class ReminderPreferences extends InjectingPreferenceActivity {
@TargetApi(Build.VERSION_CODES.O) @TargetApi(Build.VERSION_CODES.O)
private boolean openNotificationChannelSettings(Preference ignored) { private boolean openNotificationChannelSettings(Preference ignored) {
Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS); Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
intent.putExtra(Settings.EXTRA_CHANNEL_ID, NotificationManager.DEFAULT_NOTIFICATION_CHANNEL);
intent.putExtra(Settings.EXTRA_APP_PACKAGE, ReminderPreferences.this.getPackageName()); intent.putExtra(Settings.EXTRA_APP_PACKAGE, ReminderPreferences.this.getPackageName());
startActivity(intent); startActivity(intent);
return true; return true;
@ -99,7 +96,7 @@ public class ReminderPreferences extends InjectingPreferenceActivity {
private void rescheduleNotificationsOnChange(int... resIds) { private void rescheduleNotificationsOnChange(int... resIds) {
for (int resId : resIds) { for (int resId : resIds) {
findPreference(getString(resId)).setOnPreferenceChangeListener((preference, newValue) -> { findPreference(getString(resId)).setOnPreferenceChangeListener((preference, newValue) -> {
JobIntentService.enqueueWork(this, NotificationSchedulerIntentService.class, JobManager.JOB_ID_NOTIFICATION_SCHEDULER, new Intent()); NotificationSchedulerIntentService.enqueueWork(this);
return true; return true;
}); });
} }
@ -108,7 +105,7 @@ public class ReminderPreferences extends InjectingPreferenceActivity {
private void resetGeofencesOnChange(int... resIds) { private void resetGeofencesOnChange(int... resIds) {
for (int resId : resIds) { for (int resId : resIds) {
findPreference(getString(resId)).setOnPreferenceChangeListener((preference, newValue) -> { findPreference(getString(resId)).setOnPreferenceChangeListener((preference, newValue) -> {
JobIntentService.enqueueWork(this, GeofenceSchedulingIntentService.class, JobManager.JOB_ID_GEOFENCE_SCHEDULING, new Intent()); GeofenceSchedulingIntentService.enqueueWork(this);
return true; return true;
}); });
} }

@ -6,7 +6,6 @@
package com.todoroo.astrid.service; package com.todoroo.astrid.service;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteException;
import android.os.Environment; 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.ListMultimap;
import com.google.common.collect.Multimaps; import com.google.common.collect.Multimaps;
import com.todoroo.andlib.sql.Criterion; import com.todoroo.andlib.sql.Criterion;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.dao.Database; import com.todoroo.astrid.dao.Database;
import com.todoroo.astrid.dao.MetadataDao; import com.todoroo.astrid.dao.MetadataDao;
import com.todoroo.astrid.dao.TagDataDao; import com.todoroo.astrid.dao.TagDataDao;
@ -52,14 +50,14 @@ public class StartupService {
private final TagDataDao tagDataDao; private final TagDataDao tagDataDao;
private final TagService tagService; private final TagService tagService;
private final MetadataDao metadataDao; private final MetadataDao metadataDao;
private final BackgroundScheduler backgroundScheduler;
private final LocalBroadcastManager localBroadcastManager; private final LocalBroadcastManager localBroadcastManager;
private final Context context;
@Inject @Inject
public StartupService(Database database, Preferences preferences, TaskDeleter taskDeleter, public StartupService(Database database, Preferences preferences, TaskDeleter taskDeleter,
Tracker tracker, TagDataDao tagDataDao, TagService tagService, Tracker tracker, TagDataDao tagDataDao, TagService tagService,
MetadataDao metadataDao, BackgroundScheduler backgroundScheduler, MetadataDao metadataDao, LocalBroadcastManager localBroadcastManager,
LocalBroadcastManager localBroadcastManager) { @ForApplication Context context) {
this.database = database; this.database = database;
this.preferences = preferences; this.preferences = preferences;
this.taskDeleter = taskDeleter; this.taskDeleter = taskDeleter;
@ -67,8 +65,8 @@ public class StartupService {
this.tagDataDao = tagDataDao; this.tagDataDao = tagDataDao;
this.tagService = tagService; this.tagService = tagService;
this.metadataDao = metadataDao; this.metadataDao = metadataDao;
this.backgroundScheduler = backgroundScheduler;
this.localBroadcastManager = localBroadcastManager; this.localBroadcastManager = localBroadcastManager;
this.context = context;
} }
/** Called when this application is started up */ /** Called when this application is started up */
@ -97,12 +95,10 @@ public class StartupService {
preferences.setDefaults(); preferences.setDefaults();
} }
// perform startup activities in a background thread BackgroundScheduler.enqueueWork(context);
new Thread(() -> {
taskDeleter.deleteTasksWithEmptyTitles(null);
}).start();
backgroundScheduler.scheduleEverything(); // perform startup activities in a background thread
new Thread(() -> taskDeleter.deleteTasksWithEmptyTitles(null)).start();
} }
private void upgrade(int from, int to) { private void upgrade(int from, int to) {

@ -115,7 +115,7 @@ public class TimerPlugin {
.setAutoCancel(false) .setAutoCancel(false)
.setOngoing(true) .setOngoing(true)
.build(); .build();
notificationManager.notify(Constants.NOTIFICATION_TIMER, notification); notificationManager.notify(Constants.NOTIFICATION_TIMER, notification, false, false, false);
} }
} }
} }

@ -1,15 +1,9 @@
package org.tasks; package org.tasks;
import android.annotation.SuppressLint;
import android.app.Notification; import android.app.Notification;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.ContentUris;
import android.content.Context; import android.content.Context;
import android.content.Intent; 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.support.v4.app.NotificationCompat;
import android.text.TextUtils; import android.text.TextUtils;
@ -23,19 +17,23 @@ import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.reminders.ReminderService; import com.todoroo.astrid.reminders.ReminderService;
import com.todoroo.astrid.voice.VoiceOutputAssistant; import com.todoroo.astrid.voice.VoiceOutputAssistant;
import org.tasks.db.AppDatabase;
import org.tasks.injection.ForApplication; import org.tasks.injection.ForApplication;
import org.tasks.jobs.JobQueueEntry;
import org.tasks.notifications.AudioManager; import org.tasks.notifications.AudioManager;
import org.tasks.notifications.NotificationManager; import org.tasks.notifications.NotificationManager;
import org.tasks.notifications.TelephonyManager; import org.tasks.notifications.TelephonyManager;
import org.tasks.preferences.Preferences; import org.tasks.preferences.Preferences;
import org.tasks.receivers.CompleteTaskReceiver; import org.tasks.receivers.CompleteTaskReceiver;
import org.tasks.reminders.MissedCallActivity;
import org.tasks.reminders.NotificationActivity; import org.tasks.reminders.NotificationActivity;
import org.tasks.reminders.SnoozeActivity; import org.tasks.reminders.SnoozeActivity;
import org.tasks.reminders.SnoozeDialog; import org.tasks.reminders.SnoozeDialog;
import org.tasks.reminders.SnoozeOption; 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; 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_MULTIPLE_TASK;
import static android.content.Intent.FLAG_ACTIVITY_NEW_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; import static org.tasks.time.DateTimeUtils.currentTimeMillis;
public class Notifier { public class Notifier {
private static long lastNotificationSound = 0L;
private final Context context; private final Context context;
private final TaskDao taskDao; private final TaskDao taskDao;
private final NotificationManager notificationManager; private final NotificationManager notificationManager;
@ -57,12 +55,13 @@ public class Notifier {
private final AudioManager audioManager; private final AudioManager audioManager;
private final VoiceOutputAssistant voiceOutputAssistant; private final VoiceOutputAssistant voiceOutputAssistant;
private final Preferences preferences; private final Preferences preferences;
private final AppDatabase appDatabase;
@Inject @Inject
public Notifier(@ForApplication Context context, TaskDao taskDao, public Notifier(@ForApplication Context context, TaskDao taskDao,
NotificationManager notificationManager, TelephonyManager telephonyManager, NotificationManager notificationManager, TelephonyManager telephonyManager,
AudioManager audioManager, VoiceOutputAssistant voiceOutputAssistant, AudioManager audioManager, VoiceOutputAssistant voiceOutputAssistant,
Preferences preferences) { Preferences preferences, AppDatabase appDatabase) {
this.context = context; this.context = context;
this.taskDao = taskDao; this.taskDao = taskDao;
this.notificationManager = notificationManager; this.notificationManager = notificationManager;
@ -70,65 +69,7 @@ public class Notifier {
this.audioManager = audioManager; this.audioManager = audioManager;
this.voiceOutputAssistant = voiceOutputAssistant; this.voiceOutputAssistant = voiceOutputAssistant;
this.preferences = preferences; this.preferences = preferences;
} this.appDatabase = appDatabase;
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;
} }
public void triggerFilterNotification(final Filter filter) { public void triggerFilterNotification(final Filter filter) {
@ -146,56 +87,56 @@ public class Notifier {
intent.putExtra(TaskListActivity.OPEN_FILTER, filter); intent.putExtra(TaskListActivity.OPEN_FILTER, filter);
PendingIntent pendingIntent = PendingIntent.getActivity(context, (title + query).hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT); PendingIntent pendingIntent = PendingIntent.getActivity(context, (title + query).hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT);
Notification notification = new NotificationCompat.Builder(context, NotificationManager.DEFAULT_NOTIFICATION_CHANNEL) NotificationCompat.Builder notification = new NotificationCompat.Builder(context, NotificationManager.NOTIFICATION_CHANNEL_TASKER)
.setSmallIcon(R.drawable.ic_check_white_24dp)
.setCategory(NotificationCompat.CATEGORY_REMINDER) .setCategory(NotificationCompat.CATEGORY_REMINDER)
.setTicker(title) .setTicker(title)
.setWhen(currentTimeMillis())
.setContentTitle(title) .setContentTitle(title)
.setContentText(subtitle) .setContentText(subtitle)
.setContentIntent(pendingIntent) .setContentIntent(pendingIntent)
.setAutoCancel(true) .setAutoCancel(true)
.setVibrate(preferences.isVibrationEnabled() ? preferences.getVibrationPattern() : null) .setPriority(NotificationCompat.PRIORITY_HIGH)
.setLights(preferences.getLEDColor(), preferences.isLEDNotificationEnabled() ? 700 : 0, 5000) .setWhen(currentTimeMillis())
.setPriority(preferences.getNotificationPriority()) .setShowWhen(true);
.build();
notificationManager.notify(
activateNotification(1, (title + query).hashCode(), notification, null); (title + query).hashCode(),
notification.build(),
true,
false,
false);
} }
public void triggerTaskNotification(long id, int type) { public void triggerTaskNotification(long id, int type) {
if (!showNotification(id, type)) { org.tasks.notifications.Notification notification = new org.tasks.notifications.Notification();
notificationManager.cancel(id); notification.taskId = id;
} notification.type = type;
notification.timestamp = currentTimeMillis();
triggerNotifications(Collections.singletonList(notification), true);
} }
private boolean showNotification(final long id, final int type) { private NotificationCompat.Builder getTaskNotification(org.tasks.notifications.Notification notification) {
Task task; long id = notification.taskId;
try { int type = notification.type;
task = taskDao.fetch(id, Task.ID, Task.TITLE, Task.HIDE_UNTIL, Task.COMPLETION_DATE, long when = notification.timestamp;
Task.DUE_DATE, Task.DELETION_DATE, Task.REMINDER_FLAGS, Task.NOTES); Task task = taskDao.fetch(id);
if (task == null) { if (task == null) {
throw new IllegalArgumentException("cound not find item with id"); //$NON-NLS-1$ Timber.e("Could not find %s", id);
} return null;
} catch (Exception e) {
Timber.e(e, e.getMessage());
return false;
} }
// you're done, or not yours - don't sound, do delete // you're done, or not yours - don't sound, do delete
if (task.isCompleted() || task.isDeleted()) { if (task.isCompleted() || task.isDeleted()) {
return false; return null;
} }
// new task edit in progress // new task edit in progress
if (TextUtils.isEmpty(task.getTitle())) { if (TextUtils.isEmpty(task.getTitle())) {
return false; return null;
} }
// it's hidden - don't sound, don't delete // it's hidden - don't sound, don't delete
if (task.isHidden() && type == ReminderService.TYPE_RANDOM) { if (task.isHidden() && type == ReminderService.TYPE_RANDOM) {
return false; return null;
} }
// task due date was changed, but alarm wasn't rescheduled // 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; !task.hasDueTime() && task.getDueDate() - DateUtilities.now() > DateUtilities.ONE_DAY;
if ((type == ReminderService.TYPE_DUE || type == ReminderService.TYPE_OVERDUE) && if ((type == ReminderService.TYPE_DUE || type == ReminderService.TYPE_OVERDUE) &&
(!task.hasDueDate() || dueInFuture)) { (!task.hasDueDate() || dueInFuture)) {
return false; return null;
} }
// read properties // read properties
final String taskTitle = task.getTitle(); final String taskTitle = task.getTitle();
final String taskDescription = task.getNotes(); final String taskDescription = task.getNotes();
boolean nonstopMode = task.isNotifyModeNonstop();
boolean ringFiveMode = task.isNotifyModeFive();
int ringTimes = nonstopMode ? -1 : (ringFiveMode ? 5 : 1);
// update last reminder time // update last reminder time
task.setReminderLast(DateUtilities.now()); task.setReminderLast(when);
taskDao.saveExisting(task); 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); final Intent intent = new Intent(context, NotificationActivity.class);
intent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK); 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_TASK_ID, id);
intent.putExtra(NotificationActivity.EXTRA_TITLE, taskTitle); intent.putExtra(NotificationActivity.EXTRA_TITLE, taskTitle);
// don't ring multiple times if random reminder NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NotificationManager.NOTIFICATION_CHANNEL_DEFAULT)
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)
.setCategory(NotificationCompat.CATEGORY_REMINDER) .setCategory(NotificationCompat.CATEGORY_REMINDER)
.setTicker(taskTitle) .setTicker(taskTitle)
.setWhen(currentTimeMillis())
.setContentTitle(taskTitle) .setContentTitle(taskTitle)
.setContentText(text) .setContentText(appName)
.setVibrate(preferences.isVibrationEnabled() ? preferences.getVibrationPattern() : null) .setGroup(GROUP_KEY)
.setLights(preferences.getLEDColor(), preferences.isLEDNotificationEnabled() ? 700 : 0, 5000) .setSmallIcon(R.drawable.ic_check_white_24dp)
.setPriority(preferences.getNotificationPriority()) .setWhen(when)
.setShowWhen(true)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(PendingIntent.getActivity(context, (int) id, intent, PendingIntent.FLAG_UPDATE_CURRENT)); .setContentIntent(PendingIntent.getActivity(context, (int) id, intent, PendingIntent.FLAG_UPDATE_CURRENT));
if (!Strings.isNullOrEmpty(taskDescription)) { if (!Strings.isNullOrEmpty(taskDescription)) {
builder.setStyle(new NotificationCompat.BigTextStyle().bigText(taskDescription)); builder.setStyle(new NotificationCompat.BigTextStyle().bigText(taskDescription));
} }
@ -271,79 +205,59 @@ public class Notifier {
.build()); .build());
} }
builder.addAction(completeAction) return builder.addAction(completeAction)
.addAction(R.drawable.ic_snooze_white_24dp, context.getResources().getString(R.string.rmd_NoA_snooze), snoozePendingIntent) .addAction(R.drawable.ic_snooze_white_24dp, context.getResources().getString(R.string.rmd_NoA_snooze), snoozePendingIntent)
.extend(wearableExtender); .extend(wearableExtender);
activateNotification(ringTimes, (int) id, builder.build(), taskTitle);
return true;
} }
@SuppressLint("NewApi") public void restoreNotifications() {
private void activateNotification(int ringTimes, int notificationId, Notification notification, String text) { triggerNotifications(appDatabase.notificationDao().getAll(), false);
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);
if (ringTimes != 1) { public void triggerTaskNotifications(List<? extends JobQueueEntry> entries) {
notification.audioStreamType = android.media.AudioManager.STREAM_ALARM; triggerNotifications(transform(entries, JobQueueEntry::toNotification), true);
}
// insistent rings until notification is disabled public void triggerNotifications(List<org.tasks.notifications.Notification> entries, boolean alert) {
if (ringTimes < 0) { Map<org.tasks.notifications.Notification, Notification> notifications = new LinkedHashMap<>();
notification.flags |= Notification.FLAG_INSISTENT; boolean ringFiveTimes = false;
voiceReminder = 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;
} }
if (entry.type != ReminderService.TYPE_RANDOM) {
} else { ringFiveTimes |= task.isNotifyModeFive();
notification.audioStreamType = android.media.AudioManager.STREAM_NOTIFICATION; ringNonstop |= task.isNotifyModeNonstop();
} }
NotificationCompat.Builder notification = getTaskNotification(entry);
boolean soundIntervalOk = checkLastNotificationSound(); if (notification != null) {
notification.setGroupAlertBehavior(alert && (atLeastNougat() ? entries.size() == 1 : i == entries.size() - 1)
if (telephonyManager.callStateIdle()) { ? NotificationCompat.GROUP_ALERT_CHILDREN
String notificationPreference = preferences.getStringValue(R.string.p_rmd_ringtone); : NotificationCompat.GROUP_ALERT_SUMMARY);
if (audioManager.notificationsMuted()) { notifications.put(entry, notification.build());
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 (!telephonyManager.callStateIdle()) { if (notifications.isEmpty()) {
notification.sound = null; return;
notification.vibrate = null; } else {
voiceReminder = false; Timber.d("Triggering %s", notifications.keySet());
} }
for (int i = 0; i < Math.max(ringTimes, 1); i++) { notificationManager.notifyTasks(notifications, alert, ringNonstop, ringFiveTimes);
notificationManager.notify(notificationId, notification);
AndroidUtilities.sleepDeep(500);
}
if (voiceReminder) {
AndroidUtilities.sleepDeep(2000);
voiceOutputAssistant.speak(text);
}
}
/** if (alert &&
* @return true if notification should sound preferences.getBoolean(R.string.p_voiceRemindersEnabled, false) &&
*/ !ringNonstop &&
private static boolean checkLastNotificationSound() { !audioManager.notificationsMuted() &&
long now = DateUtilities.now(); telephonyManager.callStateIdle()) {
if (now - lastNotificationSound > 10000) { for (Notification notification : notifications.values()) {
lastNotificationSound = now; AndroidUtilities.sleepDeep(2000);
return true; voiceOutputAssistant.speak(notification.tickerText.toString());
}
} }
return false;
} }
} }

@ -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();
}

@ -1,9 +1,11 @@
package org.tasks.injection; package org.tasks.injection;
import android.arch.persistence.room.Room;
import android.content.Context; import android.content.Context;
import org.tasks.ErrorReportingSingleThreadExecutor; import org.tasks.ErrorReportingSingleThreadExecutor;
import org.tasks.analytics.Tracker; import org.tasks.analytics.Tracker;
import org.tasks.db.AppDatabase;
import org.tasks.locale.Locale; import org.tasks.locale.Locale;
import org.tasks.themes.ThemeCache; import org.tasks.themes.ThemeCache;
import org.tasks.ui.CheckBoxes; import org.tasks.ui.CheckBoxes;
@ -56,4 +58,10 @@ public class ApplicationModule {
public WidgetCheckBoxes getWidgetCheckBoxes(CheckBoxes checkBoxes) { public WidgetCheckBoxes getWidgetCheckBoxes(CheckBoxes checkBoxes) {
return newWidgetCheckBoxes(checkBoxes); return newWidgetCheckBoxes(checkBoxes);
} }
@Provides
@ApplicationScope
public AppDatabase getAppDatabase() {
return Room.databaseBuilder(context, AppDatabase.class, "app-database").build();
}
} }

@ -5,17 +5,15 @@ import org.tasks.jobs.BackupJob;
import org.tasks.jobs.MidnightRefreshJob; import org.tasks.jobs.MidnightRefreshJob;
import org.tasks.jobs.RefreshJob; import org.tasks.jobs.RefreshJob;
import org.tasks.location.GeofenceTransitionsIntentService; import org.tasks.location.GeofenceTransitionsIntentService;
import org.tasks.scheduling.BackgroundScheduler;
import org.tasks.scheduling.CalendarNotificationIntentService; import org.tasks.scheduling.CalendarNotificationIntentService;
import org.tasks.scheduling.GeofenceSchedulingIntentService; import org.tasks.scheduling.GeofenceSchedulingIntentService;
import org.tasks.scheduling.NotificationSchedulerIntentService; import org.tasks.scheduling.NotificationSchedulerIntentService;
import org.tasks.scheduling.SchedulerIntentService;
import dagger.Subcomponent; import dagger.Subcomponent;
@Subcomponent(modules = IntentServiceModule.class) @Subcomponent(modules = IntentServiceModule.class)
public interface IntentServiceComponent { public interface IntentServiceComponent {
void inject(SchedulerIntentService schedulerIntentService);
void inject(GeofenceSchedulingIntentService geofenceSchedulingIntentService); void inject(GeofenceSchedulingIntentService geofenceSchedulingIntentService);
void inject(CalendarNotificationIntentService calendarNotificationIntentService); void inject(CalendarNotificationIntentService calendarNotificationIntentService);
@ -31,4 +29,6 @@ public interface IntentServiceComponent {
void inject(MidnightRefreshJob midnightRefreshJob); void inject(MidnightRefreshJob midnightRefreshJob);
void inject(RefreshJob refreshJob); void inject(RefreshJob refreshJob);
void inject(BackgroundScheduler backgroundScheduler);
} }

@ -2,6 +2,11 @@ package org.tasks.jobs;
import com.todoroo.astrid.alarms.AlarmFields; import com.todoroo.astrid.alarms.AlarmFields;
import com.todoroo.astrid.data.Metadata; 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 { public class Alarm implements JobQueueEntry {
private final long alarmId; private final long alarmId;
@ -32,6 +37,15 @@ public class Alarm implements JobQueueEntry {
return time; return time;
} }
@Override
public Notification toNotification() {
Notification notification = new Notification();
notification.taskId = taskId;
notification.type = ReminderService.TYPE_ALARM;
notification.timestamp = currentTimeMillis();
return notification;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

@ -20,12 +20,12 @@ import static org.tasks.time.DateTimeUtils.printTimestamp;
public class JobManager { public class JobManager {
static final int JOB_ID_REFRESH = 1; static final int JOB_ID_REFRESH = 1;
public static final int JOB_ID_BACKGROUND_SCHEDULER = 2;
static final int JOB_ID_NOTIFICATION = 3; static final int JOB_ID_NOTIFICATION = 3;
public static final int JOB_ID_GEOFENCE_TRANSITION = 4; public static final int JOB_ID_GEOFENCE_TRANSITION = 4;
public static final int JOB_ID_GEOFENCE_SCHEDULING = 5; public static final int JOB_ID_GEOFENCE_SCHEDULING = 5;
static final int JOB_ID_MIDNIGHT_REFRESH = 6; static final int JOB_ID_MIDNIGHT_REFRESH = 6;
static final int JOB_ID_BACKUP = 7; 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_NOTIFICATION_SCHEDULER = 9;
public static final int JOB_ID_CALENDAR_NOTIFICATION = 10; public static final int JOB_ID_CALENDAR_NOTIFICATION = 10;
@ -38,7 +38,7 @@ public class JobManager {
this.alarmManager = alarmManager; this.alarmManager = alarmManager;
} }
public void schedule(String tag, long time) { void schedule(String tag, long time) {
Timber.d("%s: %s", tag, printTimestamp(time)); Timber.d("%s: %s", tag, printTimestamp(time));
alarmManager.wakeup(adjust(time), getPendingIntent(tag)); alarmManager.wakeup(adjust(time), getPendingIntent(tag));
} }

@ -72,10 +72,6 @@ public class JobQueue {
return result; return result;
} }
public synchronized boolean remove(JobQueueEntry entry) {
return jobs.remove(entry.getTime(), entry);
}
synchronized void scheduleNext() { synchronized void scheduleNext() {
scheduleNext(false); scheduleNext(false);
} }
@ -106,4 +102,12 @@ public class JobQueue {
List<JobQueueEntry> getJobs() { List<JobQueueEntry> getJobs() {
return ImmutableList.copyOf(jobs.values()); return ImmutableList.copyOf(jobs.values());
} }
public synchronized boolean remove(List<? extends JobQueueEntry> entries) {
boolean success = true;
for (JobQueueEntry entry : entries) {
success &= jobs.remove(entry.getTime(), entry);
}
return success;
}
} }

@ -1,7 +1,11 @@
package org.tasks.jobs; package org.tasks.jobs;
import org.tasks.notifications.Notification;
public interface JobQueueEntry { public interface JobQueueEntry {
long getId(); long getId();
long getTime(); long getTime();
Notification toNotification();
} }

@ -6,13 +6,14 @@ import android.content.Intent;
import android.support.v4.app.JobIntentService; import android.support.v4.app.JobIntentService;
import com.todoroo.astrid.dao.TaskDao; 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.Notifier;
import org.tasks.injection.IntentServiceComponent; import org.tasks.injection.IntentServiceComponent;
import org.tasks.preferences.Preferences; import org.tasks.preferences.Preferences;
import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
public class NotificationJob extends Job { public class NotificationJob extends Job {
@ -34,18 +35,11 @@ public class NotificationJob extends Job {
@Override @Override
protected void run() { protected void run() {
if (!preferences.isCurrentlyQuietHours()) { if (!preferences.isCurrentlyQuietHours()) {
for (JobQueueEntry entry : jobQueue.getOverdueJobs()) { List<? extends JobQueueEntry> overdueJobs = jobQueue.getOverdueJobs();
if (entry instanceof Alarm) { notifier.triggerTaskNotifications(overdueJobs);
Alarm alarm = (Alarm) entry; boolean success = jobQueue.remove(overdueJobs);
Task task = taskDao.fetch(alarm.getTaskId(), Task.REMINDER_LAST); if (BuildConfig.DEBUG && !success) {
if (task != null && task.getReminderLast() < alarm.getTime()) { throw new RuntimeException("Failed to remove jobs from queue");
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);
} }
} }
} }

@ -1,5 +1,9 @@
package org.tasks.jobs; package org.tasks.jobs;
import org.tasks.notifications.Notification;
import static org.tasks.time.DateTimeUtils.currentTimeMillis;
public class Reminder implements JobQueueEntry { public class Reminder implements JobQueueEntry {
private final long taskId; private final long taskId;
private final long time; private final long time;
@ -21,6 +25,15 @@ public class Reminder implements JobQueueEntry {
return time; return time;
} }
@Override
public Notification toNotification() {
Notification notification = new Notification();
notification.taskId = taskId;
notification.type = type;
notification.timestamp = currentTimeMillis();
return notification;
}
public int getType() { public int getType() {
return type; return type;
} }

@ -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 +
'}';
}
}

@ -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);
}
}

@ -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<Notification> getAll();
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertAll(List<Notification> notifications);
@Query("SELECT COUNT(*) FROM notification")
int count();
@Query("DELETE FROM notification WHERE task = :taskId")
void delete(long taskId);
}

@ -1,49 +1,154 @@
package org.tasks.notifications; package org.tasks.notifications;
import android.annotation.TargetApi;
import android.app.Notification; import android.app.Notification;
import android.app.NotificationChannel; import android.app.NotificationChannel;
import android.app.PendingIntent;
import android.content.Context; 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.R;
import org.tasks.db.AppDatabase;
import org.tasks.injection.ApplicationScope;
import org.tasks.injection.ForApplication; import org.tasks.injection.ForApplication;
import org.tasks.preferences.Preferences; import org.tasks.preferences.Preferences;
import java.util.ArrayList;
import java.util.Map;
import javax.inject.Inject; 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; import static com.todoroo.andlib.utility.AndroidUtilities.atLeastOreo;
@ApplicationScope
public class NotificationManager { 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 android.app.NotificationManager notificationManager;
private final AppDatabase appDatabase;
private final Context context;
private final Preferences preferences; private final Preferences preferences;
public static final String DEFAULT_NOTIFICATION_CHANNEL = "notifications";
@Inject @Inject
public NotificationManager(@ForApplication Context context, Preferences preferences) { public NotificationManager(@ForApplication Context context, Preferences preferences,
AppDatabase appDatabase) {
this.context = context;
this.preferences = preferences; this.preferences = preferences;
notificationManager = (android.app.NotificationManager) notificationManager = (android.app.NotificationManager)
context.getSystemService(Context.NOTIFICATION_SERVICE); context.getSystemService(Context.NOTIFICATION_SERVICE);
this.appDatabase = appDatabase;
if (atLeastOreo()) { if (atLeastOreo()) {
String channelName = context.getString(R.string.notifications); notificationManager.createNotificationChannel(createNotificationChannel(NOTIFICATION_CHANNEL_DEFAULT, R.string.notifications));
NotificationChannel notificationChannel = new NotificationChannel(DEFAULT_NOTIFICATION_CHANNEL, channelName, android.app.NotificationManager.IMPORTANCE_HIGH); notificationManager.createNotificationChannel(createNotificationChannel(NOTIFICATION_CHANNEL_CALLS, R.string.missed_calls));
notificationChannel.enableLights(true); notificationManager.createNotificationChannel(createNotificationChannel(NOTIFICATION_CHANNEL_TASKER, R.string.tasker_locale));
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);
} }
} }
@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) { public void cancel(long id) {
notificationManager.cancel((int) 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<org.tasks.notifications.Notification, Notification> notifications, boolean alert, boolean nonstop, boolean fiveTimes) {
appDatabase.notificationDao().insertAll(newArrayList(notifications.keySet()));
updateSummary(alert && notifications.size() > 1, nonstop, fiveTimes);
ArrayList<Map.Entry<org.tasks.notifications.Notification, Notification>> entries = newArrayList(notifications.entrySet());
int last = entries.size() - 1;
for (int i = 0; i < last; i++) {
Map.Entry<org.tasks.notifications.Notification, Notification> entry = entries.get(i);
notify(entry.getKey().taskId, entry.getValue(), false, false, false);
}
Map.Entry<org.tasks.notifications.Notification, Notification> 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)) { 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);
}
} }
} }
} }

@ -12,7 +12,7 @@ import org.tasks.R;
import org.tasks.files.FileExplore; import org.tasks.files.FileExplore;
import org.tasks.injection.ActivityComponent; import org.tasks.injection.ActivityComponent;
import org.tasks.injection.InjectingPreferenceActivity; import org.tasks.injection.InjectingPreferenceActivity;
import org.tasks.scheduling.BackgroundScheduler; import org.tasks.scheduling.CalendarNotificationIntentService;
import java.io.File; import java.io.File;
@ -31,7 +31,6 @@ public class MiscellaneousPreferences extends InjectingPreferenceActivity {
@Inject VoiceOutputAssistant voiceOutputAssistant; @Inject VoiceOutputAssistant voiceOutputAssistant;
@Inject ActivityPermissionRequestor permissionRequestor; @Inject ActivityPermissionRequestor permissionRequestor;
@Inject PermissionChecker permissionChecker; @Inject PermissionChecker permissionChecker;
@Inject BackgroundScheduler backgroundScheduler;
private CheckBoxPreference calendarReminderPreference; private CheckBoxPreference calendarReminderPreference;
@ -113,7 +112,7 @@ public class MiscellaneousPreferences extends InjectingPreferenceActivity {
return true; return true;
} }
if (permissionRequestor.requestCalendarPermissions()) { if (permissionRequestor.requestCalendarPermissions()) {
backgroundScheduler.scheduleCalendarNotifications(); CalendarNotificationIntentService.enqueueWork(this);
return true; return true;
} }
return false; return false;

@ -3,10 +3,12 @@ package org.tasks.preferences;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.res.Resources; import android.content.res.Resources;
import android.media.RingtoneManager;
import android.net.Uri;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.text.TextUtils; import android.text.TextUtils;
import com.google.common.base.Strings;
import com.todoroo.astrid.activity.BeastModePreferences; import com.todoroo.astrid.activity.BeastModePreferences;
import com.todoroo.astrid.api.AstridApiConstants; import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.core.SortHelper; import com.todoroo.astrid.core.SortHelper;
@ -15,7 +17,6 @@ import com.todoroo.astrid.data.TaskAttachment;
import org.tasks.R; import org.tasks.R;
import org.tasks.injection.ForApplication; import org.tasks.injection.ForApplication;
import org.tasks.themes.ThemeCache;
import org.tasks.time.DateTime; import org.tasks.time.DateTime;
import java.io.File; import java.io.File;
@ -26,7 +27,6 @@ import javax.inject.Inject;
import timber.log.Timber; import timber.log.Timber;
import static android.content.SharedPreferences.Editor; import static android.content.SharedPreferences.Editor;
import static com.todoroo.andlib.utility.AndroidUtilities.atLeastOreo;
public class Preferences { public class Preferences {
@ -38,14 +38,11 @@ public class Preferences {
private final PermissionChecker permissionChecker; private final PermissionChecker permissionChecker;
private final SharedPreferences prefs; private final SharedPreferences prefs;
private final SharedPreferences publicPrefs; private final SharedPreferences publicPrefs;
private final ThemeCache themeCache;
@Inject @Inject
public Preferences(@ForApplication Context context, PermissionChecker permissionChecker, public Preferences(@ForApplication Context context, PermissionChecker permissionChecker) {
ThemeCache themeCache) {
this.context = context; this.context = context;
this.permissionChecker = permissionChecker; this.permissionChecker = permissionChecker;
this.themeCache = themeCache;
prefs = PreferenceManager.getDefaultSharedPreferences(context); prefs = PreferenceManager.getDefaultSharedPreferences(context);
publicPrefs = context.getSharedPreferences(AstridApiConstants.PUBLIC_PREFS, Context.MODE_PRIVATE); publicPrefs = context.getSharedPreferences(AstridApiConstants.PUBLIC_PREFS, Context.MODE_PRIVATE);
} }
@ -88,16 +85,8 @@ public class Preferences {
return time; return time;
} }
public boolean isVibrationEnabled() {
return atLeastOreo() || getBoolean(R.string.p_rmd_vibrate, true);
}
public boolean isLEDNotificationEnabled() { public boolean isLEDNotificationEnabled() {
return atLeastOreo() || getBoolean(R.string.p_led_notification, true); return getBoolean(R.string.p_led_notification, true);
}
public int getLEDColor() {
return R.color.led_green;
} }
public boolean quietHoursEnabled() { public boolean quietHoursEnabled() {
@ -141,13 +130,15 @@ public class Preferences {
return defaultCalendar != null && !defaultCalendar.equals("-1") && !defaultCalendar.equals("0"); return defaultCalendar != null && !defaultCalendar.equals("-1") && !defaultCalendar.equals("0");
} }
public boolean isTrackingEnabled() { public Uri getRingtone() {
return getBoolean(R.string.p_collect_statistics, true); String ringtone = getStringValue(R.string.p_rmd_ringtone);
return Strings.isNullOrEmpty(ringtone)
? RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
: Uri.parse(ringtone);
} }
public int getNotificationPriority() { public boolean isTrackingEnabled() {
return Math.max(NotificationCompat.PRIORITY_MIN, Math.min(NotificationCompat.PRIORITY_MAX, return getBoolean(R.string.p_collect_statistics, true);
getIntegerFromString(R.string.p_notification_priority, NotificationCompat.PRIORITY_HIGH)));
} }
public String getDefaultCalendar() { public String getDefaultCalendar() {
@ -411,8 +402,8 @@ public class Preferences {
return directory; return directory;
} }
public long[] getVibrationPattern() { public boolean isVibrationEnabled() {
return new long[] {0, 333, 333, 333}; return getBoolean(R.string.p_rmd_vibrate, true);
} }
public void remove(int resId) { public void remove(int resId) {

@ -5,27 +5,20 @@ import android.content.Intent;
import org.tasks.injection.BroadcastComponent; import org.tasks.injection.BroadcastComponent;
import org.tasks.injection.InjectingBroadcastReceiver; import org.tasks.injection.InjectingBroadcastReceiver;
import org.tasks.scheduling.BackgroundScheduler;
import javax.inject.Inject;
import timber.log.Timber; import timber.log.Timber;
public class BootCompletedReceiver extends InjectingBroadcastReceiver { public class BootCompletedReceiver extends InjectingBroadcastReceiver {
@Inject BackgroundScheduler backgroundScheduler;
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
super.onReceive(context, intent); super.onReceive(context, intent);
if (!intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) { if (!Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
return; return;
} }
Timber.d("onReceive(context, %s)", intent); Timber.d("onReceive(context, %s)", intent);
backgroundScheduler.scheduleEverything();
} }
@Override @Override

@ -5,27 +5,20 @@ import android.content.Intent;
import org.tasks.injection.BroadcastComponent; import org.tasks.injection.BroadcastComponent;
import org.tasks.injection.InjectingBroadcastReceiver; import org.tasks.injection.InjectingBroadcastReceiver;
import org.tasks.scheduling.BackgroundScheduler;
import javax.inject.Inject;
import timber.log.Timber; import timber.log.Timber;
public class MyPackageReplacedReceiver extends InjectingBroadcastReceiver { public class MyPackageReplacedReceiver extends InjectingBroadcastReceiver {
@Inject BackgroundScheduler backgroundScheduler;
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
super.onReceive(context, intent); super.onReceive(context, intent);
if (!intent.getAction().equals(Intent.ACTION_MY_PACKAGE_REPLACED)) { if (!Intent.ACTION_MY_PACKAGE_REPLACED.equals(intent.getAction())) {
return; return;
} }
Timber.d("onReceive(context, %s)", intent); Timber.d("onReceive(context, %s)", intent);
backgroundScheduler.scheduleEverything();
} }
@Override @Override

@ -2,29 +2,56 @@ package org.tasks.scheduling;
import android.content.Context; import android.content.Context;
import android.content.Intent; 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.ForApplication;
import org.tasks.injection.InjectingJobIntentService;
import org.tasks.injection.IntentServiceComponent;
import org.tasks.jobs.JobManager; import org.tasks.jobs.JobManager;
import javax.inject.Inject; import javax.inject.Inject;
public class BackgroundScheduler { import timber.log.Timber;
private final Context context;
import static java.lang.System.currentTimeMillis;
@Inject public class BackgroundScheduler extends InjectingJobIntentService {
public BackgroundScheduler(@ForApplication Context context) {
this.context = context; public static void enqueueWork(Context context) {
BackgroundScheduler.enqueueWork(context, BackgroundScheduler.class, JobManager.JOB_ID_BACKGROUND_SCHEDULER, new Intent());
} }
public void scheduleEverything() { @Inject @ForApplication Context context;
JobIntentService.enqueueWork(context, GeofenceSchedulingIntentService.class, JobManager.JOB_ID_GEOFENCE_SCHEDULING, new Intent()); @Inject TaskDao taskDao;
JobIntentService.enqueueWork(context, SchedulerIntentService.class, JobManager.JOB_ID_SCHEDULER, new Intent()); @Inject JobManager jobManager;
JobIntentService.enqueueWork(context, NotificationSchedulerIntentService.class, JobManager.JOB_ID_NOTIFICATION_SCHEDULER, new Intent()); @Inject RefreshScheduler refreshScheduler;
scheduleCalendarNotifications();
@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() { @Override
JobIntentService.enqueueWork(context, CalendarNotificationIntentService.class, JobManager.JOB_ID_CALENDAR_NOTIFICATION, new Intent()); protected void inject(IntentServiceComponent component) {
component.inject(this);
} }
} }

@ -26,10 +26,14 @@ import timber.log.Timber;
public class CalendarNotificationIntentService extends RecurringIntervalIntentService { 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 { public static class Broadcast extends BroadcastReceiver {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
JobIntentService.enqueueWork(context, CalendarNotificationIntentService.class, JobManager.JOB_ID_CALENDAR_NOTIFICATION, new Intent()); enqueueWork(context);
} }
} }

@ -1,9 +1,12 @@
package org.tasks.scheduling; package org.tasks.scheduling;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.support.v4.app.JobIntentService;
import org.tasks.injection.InjectingJobIntentService; import org.tasks.injection.InjectingJobIntentService;
import org.tasks.injection.IntentServiceComponent; import org.tasks.injection.IntentServiceComponent;
import org.tasks.jobs.JobManager;
import org.tasks.location.GeofenceService; import org.tasks.location.GeofenceService;
import javax.inject.Inject; import javax.inject.Inject;
@ -12,13 +15,17 @@ import timber.log.Timber;
public class GeofenceSchedulingIntentService extends InjectingJobIntentService { 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; @Inject GeofenceService geofenceService;
@Override @Override
protected void onHandleWork(Intent intent) { protected void onHandleWork(Intent intent) {
super.onHandleWork(intent); super.onHandleWork(intent);
Timber.d("onHandleIntent(%s)", intent); Timber.d("onHandleWork(%s)", intent);
geofenceService.cancelGeofences(); geofenceService.cancelGeofences();
geofenceService.setupGeofences(); geofenceService.setupGeofences();

@ -1,13 +1,17 @@
package org.tasks.scheduling; package org.tasks.scheduling;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.support.v4.app.JobIntentService;
import com.todoroo.astrid.alarms.AlarmService; import com.todoroo.astrid.alarms.AlarmService;
import com.todoroo.astrid.dao.TaskDao; import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.reminders.ReminderService; import com.todoroo.astrid.reminders.ReminderService;
import org.tasks.Notifier;
import org.tasks.injection.InjectingJobIntentService; import org.tasks.injection.InjectingJobIntentService;
import org.tasks.injection.IntentServiceComponent; import org.tasks.injection.IntentServiceComponent;
import org.tasks.jobs.JobManager;
import org.tasks.jobs.JobQueue; import org.tasks.jobs.JobQueue;
import javax.inject.Inject; import javax.inject.Inject;
@ -16,19 +20,25 @@ import timber.log.Timber;
public class NotificationSchedulerIntentService extends InjectingJobIntentService { 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 AlarmService alarmService;
@Inject ReminderService reminderService; @Inject ReminderService reminderService;
@Inject TaskDao taskDao; @Inject TaskDao taskDao;
@Inject JobQueue jobQueue; @Inject JobQueue jobQueue;
@Inject Notifier notifier;
@Override @Override
protected void onHandleWork(Intent intent) { protected void onHandleWork(Intent intent) {
super.onHandleWork(intent); super.onHandleWork(intent);
Timber.d("onHandleIntent(%s)", intent); Timber.d("onHandleWork(%s)", intent);
jobQueue.clear(); jobQueue.clear();
notifier.restoreNotifications();
reminderService.scheduleAllAlarms(taskDao); reminderService.scheduleAllAlarms(taskDao);
alarmService.scheduleAllAlarms(); alarmService.scheduleAllAlarms();
} }

@ -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);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B

@ -366,7 +366,6 @@
<string name="widget_settings">Paramètres du Widget</string> <string name="widget_settings">Paramètres du Widget</string>
<string name="widget_header_settings">Paramètres de l\'en-tête</string> <string name="widget_header_settings">Paramètres de l\'en-tête</string>
<string name="widget_row_settings">Paramètres de la ligne</string> <string name="widget_row_settings">Paramètres de la ligne</string>
<string name="notification_shade">Fréquence de notification</string>
<string name="sync_error_permissions">Permissions requises de Tasks.</string> <string name="sync_error_permissions">Permissions requises de Tasks.</string>
<string name="creating_new_list">Création d\'une nouvelle liste</string> <string name="creating_new_list">Création d\'une nouvelle liste</string>
<string name="deleting_list">Suppression de la liste</string> <string name="deleting_list">Suppression de la liste</string>

@ -195,7 +195,6 @@
<string name="p_show_completed_tasks">show_completed_tasks</string> <string name="p_show_completed_tasks">show_completed_tasks</string>
<string name="p_reverse_sort">reverse_sort</string> <string name="p_reverse_sort">reverse_sort</string>
<string name="p_manual_sort">manual_sort</string> <string name="p_manual_sort">manual_sort</string>
<string name="p_notification_priority">notification_priority</string>
<string-array name="TEA_control_sets_prefs"> <string-array name="TEA_control_sets_prefs">
<item>@string/TEA_ctrl_when_pref</item> <item>@string/TEA_ctrl_when_pref</item>

@ -1,6 +1,6 @@
#Thu Aug 10 09:35:33 CDT 2017 #Wed Aug 30 13:47:01 CDT 2017
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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

Loading…
Cancel
Save