diff --git a/.travis.yml b/.travis.yml index 2e9508a12..ba1794182 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ android: - tools # https://github.com/travis-ci/travis-ci/issues/6040 - android-25 - platform-tools - - build-tools-25.0.2 + - build-tools-25.0.3 - extra-android-m2repository - extra-google-m2repository licenses: @@ -23,5 +23,5 @@ before_install: - adb shell input keyevent 82 & script: - - ./gradlew :lintGoogleplayProdDebug - - ./gradlew :connectedGoogleplayProdDebugAndroidTest + - ./gradlew :lintGoogleplayDebug + - ./gradlew :connectedGoogleplayDebugAndroidTest diff --git a/build.gradle b/build.gradle index 41489c050..2e133bed4 100644 --- a/build.gradle +++ b/build.gradle @@ -7,10 +7,13 @@ task wrapper(type: Wrapper) { buildscript { repositories { jcenter() + maven { + url 'https://maven.google.com' + } } dependencies { - classpath 'com.android.tools.build:gradle:2.3.0' + classpath 'com.android.tools.build:gradle:3.0.0-alpha3' } } @@ -26,18 +29,15 @@ android { } compileSdkVersion 25 - buildToolsVersion "25.0.2" + buildToolsVersion "25.0.3" defaultConfig { applicationId "org.tasks" - versionCode 448 - versionName "4.9.10" - minSdkVersion 15 + versionCode 457 + versionName "4.9.14" targetSdkVersion 25 + minSdkVersion 15 testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" - jackOptions { - enabled true - } } signingConfigs { @@ -47,7 +47,6 @@ android { compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 - incremental false } buildTypes { @@ -61,7 +60,7 @@ android { } } - flavorDimensions 'store', 'env' + flavorDimensions 'store' productFlavors { generic { @@ -73,13 +72,6 @@ android { amazon { dimension 'store' } - dev { - minSdkVersion 21 - dimension 'env' - } - prod { - dimension 'env' - } } if (project.hasProperty('keyAlias') && @@ -101,9 +93,10 @@ configurations { } final DAGGER_VERSION = '2.9' -final BUTTERKNIFE_VERSION = '8.5.1' -final GPS_VERSION = '10.0.1' -final SUPPORT_VERSION = '25.2.0' +final BUTTERKNIFE_VERSION = '8.6.0' +final GPS_VERSION = '10.2.6' +final SUPPORT_VERSION = '25.3.1' +final SUPPORT_ANNOTATIONS_VERSION = '26.0.0-alpha1' final STETHO_VERSION = '1.4.2' final TESTING_SUPPORT_VERSION = '0.5' @@ -114,7 +107,9 @@ dependencies { annotationProcessor "com.jakewharton:butterknife-compiler:${BUTTERKNIFE_VERSION}" compile "com.jakewharton:butterknife:${BUTTERKNIFE_VERSION}" - debugCompile "com.facebook.stetho:stetho:${STETHO_VERSION}" + debugCompile ("com.facebook.stetho:stetho:${STETHO_VERSION}") { + exclude group: 'com.google.code.findbugs', module: 'jsr305' + } debugCompile "com.facebook.stetho:stetho-timber:${STETHO_VERSION}@aar" debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5' //noinspection GradleCompatible @@ -128,6 +123,7 @@ dependencies { compile "com.android.support:cardview-v7:${SUPPORT_VERSION}" compile 'com.jakewharton.timber:timber:4.5.1' compile 'com.jakewharton.threetenabp:threetenabp:1.0.5' + //noinspection GradleDependency compile 'com.google.guava:guava:20.0' compile 'com.jakewharton:process-phoenix:1.1.1' compile 'com.google.android.apps.dashclock:dashclock-api:2.0.0' @@ -158,5 +154,5 @@ dependencies { androidTestCompile 'com.natpryce:make-it-easy:4.0.1' androidTestCompile "com.android.support.test:runner:${TESTING_SUPPORT_VERSION}" androidTestCompile "com.android.support.test:rules:${TESTING_SUPPORT_VERSION}" - androidTestCompile "com.android.support:support-annotations:${SUPPORT_VERSION}" + androidTestCompile "com.android.support:support-annotations:${SUPPORT_ANNOTATIONS_VERSION}" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 40de7e7d0..0d176f240 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Jan 26 17:04:55 CST 2017 +#Tue May 23 14:22:03 CDT 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.0-milestone-1-all.zip diff --git a/proguard.pro b/proguard.pro index bff2ae6bc..ac46a4c50 100644 --- a/proguard.pro +++ b/proguard.pro @@ -24,16 +24,3 @@ -dontwarn javax.inject.** -dontwarn com.google.j2objc.annotations.** -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement - -# https://github.com/square/leakcanary/blob/457175960e341658fc908ee12a9bf803bca63a23/leakcanary-android/consumer-proguard-rules.pro --dontwarn com.squareup.haha.guava.** --dontwarn com.squareup.haha.perflib.** --dontwarn com.squareup.haha.trove.** --dontwarn com.squareup.leakcanary.** --keep class com.squareup.haha.** { *; } --keep class com.squareup.leakcanary.** { *; } --dontwarn android.app.Notification - -# https://github.com/facebook/stetho/blob/2807d4248c6fa06cdd3626b6afb9bfc42ba50d55/stetho/proguard-consumer.pro --keep class com.facebook.stetho.** { *; } --dontwarn com.facebook.stetho.** \ No newline at end of file diff --git a/src/amazon/java/org/tasks/analytics/Tracker.java b/src/amazon/java/org/tasks/analytics/Tracker.java index f79328899..285762e42 100644 --- a/src/amazon/java/org/tasks/analytics/Tracker.java +++ b/src/amazon/java/org/tasks/analytics/Tracker.java @@ -74,17 +74,21 @@ public class Tracker { } public void reportEvent(Tracking.Events event, String label) { - reportEvent(event.category, event.action, label); + reportEvent(event, event.action, label); } public void reportEvent(Tracking.Events event, int action, String label) { + reportEvent(event, context.getString(action), label); + } + + public void reportEvent(Tracking.Events event, String action, String label) { reportEvent(event.category, action, label); } - private void reportEvent(int category, int action, String label) { + private void reportEvent(int category, String action, String label) { HitBuilders.EventBuilder eventBuilder = new HitBuilders.EventBuilder() .setCategory(context.getString(category)) - .setAction(context.getString(action)); + .setAction(action); if (!Strings.isNullOrEmpty(label)) { eventBuilder.setLabel(label); } diff --git a/src/amazon/java/org/tasks/injection/BroadcastComponent.java b/src/amazon/java/org/tasks/injection/BroadcastComponent.java index 38c99ff3a..b874c5685 100644 --- a/src/amazon/java/org/tasks/injection/BroadcastComponent.java +++ b/src/amazon/java/org/tasks/injection/BroadcastComponent.java @@ -12,8 +12,6 @@ import org.tasks.receivers.BootCompletedReceiver; import org.tasks.receivers.CompleteTaskReceiver; import org.tasks.receivers.ListNotificationReceiver; import org.tasks.receivers.MyPackageReplacedReceiver; -import org.tasks.receivers.RefreshReceiver; -import org.tasks.receivers.TaskNotificationReceiver; import org.tasks.receivers.TeslaUnreadReceiver; import org.tasks.widget.TasksWidget; @@ -37,10 +35,6 @@ public interface BroadcastComponent { void inject(MyPackageReplacedReceiver myPackageReplacedReceiver); - void inject(RefreshReceiver refreshReceiver); - - void inject(TaskNotificationReceiver taskNotificationReceiver); - void inject(CompleteTaskReceiver completeTaskReceiver); void inject(ListNotificationReceiver listNotificationReceiver); diff --git a/src/androidTest/java/com/todoroo/astrid/reminders/NotifyAtDeadlineTest.java b/src/androidTest/java/com/todoroo/astrid/reminders/NotifyAtDeadlineTest.java deleted file mode 100644 index 0f7bff540..000000000 --- a/src/androidTest/java/com/todoroo/astrid/reminders/NotifyAtDeadlineTest.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.todoroo.astrid.reminders; - -import android.support.test.runner.AndroidJUnit4; - -import com.todoroo.astrid.data.Task; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.tasks.preferences.Preferences; -import org.tasks.time.DateTime; - -import static android.support.test.InstrumentationRegistry.getContext; -import static android.support.test.InstrumentationRegistry.getTargetContext; -import static com.natpryce.makeiteasy.MakeItEasy.with; -import static com.todoroo.astrid.data.Task.NOTIFY_AT_DEADLINE; -import static com.todoroo.astrid.reminders.ReminderService.NO_ALARM; -import static junit.framework.Assert.assertEquals; -import static org.tasks.makers.TaskMaker.DUE_DATE; -import static org.tasks.makers.TaskMaker.DUE_TIME; -import static org.tasks.makers.TaskMaker.REMINDERS; -import static org.tasks.makers.TaskMaker.REMINDER_LAST; -import static org.tasks.makers.TaskMaker.newTask; - -@RunWith(AndroidJUnit4.class) -public class NotifyAtDeadlineTest { - - private ReminderService reminderService; - - @Before - public void setUp() { - Preferences preferences = new Preferences(getTargetContext(), null); - reminderService = new ReminderService(getContext(), preferences, null); - } - - @Test - public void testNoReminderWhenNoDueDate() { - Task task = newTask(with(REMINDERS, NOTIFY_AT_DEADLINE)); - assertEquals(NO_ALARM, reminderService.calculateNextDueDateReminder(task)); - } - - @Test - public void testNoReminderWhenNotifyAtDeadlineFlagNotSet() { - Task task = newTask(with(DUE_TIME, new DateTime(2014, 1, 24, 19, 23))); - assertEquals(NO_ALARM, reminderService.calculateNextDueDateReminder(task)); - } - - @Test - public void testScheduleReminderAtDueTime() { - final DateTime dueDate = new DateTime(2014, 1, 24, 19, 23); - Task task = newTask(with(DUE_TIME, dueDate), with(REMINDERS, NOTIFY_AT_DEADLINE)); - assertEquals(dueDate.plusSeconds(1).getMillis(), reminderService.calculateNextDueDateReminder(task)); - } - - @Test - public void testScheduleReminderAtDefaultDueTime() { - final DateTime dueDate = new DateTime(2015, 12, 29, 12, 0); - Task task = newTask(with(DUE_DATE, dueDate), with(REMINDERS, NOTIFY_AT_DEADLINE)); - assertEquals(dueDate.withHourOfDay(18).getMillis(), - reminderService.calculateNextDueDateReminder(task)); - } - - @Test - public void testNoReminderIfAlreadyRemindedPastDueDate() { - final DateTime dueDate = new DateTime(2015, 12, 29, 19, 23); - Task task = newTask( - with(DUE_TIME, dueDate), - with(REMINDER_LAST, dueDate.plusSeconds(1)), - with(REMINDERS, NOTIFY_AT_DEADLINE)); - assertEquals(NO_ALARM, reminderService.calculateNextDueDateReminder(task)); - } -} diff --git a/src/androidTest/java/com/todoroo/astrid/reminders/ReminderServiceTest.java b/src/androidTest/java/com/todoroo/astrid/reminders/ReminderServiceTest.java index 9c955d141..dc1887410 100644 --- a/src/androidTest/java/com/todoroo/astrid/reminders/ReminderServiceTest.java +++ b/src/androidTest/java/com/todoroo/astrid/reminders/ReminderServiceTest.java @@ -1,45 +1,72 @@ -/** - * Copyright (c) 2012 Todoroo Inc - * - * See the file "LICENSE" for the full license governing this code. - */ package com.todoroo.astrid.reminders; -import android.content.Context; import android.support.test.runner.AndroidJUnit4; -import com.todoroo.andlib.utility.DateUtilities; -import com.todoroo.astrid.dao.TaskDao; import com.todoroo.astrid.data.Task; -import com.todoroo.astrid.reminders.ReminderService.AlarmScheduler; import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.InOrder; +import org.tasks.Snippet; import org.tasks.injection.InjectingTestCase; import org.tasks.injection.TestComponent; +import org.tasks.jobs.JobQueue; +import org.tasks.jobs.Reminder; +import org.tasks.preferences.Preferences; +import org.tasks.reminders.Random; +import org.tasks.time.DateTime; import javax.inject.Inject; -import static android.support.test.InstrumentationRegistry.getContext; -import static android.support.test.InstrumentationRegistry.getTargetContext; -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertTrue; -import static junit.framework.Assert.fail; +import static com.natpryce.makeiteasy.MakeItEasy.with; +import static com.todoroo.andlib.utility.DateUtilities.ONE_HOUR; +import static com.todoroo.andlib.utility.DateUtilities.ONE_WEEK; +import static com.todoroo.astrid.data.Task.NOTIFY_AFTER_DEADLINE; +import static com.todoroo.astrid.data.Task.NOTIFY_AT_DEADLINE; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; import static org.tasks.Freeze.freezeClock; -import static org.tasks.Freeze.thaw; import static org.tasks.date.DateTimeUtils.newDateTime; +import static org.tasks.makers.TaskMaker.COMPLETION_TIME; +import static org.tasks.makers.TaskMaker.CREATION_TIME; +import static org.tasks.makers.TaskMaker.DELETION_TIME; +import static org.tasks.makers.TaskMaker.DUE_DATE; +import static org.tasks.makers.TaskMaker.DUE_TIME; +import static org.tasks.makers.TaskMaker.ID; +import static org.tasks.makers.TaskMaker.PRIORITY; +import static org.tasks.makers.TaskMaker.RANDOM_REMINDER_PERIOD; +import static org.tasks.makers.TaskMaker.REMINDERS; +import static org.tasks.makers.TaskMaker.REMINDER_LAST; +import static org.tasks.makers.TaskMaker.SNOOZE_TIME; +import static org.tasks.makers.TaskMaker.newTask; +import static org.tasks.time.DateTimeUtils.currentTimeMillis; @RunWith(AndroidJUnit4.class) public class ReminderServiceTest extends InjectingTestCase { - @Inject TaskDao taskDao; - @Inject ReminderService reminderService; + @Inject Preferences preferences; - @Override - public void setUp() { - super.setUp(); - freezeClock(); + private ReminderService service; + private Random random; + private JobQueue jobs; + + @Before + public void before() { + jobs = mock(JobQueue.class); + random = mock(Random.class); + when(random.nextFloat()).thenReturn(1.0f); + service = new ReminderService(preferences, jobs, random); + } + + @After + public void after() { + verifyNoMoreInteractions(jobs); } @Override @@ -47,222 +74,280 @@ public class ReminderServiceTest extends InjectingTestCase { component.inject(this); } - @After - public void tearDown() { - thaw(); + @Test + public void dontScheduleDueDateReminderWhenFlagNotSet() { + service.scheduleAlarm(null, newTask(with(ID, 1L), with(DUE_TIME, newDateTime()))); + + verify(jobs).cancel(1); } @Test - public void testNoReminders() { - reminderService.setScheduler(new NoAlarmExpected()); - - Task task = new Task(); - task.setTitle("water"); - task.setReminderFlags(0); - task.setReminderPeriod(0L); - taskDao.save(task); - reminderService.scheduleAlarm(taskDao, task); + public void dontScheduleDueDateReminderWhenTimeNotSet() { + service.scheduleAlarm(null, newTask(with(ID, 1L), with(REMINDERS, NOTIFY_AT_DEADLINE))); + + verify(jobs).cancel(1); } @Test - public void testDueDates() { - reminderService.setScheduler(new AlarmExpected() { - @Override - public void createAlarm(Context context, Task task, long time, int type) { - if (time == ReminderService.NO_ALARM) - return; - super.createAlarm(getTargetContext(), task, time, type); - assertEquals((long) task.getDueDate(), time); - assertEquals(type, ReminderService.TYPE_DUE); - } - }); - - // test due date in the past - final Task task = new Task(); - task.setTitle("water"); - task.setDueDate(DateUtilities.now() - DateUtilities.ONE_DAY); - task.setReminderFlags(Task.NOTIFY_AT_DEADLINE); - taskDao.save(task); - - // test due date in the future - task.setDueDate(DateUtilities.now() + DateUtilities.ONE_DAY); - taskDao.save(task); - assertTrue(((AlarmExpected) reminderService.getScheduler()).alarmCreated); + public void schedulePastDueDate() { + Task task = newTask( + with(ID, 1L), + with(DUE_TIME, newDateTime().minusDays(1)), + with(REMINDERS, NOTIFY_AT_DEADLINE)); + service.scheduleAlarm(null, task); + + InOrder order = inOrder(jobs); + order.verify(jobs).cancel(1); + order.verify(jobs).add(new Reminder(1, task.getDueDate(), ReminderService.TYPE_DUE)); } @Test - public void testRandom() { - // test random - final Task task = new Task(); - task.setTitle("water"); - task.setReminderPeriod(DateUtilities.ONE_WEEK); - reminderService.setScheduler(new AlarmExpected() { - @Override - public void createAlarm(Context context, Task task, long time, int type) { - if (time == ReminderService.NO_ALARM) - return; - super.createAlarm(getContext(), task, time, type); - assertTrue(time > DateUtilities.now()); - assertTrue(time < DateUtilities.now() + 1.2 * DateUtilities.ONE_WEEK); - assertEquals(type, ReminderService.TYPE_RANDOM); - } - }); - taskDao.save(task); - assertTrue(((AlarmExpected) reminderService.getScheduler()).alarmCreated); + public void scheduleFutureDueDate() { + Task task = newTask( + with(ID, 1L), + with(DUE_TIME, newDateTime().plusDays(1)), + with(REMINDERS, NOTIFY_AT_DEADLINE)); + service.scheduleAlarm(null, task); + + InOrder order = inOrder(jobs); + order.verify(jobs).cancel(1); + order.verify(jobs).add(new Reminder(1, task.getDueDate(), ReminderService.TYPE_DUE)); } @Test - public void testOverdue() { - // test due date in the future - reminderService.setScheduler(new AlarmExpected() { - @Override - public void createAlarm(Context context, Task task, long time, int type) { - if (time == ReminderService.NO_ALARM) - return; - super.createAlarm(getContext(), task, time, type); - assertTrue(time > task.getDueDate()); - assertTrue(time < task.getDueDate() + DateUtilities.ONE_DAY); - assertEquals(type, ReminderService.TYPE_OVERDUE); - } - }); - final Task task = new Task(); - task.setTitle("water"); - task.setDueDate(DateUtilities.now() + DateUtilities.ONE_DAY); - task.setReminderFlags(Task.NOTIFY_AFTER_DEADLINE); - taskDao.save(task); - - // test due date in the past - task.setDueDate(DateUtilities.now() - DateUtilities.ONE_DAY); - reminderService.setScheduler(new AlarmExpected() { - @Override - public void createAlarm(Context context, Task task, long time, int type) { - if (time == ReminderService.NO_ALARM) - return; - super.createAlarm(getContext(), task, time, type); - assertTrue(time > DateUtilities.now() - 1000L); - assertTrue(time < DateUtilities.now() + 2 * DateUtilities.ONE_DAY); - assertEquals(type, ReminderService.TYPE_OVERDUE); - } - }); - taskDao.save(task); - assertTrue(((AlarmExpected) reminderService.getScheduler()).alarmCreated); - - // test due date in the past, but recently notified - task.setReminderLast(DateUtilities.now()); - reminderService.setScheduler(new AlarmExpected() { - @Override - public void createAlarm(Context context, Task task, long time, int type) { - if (time == ReminderService.NO_ALARM) - return; - super.createAlarm(getContext(), task, time, type); - assertTrue(time > DateUtilities.now() + DateUtilities.ONE_HOUR); - assertTrue(time < DateUtilities.now() + DateUtilities.ONE_DAY); - assertEquals(type, ReminderService.TYPE_OVERDUE); - } - }); - taskDao.save(task); - assertTrue(((AlarmExpected) reminderService.getScheduler()).alarmCreated); + public void scheduleReminderAtDefaultDueTime() { + DateTime now = newDateTime(); + Task task = newTask( + with(ID, 1L), + with(DUE_DATE, now), + with(REMINDERS, NOTIFY_AT_DEADLINE)); + service.scheduleAlarm(null, task); + + InOrder order = inOrder(jobs); + order.verify(jobs).cancel(1); + order.verify(jobs).add(new Reminder(1, now.startOfDay().withHourOfDay(18).getMillis(), ReminderService.TYPE_DUE)); } @Test - public void testMultipleReminders() { - // test due date in the future, enable random - final Task task = new Task(); - task.setTitle("water"); - task.setDueDate(DateUtilities.now() + DateUtilities.ONE_WEEK); - task.setReminderFlags(Task.NOTIFY_AT_DEADLINE); - task.setReminderPeriod(DateUtilities.ONE_HOUR); - reminderService.setScheduler(new AlarmExpected() { - @Override - public void createAlarm(Context context, Task task, long time, int type) { - if (time == ReminderService.NO_ALARM) - return; - super.createAlarm(getContext(), task, time, type); - assertTrue(time > DateUtilities.now()); - assertTrue(time < DateUtilities.now() + DateUtilities.ONE_DAY); - assertEquals(type, ReminderService.TYPE_RANDOM); - } - }); - taskDao.save(task); - assertTrue(((AlarmExpected) reminderService.getScheduler()).alarmCreated); - - // now set the due date in the past - task.setDueDate(DateUtilities.now() - DateUtilities.ONE_WEEK); - ((AlarmExpected) reminderService.getScheduler()).alarmCreated = false; - reminderService.scheduleAlarm(taskDao, task); - assertTrue(((AlarmExpected) reminderService.getScheduler()).alarmCreated); - - // now set the due date before the random - task.setDueDate(DateUtilities.now() + DateUtilities.ONE_HOUR); - reminderService.setScheduler(new AlarmExpected() { - @Override - public void createAlarm(Context context, Task task, long time, int type) { - if (time == ReminderService.NO_ALARM) - return; - super.createAlarm(getContext(), task, time, type); - assertEquals((long) task.getDueDate(), time); - assertEquals(type, ReminderService.TYPE_DUE); - } - }); - taskDao.save(task); - assertTrue(((AlarmExpected) reminderService.getScheduler()).alarmCreated); + public void dontScheduleReminderForCompletedTask() { + Task task = newTask( + with(ID, 1L), + with(DUE_TIME, newDateTime().plusDays(1)), + with(COMPLETION_TIME, newDateTime()), + with(REMINDERS, NOTIFY_AT_DEADLINE)); + + service.scheduleAlarm(null, task); + + verify(jobs).cancel(1); } @Test - public void testSnoozeReminders() { - thaw(); // TODO: get rid of this - - // test due date and snooze in the future - final Task task = new Task(); - task.setTitle("spacemen"); - task.setDueDate(DateUtilities.now() + 5000L); - task.setReminderFlags(Task.NOTIFY_AT_DEADLINE); - task.setReminderSnooze(DateUtilities.now() + DateUtilities.ONE_WEEK); - reminderService.setScheduler(new AlarmExpected() { - @Override - public void createAlarm(Context context, Task task, long time, int type) { - if (time == ReminderService.NO_ALARM) - return; - super.createAlarm(getContext(), task, time, type); - assertTrue(time > DateUtilities.now() + DateUtilities.ONE_WEEK - 1000L); - assertTrue(time < DateUtilities.now() + DateUtilities.ONE_WEEK + 1000L); - assertEquals(type, ReminderService.TYPE_SNOOZE); - } - }); - taskDao.save(task); - assertTrue(((AlarmExpected) reminderService.getScheduler()).alarmCreated); - - // snooze in the past - task.setReminderSnooze(DateUtilities.now() - DateUtilities.ONE_WEEK); - reminderService.setScheduler(new AlarmExpected() { - @Override - public void createAlarm(Context context, Task task, long time, int type) { - if (time == ReminderService.NO_ALARM) - return; - super.createAlarm(getContext(), task, time, type); - assertTrue(time > DateUtilities.now() - 1000L); - assertTrue(time < DateUtilities.now() + 5000L); - assertEquals(type, ReminderService.TYPE_DUE); - } - }); - taskDao.save(task); - assertTrue(((AlarmExpected) reminderService.getScheduler()).alarmCreated); + public void dontScheduleReminderForDeletedTask() { + Task task = newTask( + with(ID, 1L), + with(DUE_TIME, newDateTime().plusDays(1)), + with(DELETION_TIME, newDateTime()), + with(REMINDERS, NOTIFY_AT_DEADLINE)); + + service.scheduleAlarm(null, task); + + verify(jobs).cancel(1); } - // --- helper classes + @Test + public void dontScheduleDueDateReminderWhenAlreadyReminded() { + DateTime now = newDateTime(); + Task task = newTask( + with(ID, 1L), + with(DUE_TIME, now), + with(REMINDER_LAST, now.plusSeconds(1)), + with(REMINDERS, NOTIFY_AT_DEADLINE)); + + service.scheduleAlarm(null, task); - public class NoAlarmExpected implements AlarmScheduler { - public void createAlarm(Context context, Task task, long time, int type) { - if(time == 0 || time == Long.MAX_VALUE) - return; - fail("created alarm, no alarm expected (" + type + ": " + newDateTime(time)); - } + verify(jobs).cancel(1); } - public class AlarmExpected implements AlarmScheduler { - public boolean alarmCreated = false; - public void createAlarm(Context context, Task task, long time, int type) { - alarmCreated = true; - } + @Test + public void ignoreLapsedSnoozeTime() { + Task task = newTask( + with(ID, 1L), + with(DUE_TIME, newDateTime()), + with(SNOOZE_TIME, newDateTime().minusMinutes(5)), + with(REMINDERS, NOTIFY_AT_DEADLINE)); + service.scheduleAlarm(null, task); + + InOrder order = inOrder(jobs); + order.verify(jobs).cancel(1); + order.verify(jobs).add(new Reminder(1, task.getDueDate(), ReminderService.TYPE_DUE)); + + } + + @Test + public void scheduleInitialRandomReminder() { + freezeClock().thawAfter(new Snippet() {{ + DateTime now = newDateTime(); + when(random.nextFloat()).thenReturn(0.3865f); + Task task = newTask( + with(ID, 1L), + with(REMINDER_LAST, (DateTime) null), + with(CREATION_TIME, now.minusDays(1)), + with(RANDOM_REMINDER_PERIOD, ONE_WEEK)); + + service.scheduleAlarm(null, task); + + InOrder order = inOrder(jobs); + order.verify(jobs).cancel(1); + order.verify(jobs).add(new Reminder(1L, now.minusDays(1).getMillis() + 584206592, ReminderService.TYPE_RANDOM)); + }}); + } + + @Test + public void scheduleNextRandomReminder() { + freezeClock().thawAfter(new Snippet() {{ + DateTime now = newDateTime(); + when(random.nextFloat()).thenReturn(0.3865f); + Task task = newTask( + with(ID, 1L), + with(REMINDER_LAST, now.minusDays(1)), + with(CREATION_TIME, now.minusDays(30)), + with(RANDOM_REMINDER_PERIOD, ONE_WEEK)); + + service.scheduleAlarm(null, task); + + InOrder order = inOrder(jobs); + order.verify(jobs).cancel(1); + order.verify(jobs).add(new Reminder(1L, now.minusDays(1).getMillis() + 584206592, ReminderService.TYPE_RANDOM)); + }}); + } + + @Test + public void scheduleOverdueRandomReminder() { + freezeClock().thawAfter(new Snippet() {{ + DateTime now = newDateTime(); + when(random.nextFloat()).thenReturn(0.3865f); + Task task = newTask( + with(ID, 1L), + with(REMINDER_LAST, now.minusDays(14)), + with(CREATION_TIME, now.minusDays(30)), + with(RANDOM_REMINDER_PERIOD, ONE_WEEK)); + + service.scheduleAlarm(null, task); + + InOrder order = inOrder(jobs); + order.verify(jobs).cancel(1); + order.verify(jobs).add(new Reminder(1L, now.getMillis() + 10148400, ReminderService.TYPE_RANDOM)); + }}); + } + + @Test + public void scheduleOverdueForFutureDueDate() { + freezeClock().thawAfter(new Snippet() {{ + when(random.nextFloat()).thenReturn(0.3865f); + Task task = newTask( + with(ID, 1L), + with(DUE_TIME, newDateTime().plusMinutes(5)), + with(REMINDERS, NOTIFY_AFTER_DEADLINE)); + + service.scheduleAlarm(null, task); + + InOrder order = inOrder(jobs); + order.verify(jobs).cancel(1); + order.verify(jobs).add(new Reminder(1L, task.getDueDate() + 4582800, ReminderService.TYPE_OVERDUE)); + }}); + } + + @Test + public void scheduleOverdueForPastDueDateWithNoReminderPastDueDate() { + freezeClock().thawAfter(new Snippet() {{ + DateTime now = newDateTime(); + Task task = newTask( + with(ID, 1L), + with(DUE_TIME, now.minusMinutes(5)), + with(REMINDERS, NOTIFY_AFTER_DEADLINE)); + + service.scheduleAlarm(null, task); + + InOrder order = inOrder(jobs); + order.verify(jobs).cancel(1); + order.verify(jobs).add(new Reminder(1L, currentTimeMillis(), ReminderService.TYPE_OVERDUE)); + }}); + } + + @Test + public void scheduleOverdueForPastDueDateLastReminderSixHoursAgo() { + freezeClock().thawAfter(new Snippet() {{ + Task task = newTask( + with(ID, 1L), + with(DUE_TIME, newDateTime().minusHours(12)), + with(REMINDER_LAST, newDateTime().minusHours(6)), + with(REMINDERS, NOTIFY_AFTER_DEADLINE)); + + service.scheduleAlarm(null, task); + + InOrder order = inOrder(jobs); + order.verify(jobs).cancel(1); + order.verify(jobs).add(new Reminder(1L, currentTimeMillis(), ReminderService.TYPE_OVERDUE)); + }}); + } + + @Test + public void scheduleOverdueForPastDueDateLastReminderWithinSixHours() { + freezeClock().thawAfter(new Snippet() {{ + when(random.nextFloat()).thenReturn(0.3865f); + Task task = newTask( + with(ID, 1L), + with(DUE_TIME, newDateTime().minusHours(12)), + with(PRIORITY, 2), + with(REMINDER_LAST, newDateTime().minusHours(3)), + with(REMINDERS, NOTIFY_AFTER_DEADLINE)); + + service.scheduleAlarm(null, task); + + InOrder order = inOrder(jobs); + order.verify(jobs).cancel(1); + order.verify(jobs).add(new Reminder(1L, currentTimeMillis() + 22748400, ReminderService.TYPE_OVERDUE)); + }}); + } + + @Test + public void snoozeOverridesAll() { + DateTime now = newDateTime(); + Task task = newTask( + with(ID, 1L), + with(DUE_TIME, now), + with(SNOOZE_TIME, now.plusMonths(12)), + with(REMINDERS, NOTIFY_AT_DEADLINE | NOTIFY_AFTER_DEADLINE), + with(RANDOM_REMINDER_PERIOD, ONE_HOUR)); + + service.scheduleAlarm(null, task); + + InOrder order = inOrder(jobs); + order.verify(jobs).cancel(1); + order.verify(jobs).add(new Reminder(1, now.plusMonths(12).getMillis(), ReminderService.TYPE_SNOOZE)); + } + + @Test + @Ignore + public void randomReminderBeforeDueAndOverdue() { + + } + + @Test + @Ignore + public void randomReminderAfterDue() { + + } + + @Test + @Ignore + public void randomReminderAfterOverdue() { + + } + + @Test + @Ignore + public void dueDateBeforeOverdue() { + } } diff --git a/src/androidTest/java/org/tasks/scheduling/BackupServiceTests.java b/src/androidTest/java/org/tasks/jobs/BackupServiceTests.java similarity index 86% rename from src/androidTest/java/org/tasks/scheduling/BackupServiceTests.java rename to src/androidTest/java/org/tasks/jobs/BackupServiceTests.java index 4ce7f6cac..67359247b 100644 --- a/src/androidTest/java/org/tasks/scheduling/BackupServiceTests.java +++ b/src/androidTest/java/org/tasks/jobs/BackupServiceTests.java @@ -3,7 +3,7 @@ * * See the file "LICENSE" for the full license governing this code. */ -package org.tasks.scheduling; +package org.tasks.jobs; import android.support.test.runner.AndroidJUnit4; @@ -19,6 +19,7 @@ import org.junit.runner.RunWith; import org.tasks.R; import org.tasks.injection.TestComponent; import org.tasks.preferences.Preferences; +import org.tasks.scheduling.AlarmManager; import java.io.File; import java.io.IOException; @@ -29,6 +30,7 @@ import static android.support.test.InstrumentationRegistry.getTargetContext; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; +import static org.mockito.Mockito.mock; import static org.tasks.date.DateTimeUtils.newDateTime; import static org.tasks.time.DateTimeUtils.currentTimeMillis; @@ -90,15 +92,15 @@ public class BackupServiceTests extends DatabaseTestCase { preferences.setLong(TasksXmlExporter.PREF_BACKUP_LAST_DATE, 0); // create a backup - BackupIntentService service = new BackupIntentService(); - service.testBackup(xmlExporter, preferences, getTargetContext()); + BackupJob service = new BackupJob(getTargetContext(), new JobManager(getTargetContext(), mock(AlarmManager.class)), xmlExporter, preferences); + service.startBackup(getTargetContext()); AndroidUtilities.sleepDeep(BACKUP_WAIT_TIME); // assert file created File[] files = temporaryDirectory.listFiles(); assertEquals(1, files.length); - assertTrue(files[0].getName().matches(BackupIntentService.BACKUP_FILE_NAME_REGEX)); + assertTrue(files[0].getName().matches(BackupJob.BACKUP_FILE_NAME_REGEX)); // assert summary updated assertTrue(preferences.getLong(TasksXmlExporter.PREF_BACKUP_LAST_DATE, 0) > 0); @@ -128,8 +130,8 @@ public class BackupServiceTests extends DatabaseTestCase { assertEquals(11, files.length); // backup - BackupIntentService service = new BackupIntentService(); - service.testBackup(xmlExporter, preferences, getTargetContext()); + BackupJob service = new BackupJob(getTargetContext(), new JobManager(getTargetContext(), mock(AlarmManager.class)), xmlExporter, preferences); + service.startBackup(getTargetContext()); AndroidUtilities.sleepDeep(BACKUP_WAIT_TIME); @@ -138,7 +140,7 @@ public class BackupServiceTests extends DatabaseTestCase { assertFalse(files[4].exists()); // assert user file still exists - service.testBackup(xmlExporter, preferences, getTargetContext()); + service.startBackup(getTargetContext()); assertTrue(myFile.exists()); } } diff --git a/src/androidTest/java/org/tasks/jobs/JobQueueTest.java b/src/androidTest/java/org/tasks/jobs/JobQueueTest.java new file mode 100644 index 000000000..28652db9b --- /dev/null +++ b/src/androidTest/java/org/tasks/jobs/JobQueueTest.java @@ -0,0 +1,240 @@ +package org.tasks.jobs; + +import android.support.test.runner.AndroidJUnit4; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InOrder; +import org.tasks.Freeze; +import org.tasks.Snippet; +import org.tasks.preferences.Preferences; + +import java.util.concurrent.TimeUnit; + +import static com.todoroo.astrid.reminders.ReminderService.TYPE_DUE; +import static java.util.Collections.singletonList; +import static junit.framework.Assert.assertEquals; +import static org.mockito.AdditionalAnswers.returnsFirstArg; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static org.tasks.time.DateTimeUtils.currentTimeMillis; + +@RunWith(AndroidJUnit4.class) +public class JobQueueTest { + + private static final long ONE_MINUTE = TimeUnit.MINUTES.toMillis(1); + private static final String TAG = "test"; + + private JobQueue queue; + private JobManager jobManager; + private Preferences preferences; + + @Before + public void before() { + preferences = mock(Preferences.class); + when(preferences.adjustForQuietHours(anyLong())).then(returnsFirstArg()); + jobManager = mock(JobManager.class); + queue = new JobQueue<>(preferences, jobManager, TAG); + } + + @After + public void after() { + verifyNoMoreInteractions(jobManager); + } + + @Test + public void twoJobsAtSameTime() { + queue.add(new Reminder(1, 1, 0)); + queue.add(new Reminder(2, 1, 0)); + + verify(jobManager).schedule(TAG, 1); + + assertEquals(2, queue.size()); + } + + @Test + public void rescheduleForFirstJob() { + queue.add(new Reminder(1, 1, 0)); + + verify(jobManager).schedule(TAG, 1); + } + + @Test + public void dontRescheduleForLaterJobs() { + queue.add(new Reminder(1, 1, 0)); + queue.add(new Reminder(2, 2, 0)); + + verify(jobManager).schedule(TAG, 1); + } + + @Test + public void rescheduleForNewerJob() { + queue.add(new Reminder(1, 2, 0)); + queue.add(new Reminder(1, 1, 0)); + + InOrder order = inOrder(jobManager); + order.verify(jobManager).schedule(TAG, 2); + order.verify(jobManager).schedule(TAG, 1); + } + + @Test + public void rescheduleWhenCancelingOnlyJob() { + queue.add(new Reminder(1, 2, 0)); + queue.cancel(1); + + InOrder order = inOrder(jobManager); + order.verify(jobManager).schedule(TAG, 2); + order.verify(jobManager).cancel(TAG); + } + + @Test + public void rescheduleWhenCancelingFirstJob() { + queue.add(new Reminder(1, 1, 0)); + queue.add(new Reminder(2, 2, 0)); + + queue.cancel(1); + + InOrder order = inOrder(jobManager); + order.verify(jobManager).schedule(TAG, 1); + order.verify(jobManager).schedule(TAG, 2); + } + + @Test + public void dontRescheduleWhenCancelingLaterJob() { + queue.add(new Reminder(1, 1, 0)); + queue.add(new Reminder(2, 2, 0)); + + queue.cancel(2); + + verify(jobManager).schedule(TAG, 1); + } + + @Test + public void nextScheduledTimeIsZeroWhenQueueIsEmpty() { + when(preferences.adjustForQuietHours(anyLong())).thenReturn(1234L); + + assertEquals(0, queue.nextScheduledTime()); + } + + @Test + public void adjustNextScheduledTimeForQuietHours() { + when(preferences.adjustForQuietHours(anyLong())).thenReturn(1234L); + queue.add(new Reminder(1, 1, 1)); + + verify(jobManager).schedule(TAG, 1234); + } + + @Test + public void overdueJobsAreReturned() { + long now = currentTimeMillis(); + + queue.add(new Reminder(1, now, TYPE_DUE)); + queue.add(new Reminder(2, now + ONE_MINUTE, TYPE_DUE)); + + verify(jobManager).schedule(TAG, now); + + Freeze.freezeAt(now).thawAfter(new Snippet() {{ + assertEquals( + singletonList(new Reminder(1, now, TYPE_DUE)), + queue.getOverdueJobs()); + }}); + } + + @Test + public void twoOverdueJobsAtSameTimeReturned() { + long now = currentTimeMillis(); + + queue.add(new Reminder(1, now, TYPE_DUE)); + queue.add(new Reminder(2, now, TYPE_DUE)); + + verify(jobManager).schedule(TAG, now); + + Freeze.freezeAt(now).thawAfter(new Snippet() {{ + assertEquals( + asList(new Reminder(1, now, TYPE_DUE), new Reminder(2, now, TYPE_DUE)), + queue.getOverdueJobs()); + }}); + } + + @Test + public void twoOverdueJobsAtDifferentTimes() { + long now = currentTimeMillis(); + + queue.add(new Reminder(1, now, TYPE_DUE)); + queue.add(new Reminder(2, now + ONE_MINUTE, TYPE_DUE)); + + verify(jobManager).schedule(TAG, now); + + Freeze.freezeAt(now + 2 * ONE_MINUTE).thawAfter(new Snippet() {{ + assertEquals( + asList(new Reminder(1, now, TYPE_DUE), new Reminder(2, now + ONE_MINUTE, TYPE_DUE)), + queue.getOverdueJobs()); + }}); + } + + @Test + public void overdueJobsAreRemoved() { + long now = currentTimeMillis(); + + queue.add(new Reminder(1, now, TYPE_DUE)); + queue.add(new Reminder(2, now + ONE_MINUTE, TYPE_DUE)); + + verify(jobManager).schedule(TAG, now); + + Freeze.freezeAt(now).thawAfter(new Snippet() {{ + queue.remove(new Reminder(1, now, TYPE_DUE)); + }}); + + assertEquals( + singletonList(new Reminder(2, now + ONE_MINUTE, TYPE_DUE)), + queue.getJobs()); + } + + @Test + public void multipleOverduePeriodsLapsed() { + long now = currentTimeMillis(); + + queue.add(new Reminder(1, now, TYPE_DUE)); + queue.add(new Reminder(2, now + ONE_MINUTE, TYPE_DUE)); + queue.add(new Reminder(3, now + 2 * ONE_MINUTE, TYPE_DUE)); + + 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)); + }}); + + assertEquals( + singletonList(new Reminder(3, now + 2 * ONE_MINUTE, TYPE_DUE)), + queue.getJobs()); + } + + @Test + public void clearShouldCancelExisting() { + queue.add(new Reminder(1, 1, 0)); + + queue.clear(); + + InOrder order = inOrder(jobManager); + order.verify(jobManager).schedule(TAG, 1); + order.verify(jobManager).cancel(TAG); + assertEquals(0, queue.size()); + } + + @Test + public void ignoreInvalidCancel() { + long now = currentTimeMillis(); + + queue.add(new Reminder(1, now, TYPE_DUE)); + queue.cancel(2); + + verify(jobManager).schedule(TAG, now); + } +} diff --git a/src/androidTest/java/org/tasks/makers/TaskMaker.java b/src/androidTest/java/org/tasks/makers/TaskMaker.java index 03b5ae457..97bab5d50 100644 --- a/src/androidTest/java/org/tasks/makers/TaskMaker.java +++ b/src/androidTest/java/org/tasks/makers/TaskMaker.java @@ -1,5 +1,6 @@ package org.tasks.makers; +import com.google.common.base.Strings; import com.natpryce.makeiteasy.Instantiator; import com.natpryce.makeiteasy.Property; import com.natpryce.makeiteasy.PropertyValue; @@ -8,15 +9,24 @@ import com.todoroo.astrid.data.Task; import org.tasks.time.DateTime; import static com.natpryce.makeiteasy.Property.newProperty; +import static org.tasks.date.DateTimeUtils.newDateTime; import static org.tasks.makers.Maker.make; public class TaskMaker { + public static Property ID = newProperty(); + public static Property TITLE = newProperty(); public static Property DUE_DATE = newProperty(); public static Property DUE_TIME = newProperty(); + public static Property PRIORITY = newProperty(); public static Property REMINDER_LAST = newProperty(); + public static Property RANDOM_REMINDER_PERIOD = newProperty(); public static Property HIDE_TYPE = newProperty(); public static Property REMINDERS = newProperty(); + public static Property CREATION_TIME = newProperty(); + public static Property COMPLETION_TIME = newProperty(); + public static Property DELETION_TIME = newProperty(); + public static Property SNOOZE_TIME = newProperty(); @SafeVarargs public static Task newTask(PropertyValue... properties) { @@ -26,6 +36,21 @@ public class TaskMaker { private static final Instantiator instantiator = lookup -> { Task task = new Task(); + String title = lookup.valueOf(TITLE, (String) null); + if (!Strings.isNullOrEmpty(title)) { + task.setTitle(title); + } + + long id = lookup.valueOf(ID, Task.NO_ID); + if (id != Task.NO_ID) { + task.setId(id); + } + + int priority = lookup.valueOf(PRIORITY, -1); + if (priority >= 0) { + task.setImportance(priority); + } + DateTime dueDate = lookup.valueOf(DUE_DATE, (DateTime) null); if (dueDate != null) { task.setDueDate(Task.createDueDate(Task.URGENCY_SPECIFIC_DAY, dueDate.getMillis())); @@ -36,6 +61,21 @@ public class TaskMaker { task.setDueDate(Task.createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, dueTime.getMillis())); } + DateTime completionTime = lookup.valueOf(COMPLETION_TIME, (DateTime) null); + if (completionTime != null) { + task.setCompletionDate(completionTime.getMillis()); + } + + DateTime deletedTime = lookup.valueOf(DELETION_TIME, (DateTime) null); + if (deletedTime != null) { + task.setDeletionDate(deletedTime.getMillis()); + } + + DateTime snoozeTime = lookup.valueOf(SNOOZE_TIME, (DateTime) null); + if (snoozeTime != null) { + task.setReminderSnooze(snoozeTime.getMillis()); + } + int hideType = lookup.valueOf(HIDE_TYPE, -1); if (hideType >= 0) { task.setHideUntil(task.createHideUntil(hideType, 0)); @@ -51,6 +91,14 @@ public class TaskMaker { task.setReminderLast(reminderLast.getMillis()); } + long randomReminderPeriod = lookup.valueOf(RANDOM_REMINDER_PERIOD, 0L); + if (randomReminderPeriod > 0) { + task.setReminderPeriod(randomReminderPeriod); + } + + DateTime creationTime = lookup.valueOf(CREATION_TIME, newDateTime()); + task.setCreationDate(creationTime.getMillis()); + return task; }; } diff --git a/src/androidTest/java/org/tasks/scheduling/AlarmManagerTests.java b/src/androidTest/java/org/tasks/preferences/PreferenceTests.java similarity index 78% rename from src/androidTest/java/org/tasks/scheduling/AlarmManagerTests.java rename to src/androidTest/java/org/tasks/preferences/PreferenceTests.java index 89abb00cf..8b68356f4 100644 --- a/src/androidTest/java/org/tasks/scheduling/AlarmManagerTests.java +++ b/src/androidTest/java/org/tasks/preferences/PreferenceTests.java @@ -1,4 +1,4 @@ -package org.tasks.scheduling; +package org.tasks.preferences; import android.annotation.SuppressLint; import android.support.test.runner.AndroidJUnit4; @@ -7,7 +7,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.tasks.R; -import org.tasks.preferences.Preferences; import org.tasks.time.DateTime; import java.util.concurrent.TimeUnit; @@ -16,20 +15,18 @@ import static android.support.test.InstrumentationRegistry.getTargetContext; import static junit.framework.Assert.assertEquals; @RunWith(AndroidJUnit4.class) -public class AlarmManagerTests { +public class PreferenceTests { @SuppressLint("NewApi") private static final int MILLIS_PER_HOUR = (int) TimeUnit.HOURS.toMillis(1); private Preferences preferences; - private AlarmManager alarmManager; @Before public void setUp() { preferences = new Preferences(getTargetContext(), null); preferences.clear(); preferences.setBoolean(R.string.p_rmd_enable_quiet, true); - alarmManager = new AlarmManager(getTargetContext(), preferences); } @Test @@ -40,7 +37,7 @@ public class AlarmManagerTests { long dueDate = new DateTime(2015, 12, 29, 8, 0, 1).getMillis(); - assertEquals(dueDate, alarmManager.adjustForQuietHours(dueDate)); + assertEquals(dueDate, preferences.adjustForQuietHours(dueDate)); } @Test @@ -51,7 +48,7 @@ public class AlarmManagerTests { long dueDate = new DateTime(2015, 12, 29, 18, 0, 1).getMillis(); assertEquals(new DateTime(2015, 12, 29, 19, 0).getMillis(), - alarmManager.adjustForQuietHours(dueDate)); + preferences.adjustForQuietHours(dueDate)); } @Test @@ -62,7 +59,7 @@ public class AlarmManagerTests { long dueDate = new DateTime(2015, 12, 29, 22, 0, 1).getMillis(); assertEquals(new DateTime(2015, 12, 30, 10, 0).getMillis(), - alarmManager.adjustForQuietHours(dueDate)); + preferences.adjustForQuietHours(dueDate)); } @Test @@ -73,7 +70,7 @@ public class AlarmManagerTests { long dueDate = new DateTime(2015, 12, 29, 23, 30).getMillis(); assertEquals(new DateTime(2015, 12, 30, 10, 0).getMillis(), - alarmManager.adjustForQuietHours(dueDate)); + preferences.adjustForQuietHours(dueDate)); } @Test @@ -84,7 +81,7 @@ public class AlarmManagerTests { long dueDate = new DateTime(2015, 12, 30, 7, 15).getMillis(); assertEquals(new DateTime(2015, 12, 30, 10, 0).getMillis(), - alarmManager.adjustForQuietHours(dueDate)); + preferences.adjustForQuietHours(dueDate)); } @Test @@ -94,7 +91,7 @@ public class AlarmManagerTests { long dueDate = new DateTime(2015, 12, 29, 18, 0, 0).getMillis(); - assertEquals(dueDate, alarmManager.adjustForQuietHours(dueDate)); + assertEquals(dueDate, preferences.adjustForQuietHours(dueDate)); } @Test @@ -104,7 +101,7 @@ public class AlarmManagerTests { long dueDate = new DateTime(2015, 12, 29, 18, 0).getMillis(); - assertEquals(dueDate, alarmManager.adjustForQuietHours(dueDate)); + assertEquals(dueDate, preferences.adjustForQuietHours(dueDate)); } @Test @@ -114,7 +111,7 @@ public class AlarmManagerTests { long dueDate = new DateTime(2015, 12, 29, 10, 0).getMillis(); - assertEquals(dueDate, alarmManager.adjustForQuietHours(dueDate)); + assertEquals(dueDate, preferences.adjustForQuietHours(dueDate)); } @Test @@ -124,7 +121,7 @@ public class AlarmManagerTests { long dueDate = new DateTime(2015, 12, 29, 11, 30).getMillis(); - assertEquals(dueDate, alarmManager.adjustForQuietHours(dueDate)); + assertEquals(dueDate, preferences.adjustForQuietHours(dueDate)); } @Test @@ -134,7 +131,7 @@ public class AlarmManagerTests { long dueDate = new DateTime(2015, 12, 29, 22, 15).getMillis(); - assertEquals(dueDate, alarmManager.adjustForQuietHours(dueDate)); + assertEquals(dueDate, preferences.adjustForQuietHours(dueDate)); } @Test @@ -144,7 +141,7 @@ public class AlarmManagerTests { long dueDate = new DateTime(2015, 12, 29, 13, 45).getMillis(); - assertEquals(dueDate, alarmManager.adjustForQuietHours(dueDate)); + assertEquals(dueDate, preferences.adjustForQuietHours(dueDate)); } private void setQuietHoursStart(int hour) { diff --git a/src/androidTestAmazon/java/org/tasks/injection/TestComponent.java b/src/androidTestAmazon/java/org/tasks/injection/TestComponent.java index 6713f0add..e7bd5b5a3 100644 --- a/src/androidTestAmazon/java/org/tasks/injection/TestComponent.java +++ b/src/androidTestAmazon/java/org/tasks/injection/TestComponent.java @@ -14,7 +14,7 @@ import com.todoroo.astrid.subtasks.SubtasksHelperTest; import com.todoroo.astrid.subtasks.SubtasksTestCase; import com.todoroo.astrid.sync.NewSyncTestCase; -import org.tasks.scheduling.BackupServiceTests; +import org.tasks.jobs.BackupServiceTests; import dagger.Component; diff --git a/src/androidTestGeneric/java/org/tasks/injection/TestComponent.java b/src/androidTestGeneric/java/org/tasks/injection/TestComponent.java index 6713f0add..e7bd5b5a3 100644 --- a/src/androidTestGeneric/java/org/tasks/injection/TestComponent.java +++ b/src/androidTestGeneric/java/org/tasks/injection/TestComponent.java @@ -14,7 +14,7 @@ import com.todoroo.astrid.subtasks.SubtasksHelperTest; import com.todoroo.astrid.subtasks.SubtasksTestCase; import com.todoroo.astrid.sync.NewSyncTestCase; -import org.tasks.scheduling.BackupServiceTests; +import org.tasks.jobs.BackupServiceTests; import dagger.Component; diff --git a/src/androidTestGoogleplay/java/org/tasks/injection/TestComponent.java b/src/androidTestGoogleplay/java/org/tasks/injection/TestComponent.java index e1968d196..571be3cd7 100644 --- a/src/androidTestGoogleplay/java/org/tasks/injection/TestComponent.java +++ b/src/androidTestGoogleplay/java/org/tasks/injection/TestComponent.java @@ -19,7 +19,7 @@ import com.todoroo.astrid.subtasks.SubtasksHelperTest; import com.todoroo.astrid.subtasks.SubtasksTestCase; import com.todoroo.astrid.sync.NewSyncTestCase; -import org.tasks.scheduling.BackupServiceTests; +import org.tasks.jobs.BackupServiceTests; import dagger.Component; diff --git a/src/generic/java/org/tasks/analytics/Tracker.java b/src/generic/java/org/tasks/analytics/Tracker.java index f09115222..3337d740a 100644 --- a/src/generic/java/org/tasks/analytics/Tracker.java +++ b/src/generic/java/org/tasks/analytics/Tracker.java @@ -34,4 +34,8 @@ public class Tracker { public void reportEvent(Tracking.Events setPreference, int resId, String s) { } + + public void reportEvent(Tracking.Events category, String action, String label) { + + } } diff --git a/src/generic/java/org/tasks/injection/BroadcastComponent.java b/src/generic/java/org/tasks/injection/BroadcastComponent.java index 38c99ff3a..b874c5685 100644 --- a/src/generic/java/org/tasks/injection/BroadcastComponent.java +++ b/src/generic/java/org/tasks/injection/BroadcastComponent.java @@ -12,8 +12,6 @@ import org.tasks.receivers.BootCompletedReceiver; import org.tasks.receivers.CompleteTaskReceiver; import org.tasks.receivers.ListNotificationReceiver; import org.tasks.receivers.MyPackageReplacedReceiver; -import org.tasks.receivers.RefreshReceiver; -import org.tasks.receivers.TaskNotificationReceiver; import org.tasks.receivers.TeslaUnreadReceiver; import org.tasks.widget.TasksWidget; @@ -37,10 +35,6 @@ public interface BroadcastComponent { void inject(MyPackageReplacedReceiver myPackageReplacedReceiver); - void inject(RefreshReceiver refreshReceiver); - - void inject(TaskNotificationReceiver taskNotificationReceiver); - void inject(CompleteTaskReceiver completeTaskReceiver); void inject(ListNotificationReceiver listNotificationReceiver); diff --git a/src/googleplay/java/com/todoroo/astrid/gtasks/GtasksPreferences.java b/src/googleplay/java/com/todoroo/astrid/gtasks/GtasksPreferences.java index 3fea143b1..434c91029 100644 --- a/src/googleplay/java/com/todoroo/astrid/gtasks/GtasksPreferences.java +++ b/src/googleplay/java/com/todoroo/astrid/gtasks/GtasksPreferences.java @@ -6,9 +6,9 @@ package com.todoroo.astrid.gtasks; import android.content.Intent; -import android.content.pm.PackageManager; import android.os.Bundle; import android.preference.CheckBoxPreference; +import android.preference.Preference; import android.support.annotation.NonNull; import com.todoroo.andlib.utility.DateUtilities; @@ -31,6 +31,8 @@ import org.tasks.preferences.PermissionRequestor; import javax.inject.Inject; +import static org.tasks.PermissionUtil.verifyPermissions; + public class GtasksPreferences extends InjectingPreferenceActivity implements GoogleTaskListSelectionHandler { private static final String FRAG_TAG_GOOGLE_TASK_LIST_SELECTION = "frag_tag_google_task_list_selection"; @@ -74,8 +76,8 @@ public class GtasksPreferences extends InjectingPreferenceActivity implements Go DateUtilities.getDateStringWithTime(GtasksPreferences.this, gtasksPreferenceService.getLastSyncDate()))); } - findPreference(getString(R.string.gtasks_GPr_interval_key)).setOnPreferenceChangeListener((preference, o) -> { - syncAdapterHelper.setSynchronizationInterval(Integer.parseInt((String) o)); + findPreference(getString(R.string.gtask_background_sync)).setOnPreferenceChangeListener((preference, o) -> { + syncAdapterHelper.enableSynchronization((Boolean) o); return true; }); findPreference(getString(R.string.sync_SPr_forget_key)).setOnPreferenceClickListener(preference -> { @@ -104,6 +106,19 @@ public class GtasksPreferences extends InjectingPreferenceActivity implements Go startActivityForResult(new Intent(GtasksPreferences.this, GtasksLoginActivity.class), REQUEST_LOGIN); } + @Override + protected void onPostResume() { + super.onPostResume(); + + CheckBoxPreference backgroundSync = (CheckBoxPreference) findPreference(getString(R.string.gtask_background_sync)); + backgroundSync.setChecked(syncAdapterHelper.isSyncEnabled()); + if (syncAdapterHelper.isMasterSyncEnabled()) { + backgroundSync.setSummary(null); + } else { + backgroundSync.setSummary(R.string.master_sync_warning); + } + } + @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_LOGIN) { @@ -121,7 +136,7 @@ public class GtasksPreferences extends InjectingPreferenceActivity implements Go @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == PermissionRequestor.REQUEST_ACCOUNTS) { - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + if (verifyPermissions(grantResults)) { requestLogin(); } } else { diff --git a/src/googleplay/java/org/tasks/gtasks/GoogleTaskSyncAdapter.java b/src/googleplay/java/org/tasks/gtasks/GoogleTaskSyncAdapter.java index 5ee11e7c0..1629a28c5 100644 --- a/src/googleplay/java/org/tasks/gtasks/GoogleTaskSyncAdapter.java +++ b/src/googleplay/java/org/tasks/gtasks/GoogleTaskSyncAdapter.java @@ -19,7 +19,6 @@ package org.tasks.gtasks; import android.accounts.Account; import android.app.PendingIntent; import android.content.ContentProviderClient; -import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; @@ -150,9 +149,6 @@ public class GoogleTaskSyncAdapter extends InjectingAbstractThreadedSyncAdapter syncResult.stats.numAuthExceptions++; return; } - if (!extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false)) { - preferences.setBoolean(R.string.p_sync_warning_shown, false); - } Timber.d("%s: start sync", account); RecordSyncStatusCallback callback = new RecordSyncStatusCallback(gtasksPreferenceService, broadcaster); try { diff --git a/src/googleplay/java/org/tasks/gtasks/SyncAdapterHelper.java b/src/googleplay/java/org/tasks/gtasks/SyncAdapterHelper.java index c177ccd67..0695910cd 100644 --- a/src/googleplay/java/org/tasks/gtasks/SyncAdapterHelper.java +++ b/src/googleplay/java/org/tasks/gtasks/SyncAdapterHelper.java @@ -13,6 +13,8 @@ import org.tasks.analytics.Tracker; import org.tasks.analytics.Tracking; import org.tasks.preferences.Preferences; +import java.util.concurrent.TimeUnit; + import javax.inject.Inject; import timber.log.Timber; @@ -76,7 +78,7 @@ public class SyncAdapterHelper { getAccount() != null; } - private boolean masterSyncEnabled() { + public boolean isMasterSyncEnabled() { return ContentResolver.getMasterSyncAutomatically(); } @@ -84,37 +86,23 @@ public class SyncAdapterHelper { Account account = getAccount(); if (account != null) { Timber.d("enableSynchronization=%s", enabled); - ContentResolver.setIsSyncable(account, AUTHORITY, 1); + ContentResolver.setSyncAutomatically(account, AUTHORITY, enabled); if (enabled) { - setSynchronizationInterval(preferences.getIntegerFromString(R.string.gtasks_GPr_interval_key, 0)); + ContentResolver.addPeriodicSync(account, AUTHORITY, Bundle.EMPTY, TimeUnit.HOURS.toSeconds(1)); } else { - setSynchronizationInterval(0); + ContentResolver.removePeriodicSync(account, AUTHORITY, Bundle.EMPTY); } } } - public void setSynchronizationInterval(int seconds) { - Account account = getAccount(); - if (account != null) { - boolean syncAutomatically = seconds > 0; - ContentResolver.setSyncAutomatically(account, AUTHORITY, syncAutomatically); - Timber.d("syncAutomatically=%s, syncInterval=%s", syncAutomatically, seconds); - if (syncAutomatically) { - ContentResolver.addPeriodicSync(account, AUTHORITY, Bundle.EMPTY, seconds); - } else { - ContentResolver.removePeriodicSync(account, AUTHORITY, Bundle.EMPTY); - } - } + public boolean isSyncEnabled() { + return isEnabled() && ContentResolver.getSyncAutomatically(getAccount(), AUTHORITY); } private Account getAccount() { return accountManager.getAccount(gtasksPreferenceService.getUserName()); } - public boolean shouldShowBackgroundSyncWarning() { - return isEnabled() && !masterSyncEnabled() && !ContentResolver.getPeriodicSyncs(getAccount(), AUTHORITY).isEmpty(); - } - public void checkPlayServices(TaskListFragment taskListFragment) { if (taskListFragment != null && preferences.getBoolean(R.string.sync_gtasks, false) && diff --git a/src/googleplay/java/org/tasks/injection/BroadcastComponent.java b/src/googleplay/java/org/tasks/injection/BroadcastComponent.java index 02113db18..c8223d9db 100644 --- a/src/googleplay/java/org/tasks/injection/BroadcastComponent.java +++ b/src/googleplay/java/org/tasks/injection/BroadcastComponent.java @@ -13,8 +13,6 @@ import org.tasks.receivers.CompleteTaskReceiver; import org.tasks.receivers.GoogleTaskPushReceiver; import org.tasks.receivers.ListNotificationReceiver; import org.tasks.receivers.MyPackageReplacedReceiver; -import org.tasks.receivers.RefreshReceiver; -import org.tasks.receivers.TaskNotificationReceiver; import org.tasks.receivers.TeslaUnreadReceiver; import org.tasks.widget.TasksWidget; @@ -40,10 +38,6 @@ public interface BroadcastComponent { void inject(MyPackageReplacedReceiver myPackageReplacedReceiver); - void inject(RefreshReceiver refreshReceiver); - - void inject(TaskNotificationReceiver taskNotificationReceiver); - void inject(CompleteTaskReceiver completeTaskReceiver); void inject(ListNotificationReceiver listNotificationReceiver); diff --git a/src/googleplay/java/org/tasks/injection/InjectingAbstractThreadedSyncAdapter.java b/src/googleplay/java/org/tasks/injection/InjectingAbstractThreadedSyncAdapter.java index 61e86dc92..15e8529bc 100644 --- a/src/googleplay/java/org/tasks/injection/InjectingAbstractThreadedSyncAdapter.java +++ b/src/googleplay/java/org/tasks/injection/InjectingAbstractThreadedSyncAdapter.java @@ -11,8 +11,8 @@ public abstract class InjectingAbstractThreadedSyncAdapter extends AbstractThrea } private void inject(Context context) { - inject(((InjectingApplication) context.getApplicationContext()) - .getComponent() + inject(Dagger.get(context) + .getApplicationComponent() .plus(new SyncAdapterModule())); } diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 3b0718bf9..b28031143 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -48,6 +48,11 @@ + + + + + @@ -207,14 +212,10 @@ - - - - + + + + + + + + diff --git a/src/main/java/com/todoroo/astrid/activity/TaskListActivity.java b/src/main/java/com/todoroo/astrid/activity/TaskListActivity.java index 92843dc8d..a29018a40 100644 --- a/src/main/java/com/todoroo/astrid/activity/TaskListActivity.java +++ b/src/main/java/com/todoroo/astrid/activity/TaskListActivity.java @@ -266,26 +266,7 @@ public class TaskListActivity extends InjectingAppCompatActivity implements repeatConfirmationReceiver, new IntentFilter(AstridApiConstants.BROADCAST_EVENT_TASK_REPEATED)); - TaskListFragment taskListFragment = getTaskListFragment(); - if (syncAdapterHelper.shouldShowBackgroundSyncWarning() && !preferences.getBoolean(R.string.p_sync_warning_shown, false)) { - if (taskListFragment != null) { - taskListFragment.makeSnackbar(R.string.master_sync_warning) - .setAction(R.string.TLA_menu_settings, view -> { - Intent intent = new Intent(Settings.ACTION_SYNC_SETTINGS); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); - }) - .setCallback(new Snackbar.Callback() { - @Override - public void onShown(Snackbar snackbar) { - preferences.setBoolean(R.string.p_sync_warning_shown, true); - } - }) - .show(); - } - } - - syncAdapterHelper.checkPlayServices(taskListFragment); + syncAdapterHelper.checkPlayServices(getTaskListFragment()); } public void restart() { diff --git a/src/main/java/com/todoroo/astrid/activity/TaskListFragment.java b/src/main/java/com/todoroo/astrid/activity/TaskListFragment.java index f5b2557dc..e8604acd5 100644 --- a/src/main/java/com/todoroo/astrid/activity/TaskListFragment.java +++ b/src/main/java/com/todoroo/astrid/activity/TaskListFragment.java @@ -361,10 +361,6 @@ public class TaskListFragment extends InjectingFragment implements refresh(); } - public Snackbar makeSnackbar(int resId) { - return makeSnackbar(getString(resId)); - } - public Snackbar makeSnackbar(String text) { Snackbar snackbar = Snackbar.make(coordinatorLayout, text, 8000) .setActionTextColor(getColor(context, R.color.snackbar_text_color)); diff --git a/src/main/java/com/todoroo/astrid/alarms/AlarmService.java b/src/main/java/com/todoroo/astrid/alarms/AlarmService.java index aaa5b5063..8ea0e7407 100644 --- a/src/main/java/com/todoroo/astrid/alarms/AlarmService.java +++ b/src/main/java/com/todoroo/astrid/alarms/AlarmService.java @@ -5,10 +5,7 @@ */ package com.todoroo.astrid.alarms; -import android.app.PendingIntent; import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; import com.todoroo.andlib.data.Callback; import com.todoroo.andlib.sql.Criterion; @@ -21,13 +18,13 @@ import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria; import com.todoroo.astrid.dao.TaskDao.TaskCriteria; import com.todoroo.astrid.data.Metadata; import com.todoroo.astrid.data.Task; -import com.todoroo.astrid.reminders.ReminderService; import com.todoroo.astrid.service.SynchronizeMetadataCallback; import org.tasks.injection.ApplicationScope; -import org.tasks.injection.ForApplication; -import org.tasks.receivers.TaskNotificationReceiver; -import org.tasks.scheduling.AlarmManager; +import org.tasks.jobs.Alarm; +import org.tasks.jobs.JobManager; +import org.tasks.jobs.JobQueue; +import org.tasks.preferences.Preferences; import java.util.ArrayList; import java.util.HashSet; @@ -48,15 +45,14 @@ public class AlarmService { private static final long NO_ALARM = Long.MAX_VALUE; + private final JobQueue jobs; + private final MetadataDao metadataDao; - private final Context context; - private final AlarmManager alarmManager; @Inject - public AlarmService(MetadataDao metadataDao, @ForApplication Context context, AlarmManager alarmManager) { + public AlarmService(MetadataDao metadataDao, JobManager jobManager, Preferences preferences) { this.metadataDao = metadataDao; - this.context = context; - this.alarmManager = alarmManager; + jobs = JobQueue.newAlarmQueue(preferences, jobManager); } public void getAlarms(long taskId, Callback callback) { @@ -79,11 +75,7 @@ public class AlarmService { metadata.add(item); } - boolean changed = synchronizeMetadata(taskId, metadata, m -> { - // Cancel the alarm before the metadata is deleted - PendingIntent pendingIntent = pendingIntentForAlarm(m, taskId); - alarmManager.cancel(pendingIntent); - }); + boolean changed = synchronizeMetadata(taskId, metadata, m -> jobs.cancel(m.getId())); if(changed) { scheduleAlarms(taskId); @@ -94,18 +86,23 @@ public class AlarmService { // --- alarm scheduling private void getActiveAlarms(Callback callback) { - metadataDao.query(callback, Query.select(Metadata.ID, Metadata.TASK, AlarmFields.TIME). + metadataDao.query(callback, Query.select(Metadata.PROPERTIES). join(Join.inner(Task.TABLE, Metadata.TASK.eq(Task.ID))). - where(Criterion.and(TaskCriteria.isActive(), MetadataCriteria.withKey(AlarmFields.METADATA_KEY)))); + where(Criterion.and(TaskCriteria.isActive(), + MetadataCriteria.withKey(AlarmFields.METADATA_KEY)))); } private void getActiveAlarmsForTask(long taskId, Callback callback) { - metadataDao.query(callback, Query.select(Metadata.ID, Metadata.TASK, AlarmFields.TIME). + metadataDao.query(callback, Query.select(Metadata.PROPERTIES). join(Join.inner(Task.TABLE, Metadata.TASK.eq(Task.ID))). where(Criterion.and(TaskCriteria.isActive(), MetadataCriteria.byTaskAndwithKey(taskId, AlarmFields.METADATA_KEY)))); } + public void clear() { + jobs.clear(); + } + /** * Schedules all alarms */ @@ -120,33 +117,20 @@ public class AlarmService { getActiveAlarmsForTask(taskId, this::scheduleAlarm); } - private PendingIntent pendingIntentForAlarm(Metadata alarm, long taskId) { - Intent intent = new Intent(context, TaskNotificationReceiver.class); - intent.setAction("ALARM" + alarm.getId()); //$NON-NLS-1$ - intent.putExtra(TaskNotificationReceiver.ID_KEY, taskId); - intent.putExtra(TaskNotificationReceiver.EXTRAS_TYPE, ReminderService.TYPE_ALARM); - - return PendingIntent.getBroadcast(context, (int)alarm.getId(), - intent, PendingIntent.FLAG_UPDATE_CURRENT); - } - /** * Schedules alarms for a single task */ - private void scheduleAlarm(Metadata alarm) { - if(alarm == null) { + private void scheduleAlarm(Metadata metadata) { + if(metadata == null) { return; } - long taskId = alarm.getTask(); - - PendingIntent pendingIntent = pendingIntentForAlarm(alarm, taskId); - - long time = alarm.getValue(AlarmFields.TIME); + Alarm alarm = new Alarm(metadata); + long time = alarm.getTime(); if(time == 0 || time == NO_ALARM) { - alarmManager.cancel(pendingIntent); - } else if(time > DateUtilities.now()) { - alarmManager.wakeupAdjustingForQuietHours(time, pendingIntent); + jobs.cancel(alarm.getId()); + } else { + jobs.add(alarm); } } @@ -200,4 +184,16 @@ public class AlarmService { return dirty[0]; } + + public void scheduleNextJob() { + jobs.scheduleNext(); + } + + public List getOverdueAlarms() { + return jobs.getOverdueJobs(); + } + + public boolean remove(Alarm alarm) { + return jobs.remove(alarm); + } } diff --git a/src/main/java/com/todoroo/astrid/backup/TasksXmlExporter.java b/src/main/java/com/todoroo/astrid/backup/TasksXmlExporter.java index 71518727e..8527d6f4b 100755 --- a/src/main/java/com/todoroo/astrid/backup/TasksXmlExporter.java +++ b/src/main/java/com/todoroo/astrid/backup/TasksXmlExporter.java @@ -9,6 +9,7 @@ import android.app.Activity; import android.app.ProgressDialog; import android.content.Context; import android.os.Handler; +import android.support.annotation.Nullable; import android.util.Xml; import android.widget.Toast; @@ -98,7 +99,7 @@ public class TasksXmlExporter { this.preferences = preferences; } - public void exportTasks(final Context context, final ExportType exportType, final ProgressDialog progressDialog) { + public void exportTasks(final Context context, final ExportType exportType, @Nullable final ProgressDialog progressDialog) { this.context = context; this.exportCount = 0; this.backupDirectory = preferences.getBackupDirectory(); @@ -106,9 +107,6 @@ public class TasksXmlExporter { this.progressDialog = progressDialog; handler = exportType == ExportType.EXPORT_TYPE_MANUAL ? new Handler() : null; - if(exportType != ExportType.EXPORT_TYPE_MANUAL) { - this.progressDialog = new ProgressDialog(context); - } new Thread(() -> { try { @@ -129,7 +127,7 @@ public class TasksXmlExporter { Timber.e(e, e.getMessage()); } finally { post(() -> { - if(progressDialog.isShowing() && context instanceof Activity) { + if(progressDialog != null && progressDialog.isShowing() && context instanceof Activity) { DialogUtilities.dismissDialog((Activity) context, progressDialog); } }); diff --git a/src/main/java/com/todoroo/astrid/core/CustomFilterAdapter.java b/src/main/java/com/todoroo/astrid/core/CustomFilterAdapter.java index 6cdc3b8f7..214a489eb 100644 --- a/src/main/java/com/todoroo/astrid/core/CustomFilterAdapter.java +++ b/src/main/java/com/todoroo/astrid/core/CustomFilterAdapter.java @@ -44,7 +44,7 @@ class CustomFilterAdapter extends ArrayAdapter { public CustomFilterAdapter(CustomFilterActivity activity, DialogBuilder dialogBuilder, List objects, Locale locale) { - super(activity, R.id.name, objects); + super(activity, 0, objects); this.activity = activity; this.dialogBuilder = dialogBuilder; this.locale = locale; diff --git a/src/main/java/com/todoroo/astrid/core/DefaultsPreferences.java b/src/main/java/com/todoroo/astrid/core/DefaultsPreferences.java index 2a1f234e6..10f40ce72 100644 --- a/src/main/java/com/todoroo/astrid/core/DefaultsPreferences.java +++ b/src/main/java/com/todoroo/astrid/core/DefaultsPreferences.java @@ -6,7 +6,6 @@ package com.todoroo.astrid.core; import android.content.Intent; -import android.content.pm.PackageManager; import android.os.Bundle; import android.preference.Preference; import android.support.annotation.NonNull; @@ -23,6 +22,8 @@ import org.tasks.preferences.Preferences; import javax.inject.Inject; +import static org.tasks.PermissionUtil.verifyPermissions; + /** * Displays the preference screen for users to edit their preferences * @@ -70,7 +71,7 @@ public class DefaultsPreferences extends InjectingPreferenceActivity { @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == PermissionRequestor.REQUEST_CALENDAR) { - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + if (verifyPermissions(grantResults)) { startCalendarSelectionActivity(); } } else { diff --git a/src/main/java/com/todoroo/astrid/files/AACRecordingActivity.java b/src/main/java/com/todoroo/astrid/files/AACRecordingActivity.java index 9554682df..97fae5dd2 100644 --- a/src/main/java/com/todoroo/astrid/files/AACRecordingActivity.java +++ b/src/main/java/com/todoroo/astrid/files/AACRecordingActivity.java @@ -6,25 +6,20 @@ package com.todoroo.astrid.files; import android.content.Intent; -import android.content.pm.PackageManager; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.app.FragmentManager; -import android.widget.Chronometer; -import org.tasks.R; import org.tasks.dialogs.RecordAudioDialog; import org.tasks.injection.ActivityComponent; import org.tasks.injection.InjectingAppCompatActivity; import org.tasks.preferences.ActivityPermissionRequestor; import org.tasks.preferences.PermissionRequestor; -import org.tasks.preferences.Preferences; import org.tasks.themes.Theme; import javax.inject.Inject; -import butterknife.BindView; - +import static org.tasks.PermissionUtil.verifyPermissions; import static org.tasks.dialogs.RecordAudioDialog.newRecordAudioDialog; public class AACRecordingActivity extends InjectingAppCompatActivity implements RecordAudioDialog.RecordAudioDialogCallback { @@ -33,12 +28,9 @@ public class AACRecordingActivity extends InjectingAppCompatActivity implements public static final String RESULT_OUTFILE = "outfile"; //$NON-NLS-1$ - @Inject Preferences preferences; @Inject ActivityPermissionRequestor permissionRequestor; @Inject Theme theme; - @BindView(R.id.timer) Chronometer timer; - @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -66,7 +58,7 @@ public class AACRecordingActivity extends InjectingAppCompatActivity implements @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == PermissionRequestor.REQUEST_MIC) { - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + if (verifyPermissions(grantResults)) { showDialog(); } else { finish(); diff --git a/src/main/java/com/todoroo/astrid/reminders/ReminderPreferences.java b/src/main/java/com/todoroo/astrid/reminders/ReminderPreferences.java index 0cd5e939d..281cdd98f 100644 --- a/src/main/java/com/todoroo/astrid/reminders/ReminderPreferences.java +++ b/src/main/java/com/todoroo/astrid/reminders/ReminderPreferences.java @@ -6,7 +6,6 @@ package com.todoroo.astrid.reminders; import android.content.Intent; -import android.content.pm.PackageManager; import android.media.Ringtone; import android.media.RingtoneManager; import android.net.Uri; @@ -14,13 +13,13 @@ import android.os.Bundle; import android.preference.CheckBoxPreference; import android.preference.Preference; import android.preference.PreferenceManager; +import android.provider.Settings; import android.support.annotation.NonNull; import org.tasks.R; import org.tasks.activities.ColorPickerActivity; import org.tasks.activities.TimePickerActivity; import org.tasks.dialogs.ColorPickerDialog; -import org.tasks.dialogs.DialogBuilder; import org.tasks.injection.ActivityComponent; import org.tasks.injection.InjectingPreferenceActivity; import org.tasks.preferences.ActivityPermissionRequestor; @@ -29,7 +28,7 @@ import org.tasks.preferences.PermissionChecker; import org.tasks.preferences.PermissionRequestor; import org.tasks.preferences.Preferences; import org.tasks.scheduling.GeofenceSchedulingIntentService; -import org.tasks.scheduling.ReminderSchedulerIntentService; +import org.tasks.scheduling.NotificationSchedulerIntentService; import org.tasks.themes.LEDColor; import org.tasks.themes.ThemeCache; import org.tasks.time.DateTime; @@ -38,7 +37,7 @@ import org.tasks.ui.TimePreference; import javax.inject.Inject; import static com.todoroo.andlib.utility.AndroidUtilities.atLeastJellybean; -import static com.todoroo.andlib.utility.AndroidUtilities.atLeastMarshmallow; +import static org.tasks.PermissionUtil.verifyPermissions; public class ReminderPreferences extends InjectingPreferenceActivity { @@ -50,7 +49,6 @@ public class ReminderPreferences extends InjectingPreferenceActivity { @Inject Device device; @Inject ActivityPermissionRequestor permissionRequestor; @Inject PermissionChecker permissionChecker; - @Inject DialogBuilder dialogBuilder; @Inject Preferences preferences; @Inject ThemeCache themeCache; @@ -64,7 +62,6 @@ public class ReminderPreferences extends InjectingPreferenceActivity { rescheduleNotificationsOnChange( R.string.p_rmd_time, - R.string.p_doze_notifications, R.string.p_rmd_enable_quiet, R.string.p_rmd_quietStart, R.string.p_rmd_quietEnd); @@ -89,7 +86,6 @@ public class ReminderPreferences extends InjectingPreferenceActivity { }); requires(R.string.notification_shade, atLeastJellybean(), R.string.p_rmd_notif_actions_enabled, R.string.p_notification_priority, R.string.p_rmd_show_description); - requires(atLeastMarshmallow(), R.string.p_doze_notifications); requires(device.supportsLocationServices(), R.string.geolocation_reminders); updateLEDColor(); @@ -98,7 +94,7 @@ public class ReminderPreferences extends InjectingPreferenceActivity { private void rescheduleNotificationsOnChange(int... resIds) { for (int resId : resIds) { findPreference(getString(resId)).setOnPreferenceChangeListener((preference, newValue) -> { - startService(new Intent(ReminderPreferences.this, ReminderSchedulerIntentService.class)); + startService(new Intent(ReminderPreferences.this, NotificationSchedulerIntentService.class)); return true; }); } @@ -116,12 +112,9 @@ public class ReminderPreferences extends InjectingPreferenceActivity { @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == PermissionRequestor.REQUEST_CONTACTS) { - for (int grantResult : grantResults) { - if (grantResult != PackageManager.PERMISSION_GRANTED) { - return; - } + if (verifyPermissions(grantResults)) { + fieldMissedCalls.setChecked(true); } - fieldMissedCalls.setChecked(true); } else { super.onRequestPermissionsResult(requestCode, permissions, grantResults); } @@ -143,8 +136,7 @@ public class ReminderPreferences extends InjectingPreferenceActivity { preference.setSummary(R.string.silent); } else { Ringtone ringtone = RingtoneManager.getRingtone(ReminderPreferences.this, value == null - ? RingtoneManager.getActualDefaultRingtoneUri(getApplicationContext(), RingtoneManager.TYPE_NOTIFICATION) - : Uri.parse((String) value)); + ? Settings.System.DEFAULT_NOTIFICATION_URI : Uri.parse((String) value)); preference.setSummary(ringtone == null ? "" : ringtone.getTitle(ReminderPreferences.this)); } return true; diff --git a/src/main/java/com/todoroo/astrid/reminders/ReminderService.java b/src/main/java/com/todoroo/astrid/reminders/ReminderService.java index 9a3143e04..0d4d20a0b 100644 --- a/src/main/java/com/todoroo/astrid/reminders/ReminderService.java +++ b/src/main/java/com/todoroo/astrid/reminders/ReminderService.java @@ -5,11 +5,6 @@ */ package com.todoroo.astrid.reminders; -import android.annotation.SuppressLint; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; - import com.todoroo.andlib.data.Property; import com.todoroo.andlib.sql.Criterion; import com.todoroo.andlib.sql.Query; @@ -20,26 +15,19 @@ import com.todoroo.astrid.data.Task; import org.tasks.R; import org.tasks.injection.ApplicationScope; -import org.tasks.injection.ForApplication; +import org.tasks.jobs.JobManager; +import org.tasks.jobs.JobQueue; +import org.tasks.jobs.Reminder; import org.tasks.preferences.Preferences; -import org.tasks.receivers.TaskNotificationReceiver; -import org.tasks.scheduling.AlarmManager; +import org.tasks.reminders.Random; import org.tasks.time.DateTime; -import java.util.Random; +import java.util.List; import javax.inject.Inject; -import timber.log.Timber; - import static org.tasks.date.DateTimeUtils.newDateTime; -/** - * Data service for reminders - * - * @author Tim Su - * - */ @ApplicationScope public final class ReminderService { @@ -69,70 +57,68 @@ public final class ReminderService { /** flag for an alarm reminder */ public static final int TYPE_ALARM = 4; - private static final Random random = new Random(); + private static final long NO_ALARM = Long.MAX_VALUE; - // --- instance variables - - private AlarmScheduler scheduler; + private final JobQueue jobs; + private final Random random; + private final Preferences preferences; private long now = -1; // For tracking when reminders might be scheduled all at once - private final Context context; - private final Preferences preferences; @Inject - ReminderService(@ForApplication Context context, Preferences preferences, AlarmManager alarmManager) { - this.context = context; + ReminderService(Preferences preferences, JobManager jobManager) { + this(preferences, JobQueue.newReminderQueue(preferences, jobManager), new Random()); + } + + ReminderService(Preferences preferences, JobQueue jobs, Random random) { this.preferences = preferences; - scheduler = new ReminderAlarmScheduler(alarmManager); + this.jobs = jobs; + this.random = random; } private static final int MILLIS_PER_HOUR = 60 * 60 * 1000; - // --- reminder scheduling logic - - /** - * Schedules all alarms - */ public void scheduleAllAlarms(TaskDao taskDao) { now = DateUtilities.now(); // Before mass scheduling, initialize now variable Query query = Query.select(NOTIFICATION_PROPERTIES).where(Criterion.and( TaskCriteria.isActive(), Criterion.or(Task.REMINDER_FLAGS.gt(0), Task.REMINDER_PERIOD.gt(0)))); - taskDao.forEach(query, task -> scheduleAlarm(task, null)); + taskDao.forEach(query, task -> scheduleAlarm(null, task)); now = -1; // Signal done with now variable } + public void clear() { + jobs.clear(); + } + private long getNowValue() { // If we're in the midst of mass scheduling, use the prestored now var return (now == -1 ? DateUtilities.now() : now); } - public static final long NO_ALARM = Long.MAX_VALUE; + public List getPastReminders() { + return jobs.getOverdueJobs(); + } - /** - * Schedules alarms for a single task - */ - public void scheduleAlarm(TaskDao taskDao, Task task) { - scheduleAlarm(task, taskDao); + public boolean remove(Reminder reminder) { + return jobs.remove(reminder); } - private void clearAllAlarms(Task task) { - scheduler.createAlarm(context, task, NO_ALARM, TYPE_SNOOZE); - scheduler.createAlarm(context, task, NO_ALARM, TYPE_RANDOM); - scheduler.createAlarm(context, task, NO_ALARM, TYPE_DUE); - scheduler.createAlarm(context, task, NO_ALARM, TYPE_OVERDUE); + public void scheduleNextJob() { + jobs.scheduleNext(); } - private void scheduleAlarm(Task task, TaskDao taskDao) { + public void scheduleAlarm(TaskDao taskDao, Task task) { if(task == null || !task.isSaved()) { return; } // read data if necessary + long taskId = task.getId(); if(taskDao != null) { for(Property property : NOTIFICATION_PROPERTIES) { if(!task.containsValue(property)) { - task = taskDao.fetch(task.getId(), NOTIFICATION_PROPERTIES); + task = taskDao.fetch(taskId, NOTIFICATION_PROPERTIES); if(task == null) { return; } @@ -143,7 +129,8 @@ public final class ReminderService { // Make sure no alarms are scheduled other than the next one. When that one is shown, it // will schedule the next one after it, and so on and so forth. - clearAllAlarms(task); + jobs.cancel(taskId); + if(task.isCompleted() || task.isDeleted()) { return; } @@ -157,15 +144,15 @@ public final class ReminderService { // notifications at due date long whenDueDate = calculateNextDueDateReminder(task); - // notifications after due date long whenOverdue = calculateNextOverdueReminder(task); - // For alarms around/before now, increment the now value so the next one will be later - if (whenDueDate <= now || whenOverdue <= now) { + if (whenDueDate <= now) { whenDueDate = now; + } + + if (whenOverdue <= now) { whenOverdue = now; - now += 30 * DateUtilities.ONE_MINUTE; // Prevents overdue tasks from being scheduled all at once } // if random reminders are too close to due date, favor due date @@ -175,15 +162,13 @@ public final class ReminderService { // snooze trumps all if(whenSnooze != NO_ALARM) { - scheduler.createAlarm(context, task, whenSnooze, TYPE_SNOOZE); + jobs.add(new Reminder(taskId, whenSnooze, TYPE_SNOOZE)); } else if(whenRandom < whenDueDate && whenRandom < whenOverdue) { - scheduler.createAlarm(context, task, whenRandom, TYPE_RANDOM); + jobs.add(new Reminder(taskId, whenRandom, TYPE_RANDOM)); } else if(whenDueDate < whenOverdue) { - scheduler.createAlarm(context, task, whenDueDate, TYPE_DUE); + jobs.add(new Reminder(taskId, whenDueDate, TYPE_DUE)); } else if(whenOverdue != NO_ALARM) { - scheduler.createAlarm(context, task, whenOverdue, TYPE_OVERDUE); - } else { - scheduler.createAlarm(context, task, 0, 0); + jobs.add(new Reminder(taskId, whenOverdue, TYPE_OVERDUE)); } } @@ -249,7 +234,7 @@ public final class ReminderService { * If the date was indicated to not have a due time, we read from * preferences and assign a time. */ - long calculateNextDueDateReminder(Task task) { + private long calculateNextDueDateReminder(Task task) { if(task.hasDueDate() && task.isNotifyAtDeadline()) { long dueDate = task.getDueDate(); long lastReminder = task.getReminderLast(); @@ -296,67 +281,4 @@ public final class ReminderService { } return NO_ALARM; } - - // --- alarm manager alarm creation - - /** - * Interface for testing - */ - public interface AlarmScheduler { - void createAlarm(Context context, Task task, long time, int type); - } - - public void setScheduler(AlarmScheduler scheduler) { - this.scheduler = scheduler; - } - - public AlarmScheduler getScheduler() { - return scheduler; - } - - private static class ReminderAlarmScheduler implements AlarmScheduler { - private final AlarmManager alarmManager; - - public ReminderAlarmScheduler(AlarmManager alarmManager) { - this.alarmManager = alarmManager; - } - - /** - * Create an alarm for the given task at the given type - */ - @Override - public void createAlarm(Context context, Task task, long time, int type) { - if(task.getId() == Task.NO_ID) { - return; - } - Intent intent = new Intent(context, TaskNotificationReceiver.class); - intent.setType(Long.toString(task.getId())); - intent.setAction(Integer.toString(type)); - intent.putExtra(TaskNotificationReceiver.ID_KEY, task.getId()); - intent.putExtra(TaskNotificationReceiver.EXTRAS_TYPE, type); - - // calculate the unique requestCode as a combination of the task-id and alarm-type: - // concatenate id+type to keep the combo unique - @SuppressLint("DefaultLocale") String rc = String.format("%d%d", task.getId(), type); - int requestCode; - try { - requestCode = Integer.parseInt(rc); - } catch (Exception e) { - Timber.e(e, e.getMessage()); - requestCode = type; - } - PendingIntent pendingIntent = PendingIntent.getBroadcast(context, requestCode, - intent, 0); - - if (time == 0 || time == NO_ALARM) { - alarmManager.cancel(pendingIntent); - } else { - if(time < DateUtilities.now()) { - time = DateUtilities.now() + 5000L; - } - - alarmManager.wakeupAdjustingForQuietHours(time, pendingIntent); - } - } - } } diff --git a/src/main/java/com/todoroo/astrid/ui/ReminderControlSet.java b/src/main/java/com/todoroo/astrid/ui/ReminderControlSet.java index b75174907..395018189 100644 --- a/src/main/java/com/todoroo/astrid/ui/ReminderControlSet.java +++ b/src/main/java/com/todoroo/astrid/ui/ReminderControlSet.java @@ -8,7 +8,6 @@ package com.todoroo.astrid.ui; import android.app.Activity; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; import android.os.Bundle; import android.os.Parcelable; import android.support.annotation.NonNull; @@ -58,6 +57,7 @@ import butterknife.OnItemSelected; import static com.google.common.collect.Lists.newArrayList; import static com.google.common.collect.Sets.newHashSet; import static com.todoroo.andlib.utility.DateUtilities.getLongDateStringWithTime; +import static org.tasks.PermissionUtil.verifyPermissions; import static org.tasks.date.DateTimeUtils.newDateTime; /** @@ -269,7 +269,7 @@ public class ReminderControlSet extends TaskEditControlFragment { @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == PermissionRequestor.REQUEST_LOCATION) { - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + if (verifyPermissions(grantResults)) { pickLocation(); } } else { diff --git a/src/main/java/org/tasks/PermissionUtil.java b/src/main/java/org/tasks/PermissionUtil.java index 71e54e244..ab5e779f1 100644 --- a/src/main/java/org/tasks/PermissionUtil.java +++ b/src/main/java/org/tasks/PermissionUtil.java @@ -4,7 +4,8 @@ import android.content.pm.PackageManager; public abstract class PermissionUtil { public static boolean verifyPermissions(int[] grantResults) { - if(grantResults.length < 1){ + if(grantResults.length == 0) { + // request canceled return false; } diff --git a/src/main/java/org/tasks/Tasks.java b/src/main/java/org/tasks/Tasks.java index b87a34ee5..ccfb91ac0 100644 --- a/src/main/java/org/tasks/Tasks.java +++ b/src/main/java/org/tasks/Tasks.java @@ -28,11 +28,12 @@ public class Tasks extends InjectingApplication { tracker.setTrackingEnabled(preferences.isTrackingEnabled()); - AndroidThreeTen.init(this); - if (!buildSetup.setup()) { return; } + + AndroidThreeTen.init(this); + flavorSetup.setup(); teslaUnreadReceiver.setEnabled(preferences.getBoolean(R.string.p_tesla_unread_enabled, false)); diff --git a/src/main/java/org/tasks/activities/AddAttachmentActivity.java b/src/main/java/org/tasks/activities/AddAttachmentActivity.java index 4cae33eb7..57368bd97 100644 --- a/src/main/java/org/tasks/activities/AddAttachmentActivity.java +++ b/src/main/java/org/tasks/activities/AddAttachmentActivity.java @@ -31,6 +31,8 @@ import javax.inject.Inject; import timber.log.Timber; +import static com.google.common.base.Strings.isNullOrEmpty; + public class AddAttachmentActivity extends InjectingAppCompatActivity implements DialogInterface.OnCancelListener, AddAttachmentDialog.AddAttachmentCallback { private static final String FRAG_TAG_ATTACHMENT_DIALOG = "frag_tag_attachment_dialog"; @@ -159,7 +161,9 @@ public class AddAttachmentActivity extends InjectingAppCompatActivity implements private File getFilename(String extension) { AtomicReference nameRef = new AtomicReference<>(); - if (!extension.startsWith(".")) { + if (isNullOrEmpty(extension)) { + extension = ""; + } else if (!extension.startsWith(".")) { extension = "." + extension; } try { diff --git a/src/main/java/org/tasks/activities/CalendarSelectionDialog.java b/src/main/java/org/tasks/activities/CalendarSelectionDialog.java index 002ec93fe..f622d795e 100644 --- a/src/main/java/org/tasks/activities/CalendarSelectionDialog.java +++ b/src/main/java/org/tasks/activities/CalendarSelectionDialog.java @@ -1,7 +1,6 @@ package org.tasks.activities; import android.app.Dialog; -import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; import android.support.annotation.NonNull; @@ -14,7 +13,6 @@ import org.tasks.calendars.CalendarProvider; import org.tasks.dialogs.AlertDialogBuilder; import org.tasks.dialogs.DialogBuilder; import org.tasks.injection.DialogFragmentComponent; -import org.tasks.injection.ForActivity; import org.tasks.injection.InjectingDialogFragment; import org.tasks.preferences.FragmentPermissionRequestor; import org.tasks.preferences.PermissionChecker; @@ -47,7 +45,6 @@ public class CalendarSelectionDialog extends InjectingDialogFragment { @Inject DialogBuilder dialogBuilder; @Inject CalendarProvider calendarProvider; - @Inject @ForActivity Context context; @Inject FragmentPermissionRequestor fragmentPermissionRequestor; @Inject PermissionChecker permissionChecker; @Inject Theme theme; @@ -114,7 +111,7 @@ public class CalendarSelectionDialog extends InjectingDialogFragment { @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == PermissionRequestor.REQUEST_CALENDAR) { - if (grantResults.length > 0 && !verifyPermissions(grantResults)) { + if (!verifyPermissions(grantResults)) { handler.cancel(); } } else { diff --git a/src/main/java/org/tasks/activities/DatePickerActivity.java b/src/main/java/org/tasks/activities/DatePickerActivity.java index daec9d99e..a64a95691 100644 --- a/src/main/java/org/tasks/activities/DatePickerActivity.java +++ b/src/main/java/org/tasks/activities/DatePickerActivity.java @@ -6,11 +6,11 @@ import android.os.Bundle; import com.wdullaer.materialdatetimepicker.date.DatePickerDialog; +import org.tasks.R; import org.tasks.dialogs.MyDatePickerDialog; import org.tasks.dialogs.NativeDatePickerDialog; import org.tasks.injection.ActivityComponent; import org.tasks.injection.InjectingAppCompatActivity; -import org.tasks.preferences.Device; import org.tasks.preferences.Preferences; import org.tasks.themes.ThemeAccent; import org.tasks.themes.ThemeBase; @@ -18,8 +18,6 @@ import org.tasks.time.DateTime; import javax.inject.Inject; -import static com.todoroo.andlib.utility.AndroidUtilities.atLeastLollipop; -import static com.todoroo.andlib.utility.AndroidUtilities.atLeastMarshmallow; import static org.tasks.dialogs.NativeDatePickerDialog.newNativeDatePickerDialog; import static org.tasks.time.DateTimeUtils.currentTimeMillis; @@ -31,7 +29,6 @@ public class DatePickerActivity extends InjectingAppCompatActivity implements Da @Inject ThemeBase themeBase; @Inject ThemeAccent themeAccent; - @Inject Device device; @Inject Preferences preferences; @Override @@ -42,7 +39,8 @@ public class DatePickerActivity extends InjectingAppCompatActivity implements Da DateTime initial = (timestamp > 0 ? new DateTime(timestamp) : new DateTime()).startOfDay(); FragmentManager fragmentManager = getFragmentManager(); - if (atLeastMarshmallow() || (atLeastLollipop() && !device.isBaneOfMyExistence())) { + + if (preferences.getBoolean(R.string.p_use_native_datetime_pickers, false)) { if (fragmentManager.findFragmentByTag(FRAG_TAG_DATE_PICKER) == null) { newNativeDatePickerDialog(initial) .show(fragmentManager, FRAG_TAG_DATE_PICKER); diff --git a/src/main/java/org/tasks/activities/TimePickerActivity.java b/src/main/java/org/tasks/activities/TimePickerActivity.java index 9ecbafd1d..44d7d8a68 100644 --- a/src/main/java/org/tasks/activities/TimePickerActivity.java +++ b/src/main/java/org/tasks/activities/TimePickerActivity.java @@ -7,17 +7,18 @@ import android.text.format.DateFormat; import com.wdullaer.materialdatetimepicker.time.TimePickerDialog; +import org.tasks.R; import org.tasks.dialogs.MyTimePickerDialog; import org.tasks.dialogs.NativeTimePickerDialog; import org.tasks.injection.ActivityComponent; import org.tasks.injection.InjectingAppCompatActivity; +import org.tasks.preferences.Preferences; import org.tasks.themes.ThemeAccent; import org.tasks.themes.ThemeBase; import org.tasks.time.DateTime; import javax.inject.Inject; -import static com.todoroo.andlib.utility.AndroidUtilities.atLeastLollipop; import static org.tasks.dialogs.NativeTimePickerDialog.newNativeTimePickerDialog; import static org.tasks.time.DateTimeUtils.currentTimeMillis; @@ -30,6 +31,7 @@ public class TimePickerActivity extends InjectingAppCompatActivity implements Ti @Inject ThemeBase themeBase; @Inject ThemeAccent themeAccent; + @Inject Preferences preferences; private DateTime initial; @@ -40,7 +42,7 @@ public class TimePickerActivity extends InjectingAppCompatActivity implements Ti initial = new DateTime(getIntent().getLongExtra(EXTRA_TIMESTAMP, currentTimeMillis())); FragmentManager fragmentManager = getFragmentManager(); - if (atLeastLollipop()) { + if (preferences.getBoolean(R.string.p_use_native_datetime_pickers, false)) { if (fragmentManager.findFragmentByTag(FRAG_TAG_TIME_PICKER) == null) { newNativeTimePickerDialog(initial) .show(fragmentManager, FRAG_TAG_TIME_PICKER); diff --git a/src/main/java/org/tasks/files/FileExplore.java b/src/main/java/org/tasks/files/FileExplore.java index aa3c7cf2d..b317acf54 100644 --- a/src/main/java/org/tasks/files/FileExplore.java +++ b/src/main/java/org/tasks/files/FileExplore.java @@ -2,7 +2,6 @@ package org.tasks.files; import android.app.Activity; import android.content.Intent; -import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; import android.os.Environment; @@ -20,6 +19,8 @@ import java.io.File; import javax.inject.Inject; +import static org.tasks.PermissionUtil.verifyPermissions; + public class FileExplore extends InjectingAppCompatActivity { private static final int REQUEST_PICKER = 1000; @@ -79,12 +80,10 @@ public class FileExplore extends InjectingAppCompatActivity { @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == PermissionRequestor.REQUEST_FILE_WRITE) { - if (grantResults.length > 0) { - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - launchPicker(); - } else { - finish(); - } + if (verifyPermissions(grantResults)) { + launchPicker(); + } else { + finish(); } } else { super.onRequestPermissionsResult(requestCode, permissions, grantResults); diff --git a/src/main/java/org/tasks/injection/Dagger.java b/src/main/java/org/tasks/injection/Dagger.java new file mode 100644 index 000000000..cf7369ac0 --- /dev/null +++ b/src/main/java/org/tasks/injection/Dagger.java @@ -0,0 +1,45 @@ +package org.tasks.injection; + +import android.content.Context; + +import org.tasks.locale.Locale; + +import timber.log.Timber; + +class Dagger { + + private static final Object lock = new Object(); + + private static Dagger instance; + + public static Dagger get(Context context) { + if (instance == null) { + synchronized (lock) { + if (instance == null) { + instance = new Dagger(context); + } + } + } + return instance; + } + + private ApplicationComponent applicationComponent; + + private Dagger(Context context) { + Context localeContext = context.getApplicationContext(); + try { + localeContext = Locale.getInstance(localeContext) + .createConfigurationContext(localeContext); + } catch (Exception e) { + Timber.e(e.getMessage(), e); + } + + applicationComponent = DaggerApplicationComponent.builder() + .applicationModule(new ApplicationModule(localeContext)) + .build(); + } + + ApplicationComponent getApplicationComponent() { + return applicationComponent; + } +} diff --git a/src/main/java/org/tasks/injection/InjectingApplication.java b/src/main/java/org/tasks/injection/InjectingApplication.java index f2af6fb9c..df5f01cbd 100644 --- a/src/main/java/org/tasks/injection/InjectingApplication.java +++ b/src/main/java/org/tasks/injection/InjectingApplication.java @@ -15,9 +15,7 @@ public abstract class InjectingApplication extends BaseApplication { Context context = Locale.getInstance(this).createConfigurationContext(getApplicationContext()); - applicationComponent = DaggerApplicationComponent.builder() - .applicationModule(new ApplicationModule(context)) - .build(); + applicationComponent = Dagger.get(context).getApplicationComponent(); inject(applicationComponent); } diff --git a/src/main/java/org/tasks/injection/IntentServiceComponent.java b/src/main/java/org/tasks/injection/IntentServiceComponent.java index 3400b6a1f..5cad8c782 100644 --- a/src/main/java/org/tasks/injection/IntentServiceComponent.java +++ b/src/main/java/org/tasks/injection/IntentServiceComponent.java @@ -1,25 +1,37 @@ package org.tasks.injection; +import org.tasks.jobs.AlarmJob; +import org.tasks.jobs.BackupJob; +import org.tasks.jobs.MidnightRefreshJob; +import org.tasks.jobs.RefreshJob; +import org.tasks.jobs.ReminderJob; import org.tasks.location.GeofenceTransitionsIntentService; -import org.tasks.scheduling.BackupIntentService; import org.tasks.scheduling.CalendarNotificationIntentService; import org.tasks.scheduling.GeofenceSchedulingIntentService; -import org.tasks.scheduling.RefreshSchedulerIntentService; -import org.tasks.scheduling.ReminderSchedulerIntentService; +import org.tasks.scheduling.NotificationSchedulerIntentService; +import org.tasks.scheduling.SchedulerIntentService; import dagger.Subcomponent; @Subcomponent(modules = IntentServiceModule.class) public interface IntentServiceComponent { - void inject(ReminderSchedulerIntentService reminderSchedulerIntentService); - - void inject(RefreshSchedulerIntentService refreshSchedulerIntentService); + void inject(SchedulerIntentService schedulerIntentService); void inject(GeofenceSchedulingIntentService geofenceSchedulingIntentService); void inject(CalendarNotificationIntentService calendarNotificationIntentService); - void inject(BackupIntentService backupIntentService); - void inject(GeofenceTransitionsIntentService geofenceTransitionsIntentService); + + void inject(NotificationSchedulerIntentService notificationSchedulerIntentService); + + void inject(AlarmJob alarmJob); + + void inject(BackupJob backupJob); + + void inject(MidnightRefreshJob midnightRefreshJob); + + void inject(RefreshJob refreshJob); + + void inject(ReminderJob reminderJob); } diff --git a/src/main/java/org/tasks/jobs/Alarm.java b/src/main/java/org/tasks/jobs/Alarm.java new file mode 100644 index 000000000..ffb9074b6 --- /dev/null +++ b/src/main/java/org/tasks/jobs/Alarm.java @@ -0,0 +1,58 @@ +package org.tasks.jobs; + +import com.todoroo.astrid.alarms.AlarmFields; +import com.todoroo.astrid.data.Metadata; + +public class Alarm implements JobQueueEntry { + private final long alarmId; + private final long taskId; + private final long time; + + public Alarm(Metadata metadata) { + this(metadata.getId(), metadata.getTask(), metadata.getValue(AlarmFields.TIME)); + } + + public Alarm(long alarmId, long taskId, Long time) { + this.alarmId = alarmId; + this.taskId = taskId; + this.time = time; + } + + @Override + public long getId() { + return alarmId; + } + + public long getTaskId() { + return taskId; + } + + @Override + public long getTime() { + return time; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Alarm alarm = (Alarm) o; + + return alarmId == alarm.alarmId; + } + + @Override + public int hashCode() { + return (int) (alarmId ^ (alarmId >>> 32)); + } + + @Override + public String toString() { + return "Alarm{" + + "alarmId=" + alarmId + + ", taskId=" + taskId + + ", time=" + time + + '}'; + } +} diff --git a/src/main/java/org/tasks/jobs/AlarmJob.java b/src/main/java/org/tasks/jobs/AlarmJob.java new file mode 100644 index 000000000..f8b858a2e --- /dev/null +++ b/src/main/java/org/tasks/jobs/AlarmJob.java @@ -0,0 +1,56 @@ +package org.tasks.jobs; + +import android.content.Intent; + +import com.todoroo.astrid.alarms.AlarmService; +import com.todoroo.astrid.dao.TaskDao; +import com.todoroo.astrid.data.Task; +import com.todoroo.astrid.reminders.ReminderService; + +import org.tasks.Notifier; +import org.tasks.injection.IntentServiceComponent; +import org.tasks.preferences.Preferences; + +import javax.inject.Inject; + +public class AlarmJob extends WakefulJob { + + public static final String TAG = "job_alarm"; + + @Inject Preferences preferences; + @Inject AlarmService alarmService; + @Inject Notifier notifier; + @Inject TaskDao taskDao; + + public AlarmJob() { + super(AlarmJob.class.getSimpleName()); + } + + @Override + protected void run() { + if (!preferences.isCurrentlyQuietHours()) { + for (Alarm alarm : alarmService.getOverdueAlarms()) { + Task task = taskDao.fetch(alarm.getTaskId(), Task.REMINDER_LAST); + if (task != null && task.getReminderLast() < alarm.getTime()) { + notifier.triggerTaskNotification(alarm.getTaskId(), ReminderService.TYPE_ALARM); + } + alarmService.remove(alarm); + } + } + } + + @Override + protected void scheduleNext() { + alarmService.scheduleNextJob(); + } + + @Override + protected void inject(IntentServiceComponent component) { + component.inject(this); + } + + @Override + protected void completeWakefulIntent(Intent intent) { + AlarmJobBroadcast.completeWakefulIntent(intent); + } +} diff --git a/src/main/java/org/tasks/jobs/AlarmJobBroadcast.java b/src/main/java/org/tasks/jobs/AlarmJobBroadcast.java new file mode 100644 index 000000000..233f94770 --- /dev/null +++ b/src/main/java/org/tasks/jobs/AlarmJobBroadcast.java @@ -0,0 +1,12 @@ +package org.tasks.jobs; + +import android.content.Context; +import android.content.Intent; +import android.support.v4.content.WakefulBroadcastReceiver; + +public class AlarmJobBroadcast extends WakefulBroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + startWakefulService(context, new Intent(context, AlarmJob.class)); + } +} diff --git a/src/main/java/org/tasks/scheduling/BackupIntentService.java b/src/main/java/org/tasks/jobs/BackupJob.java similarity index 67% rename from src/main/java/org/tasks/scheduling/BackupIntentService.java rename to src/main/java/org/tasks/jobs/BackupJob.java index cf4cab95a..0684e1647 100644 --- a/src/main/java/org/tasks/scheduling/BackupIntentService.java +++ b/src/main/java/org/tasks/jobs/BackupJob.java @@ -1,9 +1,10 @@ -package org.tasks.scheduling; +package org.tasks.jobs; import android.content.Context; import com.todoroo.astrid.backup.TasksXmlExporter; +import org.tasks.injection.ForApplication; import org.tasks.injection.IntentServiceComponent; import org.tasks.preferences.Preferences; @@ -15,41 +16,42 @@ import javax.inject.Inject; import timber.log.Timber; -public class BackupIntentService extends MidnightIntentService { +public class BackupJob extends MidnightJob { + + public static final String TAG = "job_backup"; public static final String BACKUP_FILE_NAME_REGEX = "auto\\.[-\\d]+\\.xml"; //$NON-NLS-1$ private static final int DAYS_TO_KEEP_BACKUP = 7; - @Inject TasksXmlExporter xmlExporter; + @Inject @ForApplication Context context; + @Inject JobManager jobManager; + @Inject TasksXmlExporter tasksXmlExporter; @Inject Preferences preferences; - public BackupIntentService() { - super(BackupIntentService.class.getSimpleName()); + public BackupJob() { + super(BackupJob.class.getSimpleName()); } - @Override - void run() { - startBackup(this); + BackupJob(Context context, JobManager jobManager, TasksXmlExporter tasksXmlExporter, Preferences preferences) { + this(); + + this.context = context; + this.jobManager = jobManager; + this.tasksXmlExporter = tasksXmlExporter; + this.preferences = preferences; } @Override - protected String getLastRunPreference() { - return TasksXmlExporter.PREF_BACKUP_LAST_DATE; + protected void run() { + startBackup(context); } - /** - * Test hook for backup - */ - void testBackup(TasksXmlExporter xmlExporter, Preferences preferences, Context context) { - this.xmlExporter = xmlExporter; - this.preferences = preferences; - startBackup(context); + @Override + protected void scheduleNext() { + jobManager.scheduleMidnightBackup(); } - private void startBackup(Context context) { - if (context == null || context.getResources() == null) { - return; - } + void startBackup(Context context) { try { deleteOldBackups(); } catch (Exception e) { @@ -57,7 +59,7 @@ public class BackupIntentService extends MidnightIntentService { } try { - xmlExporter.exportTasks(context, TasksXmlExporter.ExportType.EXPORT_TYPE_SERVICE, null); + tasksXmlExporter.exportTasks(context, TasksXmlExporter.ExportType.EXPORT_TYPE_SERVICE, null); } catch (Exception e) { Timber.e(e, e.getMessage()); } diff --git a/src/main/java/org/tasks/jobs/Job.java b/src/main/java/org/tasks/jobs/Job.java new file mode 100644 index 000000000..5845fee22 --- /dev/null +++ b/src/main/java/org/tasks/jobs/Job.java @@ -0,0 +1,39 @@ +package org.tasks.jobs; + +import android.content.Intent; + +import org.tasks.analytics.Tracker; +import org.tasks.injection.InjectingIntentService; + +import javax.inject.Inject; + +import timber.log.Timber; + +public abstract class Job extends InjectingIntentService { + + @Inject Tracker tracker; + + public Job(String name) { + super(name); + setIntentRedelivery(true); + } + + @Override + protected void onHandleIntent(Intent intent) { + super.onHandleIntent(intent); + + Timber.d("onHandleIntent(%s)", intent); + + try { + run(); + } catch (Exception e) { + tracker.reportException(e); + } finally { + scheduleNext(); + } + } + + protected abstract void run(); + + protected abstract void scheduleNext(); +} diff --git a/src/main/java/org/tasks/jobs/JobManager.java b/src/main/java/org/tasks/jobs/JobManager.java new file mode 100644 index 000000000..988e7c102 --- /dev/null +++ b/src/main/java/org/tasks/jobs/JobManager.java @@ -0,0 +1,82 @@ +package org.tasks.jobs; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; + +import org.tasks.injection.ApplicationScope; +import org.tasks.injection.ForApplication; +import org.tasks.scheduling.AlarmManager; + +import javax.inject.Inject; + +import timber.log.Timber; + +import static org.tasks.time.DateTimeUtils.currentTimeMillis; +import static org.tasks.time.DateTimeUtils.nextMidnight; +import static org.tasks.time.DateTimeUtils.printTimestamp; + +@ApplicationScope +public class JobManager { + + private Context context; + private AlarmManager alarmManager; + + @Inject + public JobManager(@ForApplication Context context, AlarmManager alarmManager) { + this.context = context; + this.alarmManager = alarmManager; + } + + public void schedule(String tag, long time) { + Timber.d("%s: %s", tag, printTimestamp(time)); + alarmManager.wakeup(adjust(time), getPendingIntent(tag)); + } + + public void scheduleRefresh(long time) { + Timber.d("%s: %s", RefreshJob.TAG, printTimestamp(time)); + alarmManager.noWakeup(adjust(time), getPendingService(RefreshJob.class)); + } + + public void scheduleMidnightRefresh() { + long time = nextMidnight(); + Timber.d("%s: %s", MidnightRefreshJob.TAG, printTimestamp(time)); + alarmManager.noWakeup(adjust(time), getPendingService(MidnightRefreshJob.class)); + } + + public void scheduleMidnightBackup() { + long time = nextMidnight(); + Timber.d("%s: %s", BackupJob.TAG, printTimestamp(time)); + alarmManager.noWakeup(adjust(time), getPendingService(BackupJob.class)); + } + + public void cancel(String tag) { + Timber.d("CXL %s", tag); + alarmManager.cancel(getPendingIntent(tag)); + } + + private long adjust(long time) { + return Math.max(time, currentTimeMillis() + 5000); + } + + private PendingIntent getPendingIntent(String tag) { + switch (tag) { + case ReminderJob.TAG: + return getPendingBroadcast(ReminderJobBroadcast.class); + case AlarmJob.TAG: + return getPendingBroadcast(AlarmJobBroadcast.class); + case RefreshJob.TAG: + return getPendingService(RefreshJob.class); + default: + throw new RuntimeException("Unexpected tag: " + tag); + } + } + + private PendingIntent getPendingBroadcast(Class c) { + return PendingIntent.getBroadcast(context, 0, new Intent(context, c), 0); + } + + private PendingIntent getPendingService(Class c) { + return PendingIntent.getService(context, 0, new Intent(context, c), 0); + } +} diff --git a/src/main/java/org/tasks/jobs/JobQueue.java b/src/main/java/org/tasks/jobs/JobQueue.java new file mode 100644 index 000000000..a1679a160 --- /dev/null +++ b/src/main/java/org/tasks/jobs/JobQueue.java @@ -0,0 +1,107 @@ +package org.tasks.jobs; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSortedSet; +import com.google.common.collect.Ordering; +import com.google.common.collect.TreeMultimap; +import com.google.common.primitives.Longs; + +import org.tasks.preferences.Preferences; + +import java.util.List; +import java.util.NavigableSet; +import java.util.SortedSet; + +import static com.google.common.collect.Iterables.filter; +import static com.google.common.collect.Lists.newArrayList; +import static org.tasks.time.DateTimeUtils.currentTimeMillis; + +public class JobQueue { + private final TreeMultimap jobs = TreeMultimap.create(Ordering.natural(), (l, r) -> Longs.compare(l.getId(), r.getId())); + private final Preferences preferences; + private final JobManager jobManager; + private final String tag; + + public static JobQueue newReminderQueue(Preferences preferences, JobManager jobManager) { + return new JobQueue<>(preferences, jobManager, ReminderJob.TAG); + } + + public static JobQueue newAlarmQueue(Preferences preferences, JobManager jobManager) { + return new JobQueue<>(preferences, jobManager, AlarmJob.TAG); + } + + JobQueue(Preferences preferences, JobManager jobManager, String tag) { + this.preferences = preferences; + this.jobManager = jobManager; + this.tag = tag; + } + + public synchronized void add(T entry) { + boolean reschedule = jobs.isEmpty() || entry.getTime() < firstTime(); + jobs.put(entry.getTime(), entry); + if (reschedule) { + scheduleNext(true); + } + } + + public synchronized void clear() { + jobs.clear(); + jobManager.cancel(tag); + } + + public synchronized void cancel(long id) { + boolean reschedule = false; + long firstTime = firstTime(); + List existing = newArrayList(filter(jobs.values(), r -> r.getId() == id)); + for (T entry : existing) { + reschedule |= entry.getTime() == firstTime; + jobs.remove(entry.getTime(), entry); + } + if (reschedule) { + scheduleNext(true); + } + } + + public synchronized List getOverdueJobs() { + List result = newArrayList(); + for (Long key : jobs.keySet().headSet(currentTimeMillis() + 1)) { + result.addAll(jobs.get(key)); + } + return result; + } + + public synchronized boolean remove(T entry) { + return jobs.remove(entry.getTime(), entry); + } + + public synchronized void scheduleNext() { + scheduleNext(false); + } + + private void scheduleNext(boolean cancelCurrent) { + if (jobs.isEmpty()) { + if (cancelCurrent) { + jobManager.cancel(tag); + } + } else { + jobManager.schedule(tag, nextScheduledTime()); + } + } + + private long firstTime() { + return jobs.isEmpty() ? 0 : jobs.asMap().firstKey(); + } + + long nextScheduledTime() { + long next = firstTime(); + return next > 0 ? preferences.adjustForQuietHours(next) : 0; + } + + int size() { + return jobs.size(); + } + + List getJobs() { + return ImmutableList.copyOf(jobs.values()); + } +} diff --git a/src/main/java/org/tasks/jobs/JobQueueEntry.java b/src/main/java/org/tasks/jobs/JobQueueEntry.java new file mode 100644 index 000000000..a52845d05 --- /dev/null +++ b/src/main/java/org/tasks/jobs/JobQueueEntry.java @@ -0,0 +1,7 @@ +package org.tasks.jobs; + +public interface JobQueueEntry { + long getId(); + + long getTime(); +} diff --git a/src/main/java/org/tasks/jobs/MidnightJob.java b/src/main/java/org/tasks/jobs/MidnightJob.java new file mode 100644 index 000000000..78d82071c --- /dev/null +++ b/src/main/java/org/tasks/jobs/MidnightJob.java @@ -0,0 +1,7 @@ +package org.tasks.jobs; + +public abstract class MidnightJob extends Job { + public MidnightJob(String name) { + super(name); + } +} diff --git a/src/main/java/org/tasks/jobs/MidnightRefreshJob.java b/src/main/java/org/tasks/jobs/MidnightRefreshJob.java new file mode 100644 index 000000000..f5551b2a0 --- /dev/null +++ b/src/main/java/org/tasks/jobs/MidnightRefreshJob.java @@ -0,0 +1,33 @@ +package org.tasks.jobs; + +import org.tasks.Broadcaster; +import org.tasks.injection.IntentServiceComponent; + +import javax.inject.Inject; + +public class MidnightRefreshJob extends MidnightJob { + + public static final String TAG = "job_midnight_refresh"; + + @Inject Broadcaster broadcaster; + @Inject JobManager jobManager; + + public MidnightRefreshJob() { + super(MidnightRefreshJob.class.getSimpleName()); + } + + @Override + protected void run() { + broadcaster.refresh(); + } + + @Override + protected void scheduleNext() { + jobManager.scheduleMidnightRefresh(); + } + + @Override + protected void inject(IntentServiceComponent component) { + component.inject(this); + } +} diff --git a/src/main/java/org/tasks/jobs/RefreshJob.java b/src/main/java/org/tasks/jobs/RefreshJob.java new file mode 100644 index 000000000..d4b07091e --- /dev/null +++ b/src/main/java/org/tasks/jobs/RefreshJob.java @@ -0,0 +1,34 @@ +package org.tasks.jobs; + +import org.tasks.Broadcaster; +import org.tasks.injection.IntentServiceComponent; +import org.tasks.scheduling.RefreshScheduler; + +import javax.inject.Inject; + +public class RefreshJob extends Job { + + public static final String TAG = "job_refresh"; + + @Inject RefreshScheduler refreshScheduler; + @Inject Broadcaster broadcaster; + + public RefreshJob() { + super(RefreshJob.class.getSimpleName()); + } + + @Override + protected void inject(IntentServiceComponent component) { + component.inject(this); + } + + @Override + protected void run() { + broadcaster.refresh(); + } + + @Override + protected void scheduleNext() { + refreshScheduler.scheduleNext(); + } +} diff --git a/src/main/java/org/tasks/jobs/Reminder.java b/src/main/java/org/tasks/jobs/Reminder.java new file mode 100644 index 000000000..06bc1f54b --- /dev/null +++ b/src/main/java/org/tasks/jobs/Reminder.java @@ -0,0 +1,57 @@ +package org.tasks.jobs; + +public class Reminder implements JobQueueEntry { + private final long taskId; + private final long time; + private final int type; + + public Reminder(long taskId, long time, int type) { + this.taskId = taskId; + this.time = time; + this.type = type; + } + + @Override + public long getId() { + return taskId; + } + + @Override + public long getTime() { + return time; + } + + public int getType() { + return type; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Reminder reminder = (Reminder) o; + + if (taskId != reminder.taskId) return false; + if (time != reminder.time) return false; + return type == reminder.type; + + } + + @Override + public int hashCode() { + int result = (int) (taskId ^ (taskId >>> 32)); + result = 31 * result + (int) (time ^ (time >>> 32)); + result = 31 * result + type; + return result; + } + + @Override + public String toString() { + return "Reminder{" + + "taskId=" + taskId + + ", time=" + time + + ", type=" + type + + '}'; + } +} diff --git a/src/main/java/org/tasks/jobs/ReminderJob.java b/src/main/java/org/tasks/jobs/ReminderJob.java new file mode 100644 index 000000000..43767a79e --- /dev/null +++ b/src/main/java/org/tasks/jobs/ReminderJob.java @@ -0,0 +1,49 @@ +package org.tasks.jobs; + +import android.content.Intent; + +import com.todoroo.astrid.reminders.ReminderService; + +import org.tasks.Notifier; +import org.tasks.injection.IntentServiceComponent; +import org.tasks.preferences.Preferences; + +import javax.inject.Inject; + +public class ReminderJob extends WakefulJob { + + public static final String TAG = "job_reminder"; + + @Inject Preferences preferences; + @Inject ReminderService reminderService; + @Inject Notifier notifier; + + public ReminderJob() { + super(ReminderJob.class.getSimpleName()); + } + + @Override + protected void inject(IntentServiceComponent component) { + component.inject(this); + } + + @Override + protected void run() { + if (!preferences.isCurrentlyQuietHours()) { + for (Reminder reminder : reminderService.getPastReminders()) { + notifier.triggerTaskNotification(reminder.getId(), reminder.getType()); + reminderService.remove(reminder); + } + } + } + + @Override + protected void scheduleNext() { + reminderService.scheduleNextJob(); + } + + @Override + protected void completeWakefulIntent(Intent intent) { + ReminderJobBroadcast.completeWakefulIntent(intent); + } +} diff --git a/src/main/java/org/tasks/jobs/ReminderJobBroadcast.java b/src/main/java/org/tasks/jobs/ReminderJobBroadcast.java new file mode 100644 index 000000000..ce75fa74b --- /dev/null +++ b/src/main/java/org/tasks/jobs/ReminderJobBroadcast.java @@ -0,0 +1,12 @@ +package org.tasks.jobs; + +import android.content.Context; +import android.content.Intent; +import android.support.v4.content.WakefulBroadcastReceiver; + +public class ReminderJobBroadcast extends WakefulBroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + startWakefulService(context, new Intent(context, ReminderJob.class)); + } +} diff --git a/src/main/java/org/tasks/jobs/WakefulJob.java b/src/main/java/org/tasks/jobs/WakefulJob.java new file mode 100644 index 000000000..b2b486b60 --- /dev/null +++ b/src/main/java/org/tasks/jobs/WakefulJob.java @@ -0,0 +1,18 @@ +package org.tasks.jobs; + +import android.content.Intent; + +public abstract class WakefulJob extends Job { + + public WakefulJob(String name) { + super(name); + } + + @Override + protected void onHandleIntent(Intent intent) { + super.onHandleIntent(intent); + completeWakefulIntent(intent); + } + + protected abstract void completeWakefulIntent(Intent intent); +} diff --git a/src/main/java/org/tasks/locale/Locale.java b/src/main/java/org/tasks/locale/Locale.java index a1b26e0b7..d8e1918c3 100644 --- a/src/main/java/org/tasks/locale/Locale.java +++ b/src/main/java/org/tasks/locale/Locale.java @@ -28,11 +28,16 @@ public class Locale { public static Locale getInstance(Context context) { if (INSTANCE == null) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - String language = prefs.getString(context.getString(R.string.p_language), null); - int directionOverride = Integer.parseInt(prefs.getString(context.getString(R.string.p_layout_direction), "-1")); - INSTANCE = new Locale(DEFAULT.getLocale(), language, directionOverride); - java.util.Locale.setDefault(INSTANCE.getLocale()); + synchronized (DEFAULT) { + if (INSTANCE == null) { + Context applicationContext = context.getApplicationContext(); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext); + String language = prefs.getString(applicationContext.getString(R.string.p_language), null); + int directionOverride = Integer.parseInt(prefs.getString(applicationContext.getString(R.string.p_layout_direction), "-1")); + INSTANCE = new Locale(DEFAULT.getLocale(), language, directionOverride); + java.util.Locale.setDefault(INSTANCE.getLocale()); + } + } } return getInstance(); diff --git a/src/main/java/org/tasks/preferences/BasicPreferences.java b/src/main/java/org/tasks/preferences/BasicPreferences.java index 39fd4b821..42ad7ff09 100644 --- a/src/main/java/org/tasks/preferences/BasicPreferences.java +++ b/src/main/java/org/tasks/preferences/BasicPreferences.java @@ -137,7 +137,7 @@ public class BasicPreferences extends InjectingPreferenceActivity implements }); findPreference(R.string.TLA_menu_donate).setOnPreferenceClickListener(preference -> { - if (BuildConfig.FLAVOR_store.equals("googleplay")) { + if (BuildConfig.FLAVOR.equals("googleplay")) { newDonationDialog().show(getFragmentManager(), FRAG_TAG_DONATION); } else { startActivity(new Intent(Intent.ACTION_VIEW).setData(Uri.parse("http://tasks.org/donate"))); @@ -214,7 +214,7 @@ public class BasicPreferences extends InjectingPreferenceActivity implements requires(R.string.get_plugins, atLeastJellybeanMR1(), R.string.p_purchased_dashclock); requires(R.string.settings_localization, atLeastJellybeanMR1(), R.string.p_language, R.string.p_layout_direction); - if (!BuildConfig.FLAVOR_store.equals("googleplay")) { + if (!BuildConfig.FLAVOR.equals("googleplay")) { requires(R.string.settings_general, false, R.string.synchronization); requires(R.string.privacy, false, R.string.p_collect_statistics); } diff --git a/src/main/java/org/tasks/preferences/Device.java b/src/main/java/org/tasks/preferences/Device.java index faf06b012..305259d3b 100644 --- a/src/main/java/org/tasks/preferences/Device.java +++ b/src/main/java/org/tasks/preferences/Device.java @@ -29,10 +29,6 @@ public class Device { this.locale = locale; } - public boolean isBaneOfMyExistence() { - return Build.MANUFACTURER.equalsIgnoreCase("samsung"); - } - public boolean hasCamera() { return context.getPackageManager().queryIntentActivities(new Intent(MediaStore.ACTION_IMAGE_CAPTURE), 0).size() > 0; } diff --git a/src/main/java/org/tasks/preferences/MiscellaneousPreferences.java b/src/main/java/org/tasks/preferences/MiscellaneousPreferences.java index eaa68d5d2..eff8a3ac2 100644 --- a/src/main/java/org/tasks/preferences/MiscellaneousPreferences.java +++ b/src/main/java/org/tasks/preferences/MiscellaneousPreferences.java @@ -1,7 +1,6 @@ package org.tasks.preferences; import android.content.Intent; -import android.content.pm.PackageManager; import android.os.Bundle; import android.preference.CheckBoxPreference; import android.speech.tts.TextToSpeech; @@ -21,6 +20,8 @@ import javax.inject.Inject; import timber.log.Timber; +import static org.tasks.PermissionUtil.verifyPermissions; + public class MiscellaneousPreferences extends InjectingPreferenceActivity { private static final int REQUEST_CODE_FILES_DIR = 2; @@ -143,7 +144,7 @@ public class MiscellaneousPreferences extends InjectingPreferenceActivity { @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == PermissionRequestor.REQUEST_CALENDAR) { - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + if (verifyPermissions(grantResults)) { calendarReminderPreference.setChecked(true); } } else { diff --git a/src/main/java/org/tasks/preferences/Preferences.java b/src/main/java/org/tasks/preferences/Preferences.java index b9974d6f2..3c531b362 100644 --- a/src/main/java/org/tasks/preferences/Preferences.java +++ b/src/main/java/org/tasks/preferences/Preferences.java @@ -28,7 +28,6 @@ import timber.log.Timber; import static android.content.SharedPreferences.Editor; import static com.todoroo.andlib.utility.AndroidUtilities.atLeastJellybean; -import static com.todoroo.andlib.utility.AndroidUtilities.atLeastMarshmallow; public class Preferences { @@ -57,10 +56,52 @@ public class Preferences { return getBoolean(R.string.p_back_button_saves_task, false); } + public boolean isCurrentlyQuietHours() { + if (quietHoursEnabled()) { + DateTime dateTime = new DateTime(); + DateTime start = dateTime.withMillisOfDay(getQuietHoursStart()); + DateTime end = dateTime.withMillisOfDay(getQuietHoursEnd()); + if (start.isAfter(end)) { + return dateTime.isBefore(end) || dateTime.isAfter(start); + } else { + return dateTime.isAfter(start) && dateTime.isBefore(end); + } + } + return false; + } + + public long adjustForQuietHours(long time) { + if (quietHoursEnabled()) { + DateTime dateTime = new DateTime(time); + DateTime start = dateTime.withMillisOfDay(getQuietHoursStart()); + DateTime end = dateTime.withMillisOfDay(getQuietHoursEnd()); + if (start.isAfter(end)) { + if (dateTime.isBefore(end)) { + return end.getMillis(); + } else if (dateTime.isAfter(start)) { + return end.plusDays(1).getMillis(); + } + } else { + if (dateTime.isAfter(start) && dateTime.isBefore(end)) { + return end.getMillis(); + } + } + } + return time; + } + public boolean quietHoursEnabled() { return getBoolean(R.string.p_rmd_enable_quiet, false); } + public int getQuietHoursStart() { + return getMillisPerDayPref(R.string.p_rmd_quietStart, R.integer.default_quiet_hours_start); + } + + public int getQuietHoursEnd() { + return getMillisPerDayPref(R.string.p_rmd_quietEnd, R.integer.default_quiet_hours_end); + } + public int getDateShortcutMorning() { return getMillisPerDayPref(R.string.p_date_shortcut_morning, R.integer.default_morning); } @@ -78,9 +119,11 @@ public class Preferences { } private int getMillisPerDayPref(int resId, int defResId) { - int defaultValue = context.getResources().getInteger(defResId); - int setting = getInt(resId, defaultValue); - return setting < 0 || setting > DateTime.MAX_MILLIS_PER_DAY ? defaultValue : setting; + int setting = getInt(resId, -1); + if (setting < 0 || setting > DateTime.MAX_MILLIS_PER_DAY) { + return context.getResources().getInteger(defResId); + } + return setting; } public boolean isDefaultCalendarSet() { @@ -100,10 +143,6 @@ public class Preferences { return getStringValue(R.string.gcal_p_default); } - public boolean isDozeNotificationEnabled() { - return atLeastMarshmallow() && getBoolean(R.string.p_doze_notifications, false); - } - public int getFirstDayOfWeek() { int firstDayOfWeek = getIntegerFromString(R.string.p_start_of_week, 0); return firstDayOfWeek < 1 || firstDayOfWeek > 7 ? 0 : firstDayOfWeek; diff --git a/src/main/java/org/tasks/receivers/RefreshReceiver.java b/src/main/java/org/tasks/receivers/RefreshReceiver.java deleted file mode 100644 index 999326304..000000000 --- a/src/main/java/org/tasks/receivers/RefreshReceiver.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.tasks.receivers; - -import android.content.Context; -import android.content.Intent; - -import org.tasks.Broadcaster; -import org.tasks.injection.BroadcastComponent; -import org.tasks.injection.InjectingBroadcastReceiver; - -import javax.inject.Inject; - -import timber.log.Timber; - -public class RefreshReceiver extends InjectingBroadcastReceiver { - - @Inject Broadcaster broadcaster; - - @Override - public void onReceive(Context context, Intent intent) { - super.onReceive(context, intent); - - Timber.d("onReceive(context, %s)", intent); - - broadcaster.refresh(); - } - - @Override - protected void inject(BroadcastComponent component) { - component.inject(this); - } -} diff --git a/src/main/java/org/tasks/receivers/TaskNotificationReceiver.java b/src/main/java/org/tasks/receivers/TaskNotificationReceiver.java deleted file mode 100644 index 7ad1e679b..000000000 --- a/src/main/java/org/tasks/receivers/TaskNotificationReceiver.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.tasks.receivers; - -import android.content.Context; -import android.content.Intent; - -import org.tasks.Notifier; -import org.tasks.injection.BroadcastComponent; -import org.tasks.injection.InjectingBroadcastReceiver; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import javax.inject.Inject; - -public class TaskNotificationReceiver extends InjectingBroadcastReceiver { - - private static final ExecutorService executorService = Executors.newSingleThreadExecutor(); - - public static final String ID_KEY = "id"; //$NON-NLS-1$ - public static final String EXTRAS_TYPE = "type"; //$NON-NLS-1$ - - @Inject Notifier notifier; - - @Override - public void onReceive(Context context, final Intent intent) { - super.onReceive(context, intent); - - executorService.execute(() -> notifier.triggerTaskNotification( - intent.getLongExtra(ID_KEY, 0), - intent.getIntExtra(EXTRAS_TYPE, (byte) 0))); - } - - @Override - protected void inject(BroadcastComponent component) { - component.inject(this); - } -} diff --git a/src/main/java/org/tasks/reminders/Random.java b/src/main/java/org/tasks/reminders/Random.java new file mode 100644 index 000000000..bb63a4c09 --- /dev/null +++ b/src/main/java/org/tasks/reminders/Random.java @@ -0,0 +1,10 @@ +package org.tasks.reminders; + +public class Random { + + private static final java.util.Random random = new java.util.Random(); + + public float nextFloat() { + return random.nextFloat(); + } +} diff --git a/src/main/java/org/tasks/reminders/SnoozeDialog.java b/src/main/java/org/tasks/reminders/SnoozeDialog.java index 6af095035..88cb75b83 100644 --- a/src/main/java/org/tasks/reminders/SnoozeDialog.java +++ b/src/main/java/org/tasks/reminders/SnoozeDialog.java @@ -97,7 +97,8 @@ public class SnoozeDialog extends InjectingDialogFragment { List snoozeOptions = new ArrayList<>(); - snoozeOptions.add(new SnoozeOption(R.string.date_shortcut_hour, now.plusHours(1))); + DateTime oneHour = now.plusHours(1).withSecondOfMinute(0).withMillisOfSecond(0); + snoozeOptions.add(new SnoozeOption(R.string.date_shortcut_hour, oneHour)); if (morning.isAfter(hourCutoff)) { snoozeOptions.add(new SnoozeOption(R.string.date_shortcut_morning, morning)); diff --git a/src/main/java/org/tasks/scheduling/AlarmManager.java b/src/main/java/org/tasks/scheduling/AlarmManager.java index 356929442..37c855f86 100644 --- a/src/main/java/org/tasks/scheduling/AlarmManager.java +++ b/src/main/java/org/tasks/scheduling/AlarmManager.java @@ -4,23 +4,19 @@ import android.annotation.SuppressLint; import android.app.PendingIntent; import android.content.Context; -import org.tasks.R; import org.tasks.injection.ForApplication; -import org.tasks.preferences.Preferences; -import org.tasks.time.DateTime; import javax.inject.Inject; import static com.todoroo.andlib.utility.AndroidUtilities.atLeastKitKat; +import static com.todoroo.andlib.utility.AndroidUtilities.atLeastMarshmallow; public class AlarmManager { private final android.app.AlarmManager alarmManager; - private final Preferences preferences; @Inject - public AlarmManager(@ForApplication Context context, Preferences preferences) { - this.preferences = preferences; + public AlarmManager(@ForApplication Context context) { alarmManager = (android.app.AlarmManager) context.getSystemService(Context.ALARM_SERVICE); } @@ -28,13 +24,9 @@ public class AlarmManager { alarmManager.cancel(pendingIntent); } - public void wakeupAdjustingForQuietHours(long time, PendingIntent pendingIntent) { - wakeup(adjustForQuietHours(time), pendingIntent); - } - @SuppressLint("NewApi") public void wakeup(long time, PendingIntent pendingIntent) { - if (preferences.isDozeNotificationEnabled()) { + if (atLeastMarshmallow()) { alarmManager.setExactAndAllowWhileIdle(android.app.AlarmManager.RTC_WAKEUP, time, pendingIntent); } else if (atLeastKitKat()) { alarmManager.setExact(android.app.AlarmManager.RTC_WAKEUP, time, pendingIntent); @@ -45,7 +37,7 @@ public class AlarmManager { @SuppressLint("NewApi") public void noWakeup(long time, PendingIntent pendingIntent) { - if (preferences.isDozeNotificationEnabled()) { + if (atLeastMarshmallow()) { alarmManager.setExactAndAllowWhileIdle(android.app.AlarmManager.RTC, time, pendingIntent); } else if (atLeastKitKat()) { alarmManager.setExact(android.app.AlarmManager.RTC, time, pendingIntent); @@ -53,24 +45,4 @@ public class AlarmManager { alarmManager.set(android.app.AlarmManager.RTC, time, pendingIntent); } } - - long adjustForQuietHours(long time) { - if (preferences.quietHoursEnabled()) { - DateTime dateTime = new DateTime(time); - DateTime start = dateTime.withMillisOfDay(preferences.getInt(R.string.p_rmd_quietStart)); - DateTime end = dateTime.withMillisOfDay(preferences.getInt(R.string.p_rmd_quietEnd)); - if (start.isAfter(end)) { - if (dateTime.isBefore(end)) { - return end.getMillis(); - } else if (dateTime.isAfter(start)) { - return end.plusDays(1).getMillis(); - } - } else { - if (dateTime.isAfter(start) && dateTime.isBefore(end)) { - return end.getMillis(); - } - } - } - return time; - } } diff --git a/src/main/java/org/tasks/scheduling/BackgroundScheduler.java b/src/main/java/org/tasks/scheduling/BackgroundScheduler.java index 7d7cee9c1..0c00efa1c 100644 --- a/src/main/java/org/tasks/scheduling/BackgroundScheduler.java +++ b/src/main/java/org/tasks/scheduling/BackgroundScheduler.java @@ -17,20 +17,11 @@ public class BackgroundScheduler { public void scheduleEverything() { context.startService(new Intent(context, GeofenceSchedulingIntentService.class)); - context.startService(new Intent(context, ReminderSchedulerIntentService.class)); - scheduleBackupService(); - scheduleMidnightRefresh(); + context.startService(new Intent(context, SchedulerIntentService.class)); + context.startService(new Intent(context, NotificationSchedulerIntentService.class)); scheduleCalendarNotifications(); } - public void scheduleBackupService() { - context.startService(new Intent(context, BackupIntentService.class)); - } - - public void scheduleMidnightRefresh() { - context.startService(new Intent(context, RefreshSchedulerIntentService.class)); - } - public void scheduleCalendarNotifications() { context.startService(new Intent(context, CalendarNotificationIntentService.class)); } diff --git a/src/main/java/org/tasks/scheduling/MidnightIntentService.java b/src/main/java/org/tasks/scheduling/MidnightIntentService.java deleted file mode 100644 index 145c852d0..000000000 --- a/src/main/java/org/tasks/scheduling/MidnightIntentService.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.tasks.scheduling; - -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; - -import org.tasks.injection.InjectingIntentService; -import org.tasks.preferences.Preferences; - -import timber.log.Timber; - -import static com.google.common.base.Strings.isNullOrEmpty; -import static java.util.concurrent.TimeUnit.SECONDS; -import static org.tasks.time.DateTimeUtils.currentTimeMillis; -import static org.tasks.time.DateTimeUtils.nextMidnight; -import static org.tasks.time.DateTimeUtils.printTimestamp; - -public abstract class MidnightIntentService extends InjectingIntentService { - - private static final long PADDING = SECONDS.toMillis(1); - - private final String name; - - MidnightIntentService(String name) { - super(name); - this.name = name; - } - - @Override - protected void onHandleIntent(Intent intent) { - super.onHandleIntent(intent); - - Context context = getApplicationContext(); - Preferences preferences = new Preferences(context); - AlarmManager alarmManager = new AlarmManager(context, preferences); - - long lastRun = preferences.getLong(getLastRunPreference(), 0); - long nextRun = nextMidnight(lastRun); - long now = currentTimeMillis(); - - if (nextRun <= now) { - nextRun = nextMidnight(now); - Timber.d("%s running now [nextRun=%s]", name, printTimestamp(nextRun)); - if (!isNullOrEmpty(getLastRunPreference())) { - preferences.setLong(getLastRunPreference(), now); - } - run(); - } else { - Timber.d("%s will run at %s [lastRun=%s]", name, printTimestamp(nextRun), printTimestamp(lastRun)); - } - - PendingIntent pendingIntent = PendingIntent.getService(this, 0, new Intent(this, this.getClass()), PendingIntent.FLAG_UPDATE_CURRENT); - alarmManager.noWakeup(nextRun + PADDING, pendingIntent); - } - - abstract void run(); - - String getLastRunPreference() { - return null; - } -} diff --git a/src/main/java/org/tasks/scheduling/ReminderSchedulerIntentService.java b/src/main/java/org/tasks/scheduling/NotificationSchedulerIntentService.java similarity index 76% rename from src/main/java/org/tasks/scheduling/ReminderSchedulerIntentService.java rename to src/main/java/org/tasks/scheduling/NotificationSchedulerIntentService.java index f7c5b5c36..ed2bb13d0 100644 --- a/src/main/java/org/tasks/scheduling/ReminderSchedulerIntentService.java +++ b/src/main/java/org/tasks/scheduling/NotificationSchedulerIntentService.java @@ -13,14 +13,14 @@ import javax.inject.Inject; import timber.log.Timber; -public class ReminderSchedulerIntentService extends InjectingIntentService { +public class NotificationSchedulerIntentService extends InjectingIntentService { @Inject AlarmService alarmService; @Inject ReminderService reminderService; @Inject TaskDao taskDao; - public ReminderSchedulerIntentService() { - super(ReminderSchedulerIntentService.class.getSimpleName()); + public NotificationSchedulerIntentService() { + super(NotificationSchedulerIntentService.class.getSimpleName()); } @Override @@ -29,6 +29,9 @@ public class ReminderSchedulerIntentService extends InjectingIntentService { Timber.d("onHandleIntent(%s)", intent); + reminderService.clear(); + alarmService.clear(); + reminderService.scheduleAllAlarms(taskDao); alarmService.scheduleAllAlarms(); } diff --git a/src/main/java/org/tasks/scheduling/RefreshScheduler.java b/src/main/java/org/tasks/scheduling/RefreshScheduler.java index 096ee09ec..894de6aaa 100644 --- a/src/main/java/org/tasks/scheduling/RefreshScheduler.java +++ b/src/main/java/org/tasks/scheduling/RefreshScheduler.java @@ -1,33 +1,34 @@ package org.tasks.scheduling; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; - import com.todoroo.astrid.data.Task; -import org.tasks.injection.ForApplication; -import org.tasks.receivers.RefreshReceiver; +import org.tasks.injection.ApplicationScope; +import org.tasks.jobs.JobManager; +import org.tasks.jobs.RefreshJob; -import javax.inject.Inject; +import java.util.SortedSet; +import java.util.TreeSet; -import timber.log.Timber; +import javax.inject.Inject; -import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; +import static com.google.common.collect.Lists.newArrayList; import static com.todoroo.andlib.utility.DateUtilities.ONE_MINUTE; import static org.tasks.time.DateTimeUtils.currentTimeMillis; -import static org.tasks.time.DateTimeUtils.nextMidnight; -import static org.tasks.time.DateTimeUtils.printTimestamp; +@ApplicationScope public class RefreshScheduler { - private final Context context; - private final AlarmManager alarmManager; + private final JobManager jobManager; + private final SortedSet jobs = new TreeSet<>(); @Inject - public RefreshScheduler(@ForApplication Context context, AlarmManager alarmManager) { - this.context = context; - this.alarmManager = alarmManager; + public RefreshScheduler(JobManager jobManager) { + this.jobManager = jobManager; + } + + public void clear() { + jobs.clear(); + jobManager.cancel(RefreshJob.TAG); } public void scheduleRefresh(Task task) { @@ -43,13 +44,26 @@ public class RefreshScheduler { private void scheduleRefresh(Long refreshTime) { long now = currentTimeMillis(); - if (now < refreshTime && refreshTime < nextMidnight(now)) { + if (now < refreshTime) { refreshTime += 1000; // this is ghetto - Timber.d("Scheduling refresh at %s", printTimestamp(refreshTime)); - Intent intent = new Intent(context, RefreshReceiver.class); - intent.setAction(Long.toString(refreshTime)); - PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, FLAG_UPDATE_CURRENT); - alarmManager.noWakeup(refreshTime, pendingIntent); + schedule(refreshTime); + } + } + + private void schedule(long timestamp) { + SortedSet upcoming = jobs.tailSet(currentTimeMillis()); + boolean reschedule = upcoming.isEmpty() || timestamp < upcoming.first(); + jobs.add(timestamp); + if (reschedule) { + scheduleNext(); + } + } + + public void scheduleNext() { + long now = currentTimeMillis(); + jobs.removeAll(newArrayList(jobs.headSet(now + 1))); + if (!jobs.isEmpty()) { + jobManager.scheduleRefresh(jobs.first()); } } } diff --git a/src/main/java/org/tasks/scheduling/RefreshSchedulerIntentService.java b/src/main/java/org/tasks/scheduling/RefreshSchedulerIntentService.java deleted file mode 100644 index a90d90e32..000000000 --- a/src/main/java/org/tasks/scheduling/RefreshSchedulerIntentService.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.tasks.scheduling; - -import com.todoroo.andlib.sql.Criterion; -import com.todoroo.astrid.dao.TaskDao; -import com.todoroo.astrid.data.Task; - -import org.tasks.Broadcaster; -import org.tasks.injection.IntentServiceComponent; - -import javax.inject.Inject; - -import static org.tasks.time.DateTimeUtils.currentTimeMillis; -import static org.tasks.time.DateTimeUtils.nextMidnight; - -public class RefreshSchedulerIntentService extends MidnightIntentService { - - @Inject Broadcaster broadcaster; - @Inject RefreshScheduler refreshScheduler; - @Inject TaskDao taskDao; - - public RefreshSchedulerIntentService() { - super(RefreshSchedulerIntentService.class.getSimpleName()); - } - - @Override - void run() { - scheduleApplicationRefreshes(); - broadcaster.refresh(); - } - - public void scheduleApplicationRefreshes() { - long now = currentTimeMillis(); - long midnight = nextMidnight(now); - Criterion criterion = Criterion.or( - Criterion.and(Task.HIDE_UNTIL.gt(now), Task.HIDE_UNTIL.lt(midnight)), - Criterion.and(Task.DUE_DATE.gt(now), Task.DUE_DATE.lt(midnight))); - taskDao.selectActive(criterion, refreshScheduler::scheduleRefresh); - } - - @Override - protected void inject(IntentServiceComponent component) { - component.inject(this); - } -} diff --git a/src/main/java/org/tasks/scheduling/SchedulerIntentService.java b/src/main/java/org/tasks/scheduling/SchedulerIntentService.java new file mode 100644 index 000000000..e55b5671c --- /dev/null +++ b/src/main/java/org/tasks/scheduling/SchedulerIntentService.java @@ -0,0 +1,49 @@ +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.InjectingIntentService; +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 InjectingIntentService { + + @Inject TaskDao taskDao; + @Inject JobManager jobManager; + @Inject RefreshScheduler refreshScheduler; + + public SchedulerIntentService() { + super(SchedulerIntentService.class.getSimpleName()); + } + + @Override + protected void onHandleIntent(Intent intent) { + super.onHandleIntent(intent); + + Timber.d("onHandleIntent(%s)", intent); + + jobManager.scheduleMidnightBackup(); + jobManager.scheduleMidnightRefresh(); + + refreshScheduler.clear(); + long now = currentTimeMillis(); + taskDao.selectActive( + Criterion.or(Task.HIDE_UNTIL.gt(now), Task.DUE_DATE.gt(now)), + refreshScheduler::scheduleRefresh); + } + + @Override + protected void inject(IntentServiceComponent component) { + component.inject(this); + } +} diff --git a/src/main/java/org/tasks/time/DateTime.java b/src/main/java/org/tasks/time/DateTime.java index 6d62b4ebe..74e5c0829 100644 --- a/src/main/java/org/tasks/time/DateTime.java +++ b/src/main/java/org/tasks/time/DateTime.java @@ -189,6 +189,10 @@ public class DateTime { return subtract(Calendar.DATE, days); } + public DateTime minusHours(int hours) { + return subtract(Calendar.HOUR, hours); + } + public DateTime minusMinutes(int minutes) { return subtract(Calendar.MINUTE, minutes); } diff --git a/src/main/java/org/tasks/time/DateTimeUtils.java b/src/main/java/org/tasks/time/DateTimeUtils.java index bcf23bcb4..91ea5f2c5 100644 --- a/src/main/java/org/tasks/time/DateTimeUtils.java +++ b/src/main/java/org/tasks/time/DateTimeUtils.java @@ -23,6 +23,10 @@ public class DateTimeUtils { MILLIS_PROVIDER = SYSTEM_MILLIS_PROVIDER; } + public static long nextMidnight() { + return nextMidnight(currentTimeMillis()); + } + public static long nextMidnight(long timestamp) { return newDateTime(timestamp).startOfDay().plusDays(1).getMillis(); } diff --git a/src/main/java/org/tasks/ui/CalendarControlSet.java b/src/main/java/org/tasks/ui/CalendarControlSet.java index b6d42d814..498c18fd7 100644 --- a/src/main/java/org/tasks/ui/CalendarControlSet.java +++ b/src/main/java/org/tasks/ui/CalendarControlSet.java @@ -5,7 +5,6 @@ import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; @@ -44,6 +43,7 @@ import timber.log.Timber; import static android.support.v4.content.ContextCompat.getColor; import static com.google.common.base.Strings.isNullOrEmpty; +import static org.tasks.PermissionUtil.verifyPermissions; public class CalendarControlSet extends TaskEditControlFragment { @@ -289,11 +289,11 @@ public class CalendarControlSet extends TaskEditControlFragment { @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == REQUEST_CODE_OPEN_EVENT) { - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + if (verifyPermissions(grantResults)) { openCalendarEvent(); } } else if (requestCode == REQUEST_CODE_CLEAR_EVENT) { - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + if (verifyPermissions(grantResults)) { clear(); } } else { diff --git a/src/main/java/org/tasks/ui/DeadlineControlSet.java b/src/main/java/org/tasks/ui/DeadlineControlSet.java index 1b08d3888..2fa990176 100644 --- a/src/main/java/org/tasks/ui/DeadlineControlSet.java +++ b/src/main/java/org/tasks/ui/DeadlineControlSet.java @@ -17,6 +17,7 @@ import android.widget.ArrayAdapter; import android.widget.Spinner; import android.widget.TextView; +import com.todoroo.andlib.utility.AndroidUtilities; import com.todoroo.andlib.utility.DateUtilities; import com.todoroo.astrid.data.Task; @@ -37,6 +38,7 @@ import javax.inject.Inject; import butterknife.BindView; import butterknife.OnClick; import butterknife.OnItemSelected; +import butterknife.OnTouch; import static android.support.v4.content.ContextCompat.getColor; import static com.google.common.collect.Lists.newArrayList; @@ -196,6 +198,12 @@ public class DeadlineControlSet extends TaskEditControlFragment { refreshDisplayView(); } + @OnTouch({R.id.due_date, R.id.due_time}) + boolean onSpinnersTouched() { + AndroidUtilities.hideKeyboard(getActivity()); + return false; + } + @OnItemSelected(R.id.due_date) void onDateSelected(int position) { DateTime today = newDateTime().startOfDay(); diff --git a/src/main/java/org/tasks/ui/TimePreference.java b/src/main/java/org/tasks/ui/TimePreference.java index b82b33269..df190ebd1 100644 --- a/src/main/java/org/tasks/ui/TimePreference.java +++ b/src/main/java/org/tasks/ui/TimePreference.java @@ -27,7 +27,7 @@ public class TimePreference extends Preference { @Override protected Object onGetDefaultValue(TypedArray a, int index) { - return a.getString(index); + return a.getInteger(index, -1); } @Override @@ -36,7 +36,7 @@ public class TimePreference extends Preference { int noon = new DateTime().startOfDay().withHourOfDay(12).getMillisOfDay(); millisOfDay = getPersistedInt(noon); } else { - millisOfDay = Integer.parseInt((String) defaultValue); + millisOfDay = (Integer) defaultValue; } setMillisOfDay(millisOfDay); diff --git a/src/main/res/values-ar/strings.xml b/src/main/res/values-ar/strings.xml index be12291ab..ff35a9051 100644 --- a/src/main/res/values-ar/strings.xml +++ b/src/main/res/values-ar/strings.xml @@ -185,19 +185,9 @@ عدد الإهتزازات إمتداد الهزة التوقف أثناء الهزات - وقف نمط دوز من أجل التنبيهات - أندرويد سيؤخر التنبيهات عندما يكون الهاتف على نمط دوز تاسكس هو مشروع مفتوح المصدر مدموع من طرف مطور واحد. بعض الخيارات متوفرة عن الطريق الدفع من داخل التطبيق من أجل دعم التطوير التعتيم أبيض - تعطيل - كل ساعة - كل ثلاث ساعات - كل ست ساعات - كل 12 ساعة - كل يوم - كل ثلاث أيام - كل أسبوع المزامنة التلقائية معطلة الآن من طرف أندرويد تخصيص اللغة و الجهة إتجاه التنسيق diff --git a/src/main/res/values-bg-rBG/strings.xml b/src/main/res/values-bg-rBG/strings.xml index 3b57f91d8..09ad03a40 100644 --- a/src/main/res/values-bg-rBG/strings.xml +++ b/src/main/res/values-bg-rBG/strings.xml @@ -266,6 +266,7 @@ Синхронизация Активирано Размер на шрифта + Разстояние между редовете Настройване на екрана за редактиране Source код Подпомагане с преводи @@ -332,9 +333,6 @@ Изпрати анонимна статистика за използването и отчети за грешки за да помогнеш да се подобри Tasks. Няма да бъдат събирани персонални данни. Този таг вече съществува Името не може да е празно - Прекъсване на Doze режим за уведомления - Android значително ще забави уведомленията когато устройството е в Doze режим - Android ще позволява ограничен брой прекъсвания когато устройството е в Doze режим (Без заглавие) Бутон \"Назад\" запазва задачата Списък по подразбиране @@ -380,14 +378,6 @@ Тапет Ден/Нощ По подразбиране - изключено - на всеки час - на всеки три часа - на всеки шест часа - на всеки дванадесет часа - всеки ден - на всеки три дена - всяка седмица Автоматичната синхронизация в момента е деактивирана от Android Общи Език @@ -416,4 +406,8 @@ %s изтрити Изтриване на избраните задачи? Копиране на избраните задачи? + Дата и време + Начало на седмицата + Локализация по подразбиране + Контроли за избор на дата и време по подразбиране \ No newline at end of file diff --git a/src/main/res/values-ca/strings.xml b/src/main/res/values-ca/strings.xml index 7b86bcf88..3c6891391 100644 --- a/src/main/res/values-ca/strings.xml +++ b/src/main/res/values-ca/strings.xml @@ -65,6 +65,7 @@ Segur que voleu suprimir els esdeveniments de calendari de les tasques completades? Demà passat La setmana que ve + No amagis Tasques actives Modificades fa poc Tasques actives @@ -106,6 +107,7 @@ Tasques sent cronometrades inici d\'aquesta tasca: finalització d\'aquesta tasca: + Temps invertit Eliminar tasca 1 tasca @@ -125,21 +127,19 @@ Informa d\'un problema Contacta amb el desenvolupador Sense recordatoris durant les hores de silenci + Radi (metres) + Filtres Configuració dels filtres Mostra ocultes Mostra completades + Ordenació inversa Alta Predeterminat Baix - desactivat - cada hora - cada tres hores - cada sis hores - cada dotze hores - diàriament - cada tres dies - setmanalment + Blanc + Idioma Localització D\'esquerra a dreta De dreta a esquerra + Led de notificació \ No newline at end of file diff --git a/src/main/res/values-cs/strings.xml b/src/main/res/values-cs/strings.xml index b3780fcac..33666d7c5 100644 --- a/src/main/res/values-cs/strings.xml +++ b/src/main/res/values-cs/strings.xml @@ -291,9 +291,6 @@ Vylepšit Úkoly Odesílat anonymní statistiky využití a zprávy o selhání ke zlepšení Úkolů. Žádné osobní údaje nebudou shromažďovány. Štítek již existuje - Přerušit Doze mód pro oznámení - Když je zařízení v Doze režimu, Android může významně zpozdit upozornění - Když je zařízení v Doze režimu, Android může významně zpozdit upozornění (Bez názvu) Tlačítko zpět uloží úkol Výchozí seznam @@ -326,14 +323,6 @@ Tapeta Den/Noc Výchozí - zakázat - každou hodinu - každé tři hodiny - každých šest hodin - každých dvanáct hodin - každý den - každé tři dny - každý týden Automatická synchronizace je právě zakázána Androidem Všeobecný Jazyk diff --git a/src/main/res/values-da/strings.xml b/src/main/res/values-da/strings.xml index aa73aae89..55a0b0311 100644 --- a/src/main/res/values-da/strings.xml +++ b/src/main/res/values-da/strings.xml @@ -103,11 +103,4 @@ Log af Sletter al synkroniserings data Kildekode - hver time - hver 3. time - hver 6. time - hver 12. time - hver dag - hver 3. dag - hver uge \ No newline at end of file diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 45c5a2766..2c3300c04 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -259,6 +259,7 @@ Synchronisierung Aktiviert Schriftgröße + Reihen-Abstand Änderungsbildschirm anpassen Quelltext Zur Übersetzung beitragen @@ -325,9 +326,6 @@ Anonyme Nutzungsstatistiken und Absturzberichte zur Verbesserung von Tasks senden. Es werden keine persönlichen Daten gesammelt. Tag existiert bereits Name darf nicht leer sein - Doze-Modus für Benachrichtigungen unterbrechen - Im Doze-Modus von Android erfolgen Benachrichtigungen mit deutlicher Verzögerung - Android erlaubt nur eine begrenzte Anzahl von Unterbrechungen des Doze-Modus (kein Titel) Zurück-Button speichert die Aufgabe Standard-Liste @@ -368,14 +366,6 @@ Hintergrundbild Tag/Nacht Standard - deaktivieren - stündlich - alle 3 Stunden - alle 6 Stunden - alle 12 Stunden - täglich - jeden dritten Tag - wöchentlich Automatische Syncronisation ist aktuell im System deaktiviert Allgemein Sprache @@ -404,4 +394,8 @@ %s gelöscht Ausgewählte Aufgaben löschen? Ausgewählte Aufgaben kopieren? + Tag und Uhrzeit + Beginn der Woche + Aus Systemeinstellungen + Datum- und Zeitauswahl des Systems benutzen \ No newline at end of file diff --git a/src/main/res/values-el/strings.xml b/src/main/res/values-el/strings.xml index bbd852ab5..db29cb704 100644 --- a/src/main/res/values-el/strings.xml +++ b/src/main/res/values-el/strings.xml @@ -203,11 +203,4 @@ Συγχρονισμός παρασκηνίου Αποσύνδεση Δωρίστε - κάθε ώρα - κάθε τρεις ώρες - κάθε έξι ώρες - κάθε δώδεκα ώρες - κάθε μέρα - κάθε τρεις ημέρες - κάθε εβδομάδα \ No newline at end of file diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index e382a49e0..beb94afb6 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -262,6 +262,7 @@ Sincronización Habilitado Tamaño de la fuente + Espaciado de fila Personalizar pantalla de edición Código fuente Contribuye con la traducción @@ -325,12 +326,9 @@ Privacidad Política de privacidad Mejorar Tasks - Enviar de forma anónima estadísticas de uso e informes de error para ayudar a mejorar Tasks. No se recogerán datos personales. + Enviar de forma anónima estadísticas de uso e informes de error para ayudar a mejorar Task. No se recogerán datos personales. La etiqueta ya existe El nombre no puede estar vacío - Interrumpir modo reposo con notificaciones - Android retrasará las notificaciones si el dispositivo está en modo reposo - Android permitirá interrupciones limitadas si el dispositivo está en modo reposo (Sin título) Botón atrás guarda la tarea Lista por defecto @@ -375,14 +373,6 @@ Fondo de pantalla Día/Noche Predeterminado - deshabilitar - cada hora - cada tres horas - cada seis horas - cada doce horas - cada día - cada tres días - cada semana La sincronización automática está actualmente deshabilitada por Android Idioma Debe reiniciar Tasks para que los cambios tengan efecto @@ -410,4 +400,8 @@ %s borradas ¿Borrar tareas seleccionadas? ¿Copiar tareas seleccionadas? + Fecha y hora + Inicio de semana + Usar configuración regional + Usar fecha y hora nativas \ No newline at end of file diff --git a/src/main/res/values-fa/strings.xml b/src/main/res/values-fa/strings.xml index cfbf28d92..b632ef5ee 100644 --- a/src/main/res/values-fa/strings.xml +++ b/src/main/res/values-fa/strings.xml @@ -252,20 +252,11 @@ گرفتن عکس بهبود وظیفه این تگ قبلاً ایجاد شده است - برای اعلانات، حالت Doze را موقتاً قطع کن (بدون عنوان) لیست پیش فرض Tasks پروژه‌ای متن‌باز است که عمدتاً توسط یک‌نفر توسعه داده می‌شود. برای حمایت از این تلاش، برخی ویژگی‌ها به‌صورت خریدهای داخل برنامه ارائه شده‌اند. شفافیت سفید - غیرفعال - هر ساعت - هر سه ساعت - هر شش ساعت - هر دوازده ساعت - هر روز - هر سه روز - هر هفته درحال‌حاضر هماهنگ‌سازی خودکار توسط اندروید غیرفعال است عمومی زبان diff --git a/src/main/res/values-fi/strings.xml b/src/main/res/values-fi/strings.xml index 46617e31b..6cfc19d49 100644 --- a/src/main/res/values-fi/strings.xml +++ b/src/main/res/values-fi/strings.xml @@ -331,9 +331,6 @@ Lähetä nimettömästi käyttäjätilastoja ja virheilmoituksia Tasks -ohjelman parantamiseksi. Mitään henkilökohtaisia tietoja ei kerätä. Tunniste on jo olemassa Nimi ei voi olla tyhjä - Keskeytä Doze -tila ilmoituksia varten - Android viivästyttää merkittävästi ilmoituksia kun laite on Doze -tilassa - Android sallii rajoitetusti keskeytyksiä kun laite on Doze -tilassa (Ei nimikettä) Takaisin -painike tallentaa tehtävän Oletuslista @@ -379,14 +376,6 @@ Taustakuva Päivä/Yö Oletus - Poistettu - Joka tunti - Joka kolmas tunti - Joka kuudes tunti - Joka 12. tunti - Joka päivä - Joka kolmas päivä - Joka viikko Android on estänyt automaattisen synkronoinnin Yleinen Kieli diff --git a/src/main/res/values-fr/strings.xml b/src/main/res/values-fr/strings.xml index 0cb87d52a..d1101b977 100644 --- a/src/main/res/values-fr/strings.xml +++ b/src/main/res/values-fr/strings.xml @@ -257,6 +257,7 @@ Synchronisation Activé Taille de police + Écartement de la ligne Personnaliser l\'écran d\'édition Code source Contribuer aux traductions @@ -322,14 +323,11 @@ Envoyer des statistiques anonymes d\'usage et les rapports de plantage afin d\'aider à l\'amélioration de Tasks. Aucune donnée personnelle ne sera collectée. Le tag existe déjà Le nom ne peut pas être vide - Interrompt le Doze mode pour les notifications - Android va différer les notifications de façon importante lorsque l\'appareil sera en Doze mode - Android autorisera des interruptions limitées lorsque l\'appareil sera en Doze mode (Sans titre) Faire un retour-arrière pour sauvegarder la tâche Liste par défaut Tasks est un projet à source ouverte entretenu par un développeur. Quelques contenus payants sont disponible dans l\'application pour supporter le développement. - Affiche un badge pour le nombre de tâches actives dans votre liste par défaut. TeslaUnread pour Nova Launcher est requis + Affiche un badge pour le nombre de tâches actives dans votre liste par défaut. TeslaUnread pour Nova Launcher est requis. Débloquer tous les thèmes et quelques couleurs à Tasks Notifications de la sensibilité du contexte de la liste. Tasker ou Local est requis. Les donations sont grandement appréciées. @@ -366,14 +364,6 @@ Fond d\'écran Jour/Nuit Par défaut - désactiver - toutes les heures - toutes les trois heures - toutes les six heures - toutes les douze heures - tous les jours - tous les trois jours - toutes les semaines La synchronisation automatique est actuellement déactivé par Android Général Langage @@ -402,4 +392,8 @@ %s supprimé Supprimer les tâches sélectionnées ? Copier les tâches sélectionnées ? + Date et heure + Début de la semaine + Par défaut du lieu d\'utilisation + Utiliser la date et l\'heure native \ No newline at end of file diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index e382a49e0..35b34ab79 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -36,11 +36,11 @@ %s [oculto] Terminado\n%s Editar - Orden manual con subtareas - Ordenación inteligente + Orde manual con subtarefas + Ordenación intelixente Por título - Por fecha límite - Por prioridad + Por data límite + Por prioridade Por última modificación Coincidencias con \'%s\' Crear nuevo filtro @@ -133,28 +133,29 @@ Mis Tareas Recién modificadas Tareas activas - o - no - también - %s cumple el criterio + ou + non + tamén + %s cumpre o criterio Eliminar Fila - Mantén pulsado sobre los elementos para opciones adicionales - Añadir Criterio - Vence el: ? + Mantén pulsado sobre os elementos para opcións adicionáis + Engadir Criterio + Vence o: ? Vence el... Sin fecha de vencimiento Próximo mes - ¿Prioridad mínima? + Prioridade mínima? Prioridad... Etiqueta: ? Etiqueta... El nombre de la etiqueta contiene... El nombre de la etiqueta contiene: ? Título contiene... - Título contiene: ? - ¡Ocurrió un error al agregar la tarea al calendario! - Agregar al calendario - Abrir evento del calendario + Contén título:? + Erro ao engadir a tarefa ao calendario! + Engadir ao calendario + Abrir o evento do calendario + ¿Borrar evento de calendario? Evento de calendario no encontrado %s (completado) @@ -200,7 +201,7 @@ bimensualmente Cada %d Intervalo de repetición - Sin repetir + Sen repetir Día(s) Semana(s) Mes(es) @@ -228,7 +229,7 @@ Ingreso por voz Avisos de voz Tasks dirá los nombres de las tareas durante los avisos - Eliminar tarea + Eliminar tarefa Tarea agregada Almacenamiento externo inaccesible @@ -262,14 +263,15 @@ Sincronización Habilitado Tamaño de la fuente + Espaciado de fila Personalizar pantalla de edición - Código fuente - Contribuye con la traducción - Informa de un error + Código fonte + Contribúe coa tradución + Informa dun erro Ayuda y valoraciones - Contactar con el desarrollador + Contactar co revelador Valorar Tasks - Sin avisos en horario silencioso + Sen avisos en horario silencioso Donar Elegir cantidad Acciones de notificación @@ -314,7 +316,7 @@ Adjuntar archivo Alta Por defecto - Baja + Baixa Prioridad de la notificación Número de vibraciones Duración de cada vibración (milisegundos) @@ -328,9 +330,6 @@ Enviar de forma anónima estadísticas de uso e informes de error para ayudar a mejorar Tasks. No se recogerán datos personales. La etiqueta ya existe El nombre no puede estar vacío - Interrumpir modo reposo con notificaciones - Android retrasará las notificaciones si el dispositivo está en modo reposo - Android permitirá interrupciones limitadas si el dispositivo está en modo reposo (Sin título) Botón atrás guarda la tarea Lista por defecto @@ -346,6 +345,7 @@ Filtro Opacidad Tema + Cor Acentuado Temas adicionales Rojo @@ -375,22 +375,15 @@ Fondo de pantalla Día/Noche Predeterminado - deshabilitar - cada hora - cada tres horas - cada seis horas - cada doce horas - cada día - cada tres días - cada semana La sincronización automática está actualmente deshabilitada por Android + Xeneral Idioma Debe reiniciar Tasks para que los cambios tengan efecto Reiniciar ahora Más tarde - Configuración regional - Sentido de la escritura - De izquierda a derecha + Localización + Sentido da escritura + De esquerda a dereita De derecha a izquierda Notificación de la LED Apoyo de hardware requerido @@ -410,4 +403,5 @@ %s borradas ¿Borrar tareas seleccionadas? ¿Copiar tareas seleccionadas? + Escoller data e hora \ No newline at end of file diff --git a/src/main/res/values-hu/strings.xml b/src/main/res/values-hu/strings.xml index e17c03c6a..804d9cb9e 100644 --- a/src/main/res/values-hu/strings.xml +++ b/src/main/res/values-hu/strings.xml @@ -266,6 +266,7 @@ Szinkronizáció Engedélyezve Karakter méret + Sorköz Szerkesztő képernyő testreszabása Forráskód Részvétel a fordításban @@ -332,9 +333,6 @@ Anonim felhasználási statisztikák és hibajelentések küldése a Tasks fejlesztése érdekében. Személyes adatok gyűjtése nem történik. A címke már létezik A cím nem lehet üres - Értesítéskor kilépés Szundikáló módból - Az Android jelentősen késlelteti az értesítéseket Szundikáló módban - Az Android csak bizonyos megszakításokat engedélyez, amennyiben az eszköz Szundikáló módban van (Név nélkül) A vissza gomb elmenti a feladatot Alapértelmezett lista @@ -380,14 +378,6 @@ Háttérkép Nappal/Éjszaka Alapértelmezett - Letiltás - Óránként - Háromóránként - Hatóránként - Tizenkét óránként - Naponta - Háromnaponta - Hetente Az Android pillanatnyilag letiltotta az automatikus szinkronizálást. Általános Nyelv @@ -416,4 +406,8 @@ %s törölve Kiválasztott feladatok törlése? Kiválasztott feladatok másolása? + Dátum és időpont + Hét kezdőnapja + Beállított nyelv alapján + Natív dátum és időpont kijelölő használata \ No newline at end of file diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index 1cdc9a32d..0bfbaf2d0 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -145,7 +145,7 @@ Entro... Nessuna data di scadenza Mese successivo - Almeno per priorità ? + Solo per priorità ? Priorità... Etichetta: ? Etichetta... @@ -263,6 +263,7 @@ Sincronizzazione Abilitata Dimensione Carattere + Interlinea Personalizza schermata di modifica Codice sorgente Contribuisci alla traduzione @@ -314,7 +315,6 @@ Nessuna applicazione in grado di aprire l\'allegato Aggiungi allegato Alta - Predefinita Bassa Priorità avvisi Numero di vibrazioni @@ -329,9 +329,6 @@ Invio anonimo di statistiche e problemi di funzionamento atti a migliorare \"Tasks\". Non verrà inviato nessun dato personale. Etichetta già presente Il nome non può essere omesso - Le notifiche interrompono il pisolino - Android limiterà le notifiche se il dispositivo è in modalità riposo - Android limiterà le notifiche se il dispositivo è in modalità riposo (nessun titolo) Il tasto indietro salva l\'attività Lista predefinita @@ -377,14 +374,6 @@ Sfondo Giorno/Notte Predefinito - disabilita - ogni ora - ogni tre ore - ogni sei ore - ogni dodici ore - ogni giorno - ogni tre giorni - Ogni settimana La sincronizzazione automatica è disabilitata da Android Generale Lingua @@ -413,4 +402,8 @@ %s eliminati Elimino i compiti selezionati? Copio i compiti selezionati? + Data e ora + Inizio settimana + Usa default locale + Usa data e ora locali \ No newline at end of file diff --git a/src/main/res/values-iw/strings.xml b/src/main/res/values-iw/strings.xml index 37c384b84..a08a54179 100644 --- a/src/main/res/values-iw/strings.xml +++ b/src/main/res/values-iw/strings.xml @@ -330,9 +330,6 @@ שליחת סטסיטיקות ודיווחי קריסה של האפליקציה באופן אנונימי. מידע אישי לא נאסף כלל. תגית כבר קיימת השם לא יכול להיות ריק - ניטרול Doze Mode בהתראות - אנדרואיד יעכב את ההתראות באופן משמעותי כאשר Doze mode פעיל - מערכת אנדרואיד תאפשר מספר מוגבל של יקיצות כאשר המכשיר במצב Doze (אין כותרת) כפתור \"חזרה\" שומר שינויים במשימה רשימת ברירת מחדל @@ -378,14 +375,6 @@ תמונת רקע יום / לילה ברירת מחדל - השבת - כל שעה - כל שלוש שעות - כל שש שעות - כל שתים עשרה שעות - כל יום - כל שלושה ימים - כל שבוע סנכרון אוטומטי כרגע מושבת ע\"י Android כללי שפה diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml index b0947bfd6..74546ec2a 100644 --- a/src/main/res/values-ja/strings.xml +++ b/src/main/res/values-ja/strings.xml @@ -264,6 +264,7 @@ 同期 有効 フォントサイズ + 行間隔 編集画面をカスタマイズ ソースコード 翻訳に貢献する @@ -330,9 +331,6 @@ Tasks を改善するために、匿名で使用状況データとクラッシュレポートを送信します。個人情報は収集されません。 タグは既に存在します 名前は空にできません。 - 通知の Doze モード割り込み - デバイスが Doze モードの間、Android は通知を大幅に遅らせます - デバイスが Doze モードの間、Android は限定された割り込みを許可します (タイトルなし) 戻るボタンでタスクを保存します デフォルトリスト @@ -378,14 +376,6 @@ 壁紙 デイナイト デフォルト - 無効 - 1時間毎 - 3時間毎 - 6時間毎 - 12時間毎 - 毎日 - 3日に一度 - 毎週 自動同期は、現在 Android によって無効にされています 全般 言語 @@ -414,4 +404,8 @@ %s 削除済 選択したタスクを削除しますか? 選択したタスクをコピーしますか? + 日付と時刻 + 週の始まり + ロケールのデフォルトを使用する + ネイティブの日付と時刻選択を使用する \ No newline at end of file diff --git a/src/main/res/values-ko/strings.xml b/src/main/res/values-ko/strings.xml index 468a575f1..1fc96697e 100644 --- a/src/main/res/values-ko/strings.xml +++ b/src/main/res/values-ko/strings.xml @@ -12,34 +12,34 @@ 필터 이름 입력 구글 계정 선택 백업 - 일정 가져오기 - 일정 내보내기 + 할일 가져오기 + 할일 내보내기 %1$s 를 %2$s 로 백업함. - 내보낼 일정 없음. + 내보낼 할일 없음. 간편 복원 파일 %1$s: %2$s 개의 일정 포함.\n\n %3$s 개 가져오기 성공,\n %4$s 개 이미 존재,\n %5$s 개 에러 발생\n - 일정 %d 읽는 중... + 할일 %d 읽는 중... 폴더에 접근 불가: %s SD 카드에 접근할 수 없습니다! Tasks 권한 Tasks 권한 변경 사항을 버리시겠습니까? 계속 편집 - 이 일정을 삭제할까요? + 이 할일을 삭제할까요? 시간 (시 : 분) 실행 취소 누르세요 - 일정이 없습니다. + 할일이 없습니다. 정렬 검색 설정 %s [숨김] 일정 완료\n%s 편집 - 수동 정렬 (하위일정 포함) + 수동 정렬 (하위할일 포함) Tasks 스마트 정렬 제목 순서 마감일 순서 @@ -47,7 +47,7 @@ 최종 수정일 순서 \'%s\' 검색 중 새 필터 만들기 - 일정 이름 + 할일 제목 우선순위 기한 없음 숨기기 기간: @@ -69,6 +69,7 @@ 우선순위 설명 파일 + 구글 할일 목록 일정 알림 타이머 조절 ----항상 숨김---- @@ -96,27 +97,28 @@ 표시할 필터 화면 설정 편집하기 기본값으로 초기화하기 - 전체 일정 제목 보기 - 일정 편집에서 댓글 보기 - 일정 목록 옵션 + 할일 제목 전체 보기 + 할일 편집에서 댓글 보기 + 할일 목록 옵션 달력 이벤트 시간 설정한 시간에 달력 이벤트 종료 설정한 시간에 달력 이벤트 시작 - 오래된 일정 관리하기 - 삭제한 일정을 비우기 - 삭제한 일정을 모두 비울까요?\n\n비운 일정은 다시 살릴 수 없습니다! - %d 일정을 비웠습니다! + 오래된 할일 관리하기 + 삭제한 할일을 비우기 + 삭제한 할일을 모두 비울까요? +비운 할일은 되돌릴 수 없습니다! + %d 할일을 비웠습니다! 설정 초기화 설정이 기본값으로 초기화될 것입니다 - 일정 데이터 지우기 - 모든 일정이 영구적으로 삭제될 것입니다 - 완료한 일정의 달력 이벤트를 삭제하기 - 완료한 일정의 모든 이벤트를 정말 삭제할까요? + 할일 데이터 지우기 + 모든 할일이 영구적으로 삭제될 것입니다 + 완료한 할일의 달력 이벤트를 삭제하기 + 완료한 할일의 모든 이벤트를 정말 삭제할까요? %d 달력 이벤트를 삭제했습니다! - 일정의 모든 달력 이벤트 삭제하기 - 일정의 모든 이벤트를 정말 삭제할까요? + 할일의 모든 달력 이벤트 삭제하기 + 할일의 모든 이벤트를 정말 삭제할까요? %d 달력 이벤트를 삭제했습니다! - 일정 기본값 + 할일 기본값 기본 중요도 기본 우선순위 기본 숨기기 기간: @@ -133,9 +135,9 @@ 기한 알림 없음 완료일에 마감일이나 그 이후 - 나의 일정 + 나의 할일 최근에 수정한 일정 - 실행중인 일정 + 실행중인 할일 또는 제외 또한 @@ -155,7 +157,7 @@ 태그 이름이 다음을 포함: ? 제목이 다음을 포함... 제목이 다음을 포함: ? - 달력에 일정 추가 실패! + 달력에 할일 추가 실패! 달력에 일정 추가하기 달력 이벤트 열기 달력 일정을 지울까요? @@ -167,8 +169,8 @@ 동기화 가능한 구글 계정이 없음 인증 중... 죄송합니다, 구글 서버와 통신하는 데 문제가 있습니다. 잠시 후 다시 시도하세요. - 구글 일정 - %s 계정을 찾을 수 없습니다 - 로그아웃하고 구글 일정 설정에서 다시 로그인해 보세요. + 구글 할일 목록 (Google Tasks) + %s 계정을 찾을 수 없습니다 - 로그아웃하고 구글 할일목록 (Google Tasks) 설정에서 다시 로그인해 보세요. 노트 기록 정말입니까? 되돌릴 수 없습니다 오디오 녹음 중 @@ -189,11 +191,11 @@ 알림 꺼짐 시작 시간 알림 꺼짐 종료 시간 기본 알림 설정 - 마감일이 없는 일정 알림은 %s 에 나타날 것입니다. + 마감일이 없는 할일 알림은 %s 에 나타날 것입니다. 항구적 알림 항구적 알림은 지울 수 없습니다 다중-링 알림의 최대 볼륨 - Tasks는 다중-링 알림을 최대 볼륨으로 출력합니다 + Tasks는 다중 소리 알림을 최대 볼륨으로 출력합니다 랜덤 알림 사용안함 매시간 @@ -219,24 +221,25 @@ %s 까지 반복 %1$s 이 %2$s 로 변경되었습니다 새 태그 만들기 + 새 목록 만들기 미분류 일정 %s 삭제할까요? %s 동안 타이머 작동함! - 기한이 정해진 일정 + 기한이 정해진 할일 타이머 - 이 일정 시작: - 이 일정 중지: + 이 할일 시작: + 이 할일 중지: 소요 시간: - 일정을 만들려면 말하세요 + 할일을 만들려면 말하세요 음성 입력 음성 알림 - 일정 알림기간 동안 일정 이름을 직접 말해 줍니다 - 일정 지우기 - 추가된 일정 + 할일 알림 시 할일 제목을 소리내어 읽어줍니다 + 할일 지우기 + 추가된 할일 외부 저장소에 접근할 수 없음 - 1 일정 - %d 일정 + 1 할일 + %d 할일 오늘 내일 @@ -250,6 +253,10 @@ 로그아웃 모든 동기화 자료 삭제 로그아웃 / 모든 동기화 데이터 삭제? + 완료 예정일 표시 + 체크박스 표시 + 헤더 표시 + 설정버튼 표시 알림 무음 소리 @@ -261,8 +268,10 @@ 동기화 활성화됨 글자 크기 + 줄 간격 일정 편집화면 레이아웃 설정하기 소스 코드 + 번역 참여하기 문제점 보고하기 도움말 & 피드백 개발자와 연락 @@ -299,11 +308,14 @@ 변경 사항을 취소할까요? 버리기 태그 설정 + 목록 설정 삭제 + 복사 필터 설정 숨겨진 일정 표시 - 완료된 일정 표시 + 완료된 할일 표시 반전 + %d 할일 인앱 결제 첨부 파일을 열 수 있는 앱이 발견되지 않았습니다 첨부파일 추가 @@ -322,24 +334,24 @@ Tasks 향상 Tasks를 향상시키기 위해 사용 기록과 충돌 보고서를 익명으로 전송합니다. 개인 정보는 수집되지 않습니다. 태그가 이미 존재합니다 - 알림을 위해 Doze Mode 방해하기 - Android는 기기가 Doze Mode에 있을 때 알림을 매우 늦춥니다 - Android는 기기가 Doze Mode에 있을 때 제한된 수준에서 이 모드를 방해합니다 + 이름은 필수 입력항목입니다. (제목 없음) - 뒤로가기 버튼으로 일정 저장 + 뒤로가기 버튼으로 할일 저장 기본 목록 Tasks는 한 명의 개발자에 의해 유지되고 있는 오픈 소스 프로젝트입니다. 개발을 지속하기 위해 몇몇 기능들은 인앱 결제를 통해 제공되고 있습니다. - 기본 목록에 있는 활성화된 일정의 숫자 뱃지를 표시합니다. Nova Launcher용 TeslaUnread가 필요합니다. + 기본 목록에 있는 활성화된 할일의 숫자 뱃지를 표시합니다. Nova Launcher용 TeslaUnread가 필요합니다. 모든 테마 잠금 해제 및 색상 추가 컨텍스트 기반의 리스트 알림. Tasker 또는 Locale이 필요합니다. 기부를 해주시면 감사하겠습니다 - 활성화된 일정의 개수를 표시합니다. DashClock Widget을 필요로 합니다. + 활성화된 할일의 개수를 표시합니다. DashClock Widget을 필요로 합니다. 구매 확장팩 구매 인앱 결제 서비스가 혼잡합니다, 나중에 다시 시도하세요 필터 불투명도 테마 + 색상 + 강조 추가적인 테마 빨강 분홍 @@ -361,12 +373,43 @@ 회색 회청색 검정 - 사용안함 - 매시간 - 3시간마다 - 6시간마다 - 12시간마다 - 매일 - 3일마다 - 일주일마다 + 어두운 회색 + 흰색 + 밝게 + 어둡게 + 바탕화면 + 주간/야간 + 기본값 + 현재 자동 동기화는 안드로이드에 의해 사용이 불가합니다. + 일반 + 언어 + 이 변경을 적용하려면 Tasks 앱을 재시작해야 합니다. + 지금 재시작하기 + 나중에 + 현지화 + 레이아웃 정렬 방향 + 왼쪽 정렬 + 오른쪽 정렬 + LED 알림 + 하드웨어 지원을 필요로 합니다. + LED 색상 + 달력 없음 + 위젯 설정 + 헤더 설정 + 줄 설정 + 상단바 + 할일 설명 보기 + Tasks는 권한이 필요합니다. + 새 목록 만들기 + 목록 삭제하기 + 목록 이름 변경하기 + 완료한 할일을 지울까요? + %s 복사 완료 + %s 삭제 완료 + 선택한 할일을 삭제할까요? + 선택한 할일을 복사할까요? + 날짜와 시간 + 한 주의 시작 + 기본으로 설정된 언어 사용하기 + 시스템 날짜와 시간 입력창 사용하기 \ No newline at end of file diff --git a/src/main/res/values-nb/strings.xml b/src/main/res/values-nb/strings.xml index 187c9cbda..c03f154f6 100644 --- a/src/main/res/values-nb/strings.xml +++ b/src/main/res/values-nb/strings.xml @@ -96,12 +96,4 @@ Logg ut Sletter all synkroniseringsdata Logg ut / slett synkroniseringsdata? - deaktiver - hver time - hver tredje time - hver sjette time - hver tolvte time - daglig - hver tredje dag - hver uke \ No newline at end of file diff --git a/src/main/res/values-nl/strings.xml b/src/main/res/values-nl/strings.xml index f26c3b22e..e576641dc 100644 --- a/src/main/res/values-nl/strings.xml +++ b/src/main/res/values-nl/strings.xml @@ -324,9 +324,6 @@ Verstuur anoniem gebruikersstatistieken en crash rapporten om Tasks te verbeteren. Er worden geen persoonlijke gegevens verzameld. Label bestaat reeds Naam mag niet leeg zijn - Onderbreek Snooze mode voor notificaties - Android zal vertraagd notificaties weergeven in Snooze mode - Android zal gelimiteerd onderbreken in Snooze mode (geen titel) Terug knop slaat taak op Standaard lijst @@ -369,14 +366,6 @@ Achtergrond Dag/Nacht Standaard - uitschakelen - elk uur - elke 3 uur - elke 6 uur - elke 12 uur - elke dag - elke 3 dagen - elke week Automatische synchronisatie is momenteel uitgezet door Android Globaal Taal diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 00ffd34bb..02c93fa95 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -263,6 +263,7 @@ Synchronizacja Włączone Rozmiar czcionki + Odstęp wierszy Dostosuj ekran edycji Kod źródłowy Wspomóż tłumaczenie @@ -329,9 +330,6 @@ Wyślij anonimowe statyki użycia i raporty o awariach celem ulepszenia Tasks. Żadne prywatne dane nie będą gromadzone. Tag już istnieje Nazwa nie może być pusta - Przerwij tryb Doze dla powiadomień - Android będzie znacznie opóźniał powiadomienia w czasie trybu drzemki. - Android będzie pozwalał na ograniczone przerwania kiedy urządzenie jest w trybie drzemki. (Bez tytułu) Przycisk Cofnij zapisuje zadanie Domyślna lista @@ -376,14 +374,6 @@ Tapeta Dzień/noc Domyślny - Wyłączone - co godzinę - co 3 godziny - co 6 godzin - co 12 godzin - raz dziennie - co 3 dni - co tydzień Automatyczna synchronizacja jest obecnie wyłączona przez Androida Podstawowe Język @@ -410,4 +400,9 @@ Wyczyścić ukończone zadania? %s skopiowanych %s usuniętych + Usunąć zaznaczone zadania? + Skopiować zaznaczone zadania? + Data i czas + Początek tygodnia + Użyj domyślnych \ No newline at end of file diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index ce94160d4..baaf01f23 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -8,6 +8,9 @@ Limpar imagem Comentar... + Nome + Digite o nome do filtro + Selecione a Conta do Google Importar tarefas Exportar tarefas Backups feitos: de %1$s para %2$s. @@ -48,6 +51,7 @@ Descrição Quanto tempo isto vai levar? Tempo já gasto na tarefa + Salvar Decorrido %s Sem horário Data de vencimento @@ -55,9 +59,12 @@ Dias antes do vencimento Semanas antes do vencimento Quando + Repetir + Calendário Prioridade Descrição Arquivos + Lista de Tarefas do Google Lembretes Temporizador ----Esconder Sempre--- @@ -124,7 +131,7 @@ Minhas Tarefas Modificadas recentemente Tarefas ativas - OU + ou NÃO E TAMBÉM %s tem @@ -137,11 +144,16 @@ Próximo mês Pelo menos por prioridade? Prioridade... + Tag... + Nome da tag contém... + Nome da tag contém: ? Título... Título contêm: ? Erro ao inserir a tarefa no calendário! Inserir no calendário Abrir evento no calendário + Excluir evento no calendário? + Evento no calendário não encontrado %s (concluído) Na lista: ? No Google Tasks... @@ -157,6 +169,9 @@ Parar Gravação Desculpa! Nenhuma aplicação para manipular este tipo de arquivo foi encontrada. Erro ao copiar o arquivo para o anexo + Tocar uma vez + Tocar cinco vezes + Tocar continuamente por hora por dia por semana @@ -196,8 +211,11 @@ Todo %1$s\naté %2$s Repetir para sempre Repetir até %s + %1$s remarcada para %2$s Criar nova etiqueta + Criar nova lista Sem categoria + Excluir %s? Temporizador ativado para %s! Tarefas com contagem de tempo Temporizador @@ -217,6 +235,7 @@ Hoje Amanhã + Próximo %s Ontem amanhã ontem @@ -226,10 +245,20 @@ Desconectar Limpar todos os dados de sincronização Desconectar / limpar dados de sincronização? + Mostrar vencimentos + Mostrar tarefas ocultas + Mostrar ocultas + Configurações + Notificações + Silencioso + Som Vibrações Horas calmas Diretório para anexos Diretório de backup + Sincronização + Ativado + Tamanho do texto Personalizar tela de edição Código fonte Ajude com as traduções @@ -239,18 +268,43 @@ Avaliar o Tasks Sem lembretes durante as horas calmas Doar + Selecionar quantia Ações na notificação Mostrar as ações \"Adiar\" e \"Concluída\" na notificação + Adicionar lembrete + Remover + Aleatoriamente uma vez + Aleatoriamente Escolha uma data Escolha uma hora + Escolha data e horário + Escolher lugar + Quando vencida + Quando vencer Raio (metros) Tempo de resposta (valores maiores conservam bateria) Lembretes com Geolocalização Etiquetas Filtros + Por uma hora + Manhã + Tarde + Noite + Noite + Amanhã de manhã + Amanhã de tarde + %1$s deve vir antes de %2$s + %1$s deve vir depois de %2$s + Descartar alterações? + Descartar + Config. Tags + Configurações da lista + Excluir + Copiar Configurações de filtro Mostrar ocultas Mostrar completas + %d tarefas Compras no app Nenhuma aplicação encontrada para abrir o anexo Adicionar anexo @@ -261,14 +315,15 @@ Número de vibrações Duração de cada vibração (milissegundos) Pausa entre vibrações (milissegundos) + Tirar uma foto + Selecionar da galeria Privacidade Política de privacidade Melhorar o Tasks Enviar estatísticas de uso e relatórios de falha anonimamente para ajudar a melhorar o Tasks. Nenhuma informação pessoal será coletada. Etiqueta já existe - Interromper modo Doze para notificações - O Android irá adiar as notificações significativamente enquanto o dispositivo estiver no modo Doze - O Android irá permitir interrupções limitadas enquanto o dispositivo estiver no modo Doze + Nome não pode ser vazio + (Sem título) Botão voltar salva a tarefa Lista padrão Tasks é um projeto de código aberto mantido por um desenvolvedor. Algumas funções são oferecidas como compras dentro do app a fim de apoiar o desenvolvimento. @@ -277,19 +332,24 @@ Notificações contextuadas de listas. Necessita Tasker ou Locale. Doações são muito valiosas Mostra um contador de tarefas ativas. Necessita DashClock Widget. + Comprar + Comprar extensão O serviço de compras no app está ocupado. Tente novamente mais tarde. + Filtrar Opacidade Tema Cor Cor de realce Temas adicionais Vermelho + Rosa Roxo Roxo Profundo Azul-escuro Azul Azul Claro Ciano + Verde-azulado Verde Verde Claro Limão @@ -300,19 +360,43 @@ Marrom Cinza Azul Acinzentado + Preto + Cinza escuro + Branco Claro Escuro - desabilitar - a cada hora - a cada três horas - a cada seis horas - a cada doze horas - diariamente - a cada três dias - semanalmente + Plano de fundo + Dia/Noite + Padrão Sincronização automática está atualmente desabilitada pelo Android + Geral + Idioma + Você deve reiniciar seu aplicativo para que as alterações sejam aplicadas. + Reiniciar agora + Depois Localização Direção do Layout Esquerda para direita Direita para esquerda + LED de notificações + Cor do LED + Não foi encontrado nenhum calendário + Configurações de Widget + Config. cabeçalho + Congif. entrada + Painel de notificações + Mostrar descrição + Tasks precisa de permissão + Criando uma nova lista + Excluindo lista + Renomeando uma lista + Limpar tarefas concluídas? + %s copiadas + %s excluídas + Excluir tarefas selecionadas? + Copiar tarefas selecionadas? + Hora e data + Começo da semana + Usar padrão do aparelho + Usar calendário nativo \ No newline at end of file diff --git a/src/main/res/values-pt/strings.xml b/src/main/res/values-pt/strings.xml index b3f1eea50..d455cdf63 100644 --- a/src/main/res/values-pt/strings.xml +++ b/src/main/res/values-pt/strings.xml @@ -303,14 +303,6 @@ Preto Escuro Dia/Noite - desativar - cada hora - cada 3 horas - cada 6 horas - cada 12 horas - todos os dias - cada 3 dias - todas as semanas Reiniciar agora Da esquerda para a direita Da direita para a esquerda diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index 16853019e..2b71ccbb5 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -29,7 +29,7 @@ Время (час : мин) Отмена Установить - Здесь нет задач. + Задач нет. Сортировка Поиск Настройки @@ -66,7 +66,7 @@ Приоритет Описание Файлы - Список задач Google + Список Google Task Напоминания Учет времени Всегда скрывать @@ -80,21 +80,21 @@ Перезвонить %s Игнорировать Необходим список встреч? - Вы пропустили несколько событий календаря. Tasks не следует больше спрашивать Вас о них? + Вы пропустили несколько событий календаря. Прекратить спрашивать о них? Пропустить все события Пропустить только это событие - Похоже скоро начнется %s. Хотите создать список для пунктов события? - Похоже Вы только что завершили %s. Хотите создать список для пунктов события? + Похоже скоро начнется %s. Хотите создать список действий? + Похоже Вы только что завершили %s. Хотите создать список действий? Возможно позднее Ассистент календаря - Tasks будет напоминать вам о предстоящих событиях в календаре и поможет вам подготовиться к ним + Tasks будет напоминать вам о предстоящих событиях в календаре и напомнит вам подготовиться к ним Пункты события: %s Пусто Интерфейс Показать фильтры Редактировать настройки экрана - Настройки по умолчанию - Показывать полное название задачи + Сброс настроек + Показывать полный заголовок задачи Показывать комментарии в редакторе задачи Настройка списка задач Время события в календаре @@ -106,8 +106,8 @@ Убрано %d задач! Сбросить настройки Настройки будут сброшены в первоначальное состояние - Удалить данные задачи - Все задачи будут окончательно удалены + Удалить все задачи + Все задачи будут удалены навсегда Удалить календарные события для завершенных задач Вы уверены, что хотите удалить все ваши события для завершенных задач? Удалено %d календарных событий! @@ -125,7 +125,7 @@ Средний Низкий Без срока выполнения - Через день + Послезавтра Следующая неделя Не скрывать Нет напоминаний @@ -167,8 +167,8 @@ При обращении к серверам Google возникли проблемы. Пожалуйста, попробуйте позже. Учетная запись %s не найдена -- пожалуйста, выйдите и войдите снова через настройки Google Tasks. Записать заметку - Вы уверены? Это действия нельзя отменить. - Запись голоса. + Вы уверены? Действие нельзя отменить + Запись голоса Остановить запись Извините! Не найдена программа для просмотра файлов этого типа. Ошибка копирования прикрепляемого файла. @@ -223,9 +223,9 @@ Задачи с таймером Таймер задача началась: - зада завершилась: - Времени потрачено: - Говорите чтобы создать задачу + задача завершилась: + Времени затрачено: + Говорите, чтобы создать задачу Голосовой ввод Голосовые напоминания Tasks должен произносить название задач во время напоминаний @@ -248,7 +248,7 @@ Выйти Очищает все данные синхронизации Выйти / очистить данные синхронизации? - Отображать намеченную дату + Показать даты Показать флажки Показать заголовок Показать настройки @@ -263,6 +263,7 @@ Синхронизация Показывать уведомления Размер шрифта + Межстрочный интервал Порядок полей в задаче... Исходный код Участвовать в переводе программы @@ -271,7 +272,7 @@ Связь с разработчиком Оставить отзыв Выкл. напоминания в тихие часы - Поддержать автора ($) + Поддержать разработку Выберите количество Команды в уведомлении Показывать «Повтор» и «Уже готово!» в уведомлении @@ -287,7 +288,7 @@ В срок Радиус (в метрах) Период отслеживания (чем выше, тем меньше расход батареи) - Напоминания по местонахождению: + Напоминания по местонахождению Теги Фильтры Через час @@ -311,7 +312,7 @@ Реверс %d задач Покупки в приложении - Не найдено приложение, позволяющее открыть прикреплённый файл + Не найдено приложение для открытия прикреплённого файла Прикрепить файл Высокий По умолчанию @@ -325,15 +326,12 @@ Выбрать с диска Конфиденциальность Политика конфиденциальности - Улучшить Tasks - Высылать анонимную статистику и отчёты об ошибках, чтобы помочь улучшить Tasks. Персональная информация собираться не будет. + Содействовать улучшению + Отправлять анонимную статистику и отчёты об ошибках для содействия улучшению программы. Персональная информация не собираеться. Тег уже существует Необходимо задать имя - Прерывать спящий режим для уведомлений - Android будет сильно задерживать уведомления, если устройство находится в спящем режиме - Android разрешит ограниченные прерывания, если устройство находится в спящем режиме (Без заголовка) - Кнопка \"назад\" сохраняет задачу + Кнопка «Назад» сохраняет задачу Список по умолчанию Tasks развивается как проект с открытым исходным кодом и поддерживается единственным разработчиком. Некоторые функции приложения предлагаются как платные для дальнейшего развития программы Показывать значок для числа активных задач в вашем основном списке. Требуется TeslaUnread для Nova Launcher @@ -346,7 +344,7 @@ Встроенный сервис перегружен, попробуйте позже Фильтр Прозрачность - Тема + Цветовая тема Цвет Акцент Дополнительные темы @@ -357,8 +355,8 @@ Индиго Синий Светло-синий - Голубой - Бирюзовый + Ярко-бирюзовый + Тёмно-бирюзовый Зелёный Светло-зелёный Лайм @@ -377,19 +375,11 @@ Как обои День / ночь По умолчанию - отключить - каждый час - каждые 3 часа - каждые 6 часов - каждые 12 часов - каждый день - каждые 3 дня - каждую неделю Автосинхронизация в Android выключена Общие Язык - Для изменений нужно перезагрузить Tasks - Перезагрузить сейчас + Для применения изменений программа должна быть перезапущена + Перезапустить сейчас Позже Локализация Ориентация интерфейса @@ -413,4 +403,8 @@ %s удалено Удалить выделенные задачи? Скопировать выделенные задачи? + Дата и время + Начало недели + Как в системе + Встроенные элементы даты/времени \ No newline at end of file diff --git a/src/main/res/values-sk/strings.xml b/src/main/res/values-sk/strings.xml index ac6b16e82..4c8a0eae0 100644 --- a/src/main/res/values-sk/strings.xml +++ b/src/main/res/values-sk/strings.xml @@ -265,12 +265,4 @@ Zobraziť skryté Zobraziť dokončené Nízka - zakázať - každú hodinu - každé tri hodiny - každých šesť hodín - každých dvanásť hodín - každý deň - každý tretí deň - každý týždeň \ No newline at end of file diff --git a/src/main/res/values-sl-rSI/strings.xml b/src/main/res/values-sl-rSI/strings.xml index 5f3d38b49..c7c97e2d5 100644 --- a/src/main/res/values-sl-rSI/strings.xml +++ b/src/main/res/values-sl-rSI/strings.xml @@ -212,12 +212,4 @@ Zbriše vse usklajene podatke Odjava/brisanje usklajenih podatkov? Donirajte - onemogoči - vsako uro - vsake 3 ure - vsakih 6 ur - vsakih 12 ur - vsak dan - vsake 3 dni - vsak teden \ No newline at end of file diff --git a/src/main/res/values-sr/strings.xml b/src/main/res/values-sr/strings.xml index 42f019700..52bf10ca8 100644 --- a/src/main/res/values-sr/strings.xml +++ b/src/main/res/values-sr/strings.xml @@ -11,6 +11,9 @@ Po naslovu Po datumu Po prioritetnosti + Po zadnjoj izmeni + Ime zadatka + Prioritet ili Ne takođe @@ -18,12 +21,25 @@ Izbriši red Dugi pritisak za dodatne opcije Dodati kriterijum + Po datumu: ? + %s (završeno) Nasumice posetnik + Interval ponavljanja + Bez ponavljanja + Svaki %s + Ponavljaj zauvek + Interval ponavljanja %s + Obriši zadatak + Vibracije + Kontaktiraj razvojni tim + Oceni Task Bez podsetnika u mirnim satima Filter podešavanja + Prikaži skrivene Visok Podrazumevano Nisak + Broj vibracija Raspored komandi Sleva nadesno Zdesna nalevo diff --git a/src/main/res/values-sv/strings.xml b/src/main/res/values-sv/strings.xml index 001a2c61b..bff98b42f 100644 --- a/src/main/res/values-sv/strings.xml +++ b/src/main/res/values-sv/strings.xml @@ -327,9 +327,6 @@ Skicka anonym användarstatistik och crashrapporter för att hjälpa till att förbättra Tasks. Ingen personlig information kommer samlas in. Etiketten finns redan Namn måste anges - Avbryt Doze-läge för påminnelser - Android kommer att avsevärt fördröja påminnelser medan enheten är i Doze-läge - Android kommer att tillåta begränsade avbrott medan enheten är i Doze-läge (Ingen titel) Bakåtknapp sparar uppgift Standardlista @@ -369,14 +366,6 @@ Mörkt Bakgrundsbild Dag/Natt - inaktivera - varje timme - var tredje timme - var sjätte timme - var tolfte timme - varje dag - var tredje dag - varje vecka Allmäna Språk Tasks måste startas om innan ändringarna börjar gälla diff --git a/src/main/res/values-tr/strings.xml b/src/main/res/values-tr/strings.xml index a66a3602d..e04e25e44 100644 --- a/src/main/res/values-tr/strings.xml +++ b/src/main/res/values-tr/strings.xml @@ -29,11 +29,17 @@ %s [gizli] Bitti\n%s Düzenle + Alt görevleri elle sırala Tasks Akıllı Sıralama Başlığa göre Bitiş Tarihine Göre + Önceliğe göre Son Değiştirme Tarihine Göre \'%s\' ile eşleşenler + Görev adı + Öncelik + Kadar gizle + %s kadar gizle Açıklama Ne kadar sürecek? Görev üzerinde çalışılan zaman @@ -92,11 +98,13 @@ aynı zamanda %s ölçütüne sahip Satırı Sil + Ek seçenekler için öğelere uzun basın Ölçüt Ekle Şu tarihte: ? Şu tarihte... Son Tarih Yok Gelecek Ay + En az öncelik ? Başlık şunu içersin... Başlık şunu içersin:? Takvime görev eklenmesinde hata! @@ -141,6 +149,7 @@ ayda 2 kez Her %d Yineleme Aralığı + Yenilenmeyen Gün Hafta Ay @@ -162,6 +171,7 @@ Ses Girişi Sesli Hatırlatmalar Tasks görev isimlerini görev hatırlatmaları sırasında söyleyecek + Görevi sil 1 görev %d görev @@ -177,12 +187,32 @@ Çıkış Yap Bütün eşleme verilerini temizle Çıkış Yap / Senkron verisini sil? - devre dışı bırak - her saat - her 3 saat - her 6 saat - her 12 saat - hergün - her 3 gün - her hafta + Bitiş tarihlerini göster + Titreşimler + Satır aralığı + Kaynak kodu + Çeviri katkıları + Sorun bildir + Geliştiriciyle iletişime geç + Tasks\'i Oyla + Sessiz saatlerde hatırlatıcı yok + Bildirimde erteleme ve tamamlanma hareketlerini göster + Etiketler + Filtreler + Filtre Ayarları + Gizlenenleri göster + Tamamlananları göster + Yüksek + Varsayılan + Düşük + Titreşim sayısı + Her titreşimin uzunluğu (milisaniye) + Titreşimler arasında duraklama (milisaniye) + Tasks, bir geliştirici tarafından tutulan açık kaynaklı bir projedir. Bazı özellikleri, geliştirmeyi desteklemek için uygulama içi satın alma olarak sunulmaktadır. + Şeffaflık + Otomatik eşitleme şu anda Android tarafından devre dışı bırakıldı + Yerelleştirme + Yerleşim yönü + Soldan sağa + Sağdan sola \ No newline at end of file diff --git a/src/main/res/values-uk/strings.xml b/src/main/res/values-uk/strings.xml index 066daf351..91f194f44 100644 --- a/src/main/res/values-uk/strings.xml +++ b/src/main/res/values-uk/strings.xml @@ -332,9 +332,6 @@ Відсилати анонімну статистику використання і звіти про помилки щоб покращити Tasks. Жодних персональних даних не буде передано. Тег вже існує Ім\'я не може бути порожнє - Переривати режим \"Doze\" для сповіщень - Андроїд зазвичай буде значно відкладати повідомлення коли пристрій в режимі \"Doze\" - Android дозволить деякі переривання в режимі \"Doze\" (без назви) Зберігати завдання кнопкою Назад Типовий список @@ -380,14 +377,6 @@ Шпалери День/Ніч Типово - вимкнути - кожну годину - кожні 3 години - кожні 6 годин - кожні 12 годин - щодня - кожні 3 дня - кожного тижня Автоматична синхронізація наразі недоступна в Android Загальні Мова diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index ba4cdda12..852e0a4a9 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -236,14 +236,6 @@ 蓝灰色 黑色 深灰色 - 停用 - 每小时 - 每3小时 - 每6小时 - 每12小时 - 每天 - 每3天 - 每周 语言 马上重启 稍后 diff --git a/src/main/res/values-zh-rTW/strings.xml b/src/main/res/values-zh-rTW/strings.xml index 5cb30c526..e7fc5aed8 100644 --- a/src/main/res/values-zh-rTW/strings.xml +++ b/src/main/res/values-zh-rTW/strings.xml @@ -234,14 +234,6 @@ 強調色 其他主題 預設 - 停用 - 每小時 - 每3小時 - 每6小時 - 每12小時 - 每天 - 每3天 - 每週 一般 語言 排版方向 diff --git a/src/main/res/values/arrays.xml b/src/main/res/values/arrays.xml index 0c709de1f..0a3bddb33 100644 --- a/src/main/res/values/arrays.xml +++ b/src/main/res/values/arrays.xml @@ -40,18 +40,6 @@ 600 - - - @string/sync_interval_disable - @string/sync_interval_one_hour - @string/sync_interval_three_hours - @string/sync_interval_six_hours - @string/sync_interval_twelve_hours - @string/sync_interval_one_day - @string/sync_interval_three_days - @string/sync_interval_one_week - - @string/due_date @string/due_time diff --git a/src/main/res/values/keys.xml b/src/main/res/values/keys.xml index c8d314a5b..926a8b3df 100644 --- a/src/main/res/values/keys.xml +++ b/src/main/res/values/keys.xml @@ -158,25 +158,9 @@ - - - 0 - 3600 - 10800 - 21600 - 43200 - 86400 - 259200 - 604800 - - sync_forget - - - gtasks_sync_freq gtasks_noteMetadataSync - @@ -220,7 +204,6 @@ reverse_sort manual_sort notification_priority - doze_notifications @string/TEA_ctrl_when_pref @@ -295,7 +278,6 @@ theme_color theme_accent default_gtasks_list - sync_warning_shown language layout_direction led_color @@ -320,5 +302,7 @@ Debug rmd_show_description start_of_week + use_native_datetime_pickers + gtask_background_sync diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 65d2a9f6e..b1815d8c3 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -736,9 +736,6 @@ File %1$s contained %2$s.\n\n Send anonymous usage statistics and crash reports to help improve Tasks. No personal data will be collected. Tag already exists Name cannot be empty - Interrupt Doze mode for notifications - Android will significantly delay notifications while device is in Doze mode - Android will allow limited interruptions while device is in Doze mode (No title) Back button saves task Default list @@ -786,14 +783,6 @@ File %1$s contained %2$s.\n\n Day/Night Default - Disable - Every hour - Every three hours - Every six hours - Every twelve hours - Every day - Every three days - Every week Automatic synchronization is currently disabled by Android General Language @@ -825,4 +814,5 @@ File %1$s contained %2$s.\n\n Date and time Start of week Use locale default + Use native date and time pickers diff --git a/src/main/res/xml/preferences_date_time.xml b/src/main/res/xml/preferences_date_time.xml index e7298ad4e..034ef6916 100644 --- a/src/main/res/xml/preferences_date_time.xml +++ b/src/main/res/xml/preferences_date_time.xml @@ -7,6 +7,11 @@ android:key="@string/p_start_of_week" android:title="@string/start_of_week" /> + + - + android:key="@string/gtask_background_sync" + android:title="@string/sync_SPr_interval_title"/> - \ No newline at end of file