Bundle and persist notifications

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

@ -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'

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

@ -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

@ -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

@ -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(

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

@ -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

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

@ -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 {

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

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

@ -240,6 +240,10 @@ public class AndroidUtilities {
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() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
}

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

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

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

@ -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) {

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

@ -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<? extends JobQueueEntry> 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<org.tasks.notifications.Notification> entries, boolean alert) {
Map<org.tasks.notifications.Notification, Notification> 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;
}
}

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

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

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

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

@ -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<JobQueueEntry> getJobs() {
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;
import org.tasks.notifications.Notification;
public interface JobQueueEntry {
long getId();
long getTime();
Notification toNotification();
}

@ -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<? extends JobQueueEntry> overdueJobs = jobQueue.getOverdueJobs();
notifier.triggerTaskNotifications(overdueJobs);
boolean success = jobQueue.remove(overdueJobs);
if (BuildConfig.DEBUG && !success) {
throw new RuntimeException("Failed to remove jobs from queue");
}
}
}

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

@ -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;
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<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)) {
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.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;

@ -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) {

@ -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

@ -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

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

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

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

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

@ -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_header_settings">Paramètres de l\'en-tête</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="creating_new_list">Création d\'une nouvelle 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_reverse_sort">reverse_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">
<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
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

Loading…
Cancel
Save