diff --git a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.java b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.java index f7b351e36..d5a37dbaa 100644 --- a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.java +++ b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.java @@ -23,14 +23,12 @@ import android.appwidget.*; import android.content.*; import android.content.res.*; import android.os.*; - -import androidx.annotation.NonNull; -import androidx.annotation.StyleRes; -import androidx.test.*; -import androidx.test.filters.*; import android.util.*; -import androidx.test.platform.app.InstrumentationRegistry; +import androidx.annotation.*; +import androidx.test.filters.*; +import androidx.test.platform.app.*; +import androidx.test.uiautomator.*; import junit.framework.*; @@ -44,9 +42,12 @@ import org.isoron.uhabits.core.utils.*; import org.junit.*; import java.io.*; +import java.time.*; import java.util.*; import java.util.concurrent.*; +import static androidx.test.platform.app.InstrumentationRegistry.*; +import static androidx.test.uiautomator.UiDevice.*; import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.MatcherAssert.*; @@ -78,11 +79,14 @@ public class BaseAndroidTest extends TestCase private boolean isDone = false; + private UiDevice device; + @Override @Before public void setUp() { if (Looper.myLooper() == null) Looper.prepare(); + device = getInstance(getInstrumentation()); targetContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); testContext = InstrumentationRegistry.getInstrumentation().getContext(); @@ -215,4 +219,52 @@ public class BaseAndroidTest extends TestCase { return DateUtils.getToday().minus(offset); } + + + public void setSystemTime(String tz, + int year, + int javaMonth, + int day, + int hourOfDay, + int minute) throws Exception + { + GregorianCalendar cal = new GregorianCalendar(); + cal.setTimeZone(TimeZone.getTimeZone(tz)); + cal.set(Calendar.SECOND, 0); + cal.set(year, javaMonth, day, hourOfDay, minute); + setSystemTime(cal); + } + + private void setSystemTime(GregorianCalendar cal) throws Exception + { + ZoneId tz = cal.getTimeZone().toZoneId(); + + // Set time zone (temporary) + String command = String.format("service call alarm 3 s16 %s", tz); + device.executeShellCommand(command); + + // Set time zone (permanent) + command = String.format("setprop persist.sys.timezone %s", tz); + device.executeShellCommand(command); + + // Set time + command = String.format("date -u @%d", cal.getTimeInMillis() / 1000); + device.executeShellCommand(command); + + // Wait for system events to settle + Thread.sleep(1000); + } + + private GregorianCalendar savedCalendar = null; + + public void saveSystemTime() + { + savedCalendar = new GregorianCalendar(); + } + + public void restoreSystemTime() throws Exception + { + if (savedCalendar == null) throw new NullPointerException(); + setSystemTime(savedCalendar); + } } diff --git a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseUserInterfaceTest.java b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseUserInterfaceTest.java index 02eb10c31..c323dffcd 100644 --- a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseUserInterfaceTest.java +++ b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseUserInterfaceTest.java @@ -31,6 +31,9 @@ import org.isoron.uhabits.core.ui.screens.habits.list.*; import org.isoron.uhabits.core.utils.*; import org.junit.*; +import java.time.*; +import java.util.*; + import static androidx.test.core.app.ApplicationProvider.*; import static androidx.test.platform.app.InstrumentationRegistry.*; import static androidx.test.uiautomator.UiDevice.*; @@ -128,4 +131,5 @@ public class BaseUserInterfaceTest device.setOrientationLeft(); device.setOrientationNatural(); } + } diff --git a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitsActivityTestComponent.kt b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitsActivityTestComponent.kt index 508077ac7..a3fb21ba3 100644 --- a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitsActivityTestComponent.kt +++ b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitsActivityTestComponent.kt @@ -22,7 +22,6 @@ package org.isoron.uhabits import dagger.* import org.isoron.androidbase.activities.* import org.isoron.uhabits.activities.* -import org.isoron.uhabits.activities.about.* import org.isoron.uhabits.activities.habits.list.* import org.isoron.uhabits.activities.habits.list.views.* import org.isoron.uhabits.activities.habits.show.* diff --git a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitsApplicationTestComponent.java b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitsApplicationTestComponent.java index 7ff4e2e26..313e64bab 100644 --- a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitsApplicationTestComponent.java +++ b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitsApplicationTestComponent.java @@ -22,6 +22,7 @@ package org.isoron.uhabits; import org.isoron.androidbase.*; import org.isoron.uhabits.core.*; import org.isoron.uhabits.core.tasks.*; +import org.isoron.uhabits.intents.*; import dagger.*; @@ -34,7 +35,7 @@ import dagger.*; public interface HabitsApplicationTestComponent extends HabitsApplicationComponent { - + IntentScheduler getIntentScheduler(); } @Module diff --git a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/intents/IntentSchedulerTest.kt b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/intents/IntentSchedulerTest.kt new file mode 100644 index 000000000..7b260278c --- /dev/null +++ b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/intents/IntentSchedulerTest.kt @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2016-2020 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.intents + +import android.content.ContentUris.* +import androidx.test.ext.junit.runners.* +import androidx.test.filters.* +import org.hamcrest.Matchers.* +import org.isoron.uhabits.* +import org.isoron.uhabits.core.reminders.ReminderScheduler.SchedulerResult.* +import org.isoron.uhabits.receivers.* +import org.junit.* +import org.junit.Assert.* +import org.junit.runner.* +import java.util.* +import java.util.Calendar.* + +class IntentSchedulerTest : BaseAndroidTest() { + + @Before + override fun setUp() { + super.setUp() + saveSystemTime() + } + + @After + override fun tearDown() { + restoreSystemTime() + super.tearDown() + } + + @Test + @MediumTest + @Throws(Exception::class) + fun testSetSystemTime() { + setSystemTime("America/Chicago", 2020, JUNE, 1, 12, 40) + var cal = GregorianCalendar() + assertThat(cal.timeZone, equalTo(TimeZone.getTimeZone("America/Chicago"))) + assertThat(cal[YEAR], equalTo(2020)) + assertThat(cal[MONTH], equalTo(JUNE)) + assertThat(cal[DAY_OF_MONTH], equalTo(1)) + assertThat(cal[HOUR_OF_DAY], equalTo(12)) + assertThat(cal[MINUTE], equalTo(40)) + + setSystemTime("Europe/Paris", 2019, MAY, 15, 6, 30) + cal = GregorianCalendar() + assertThat(cal.timeZone, equalTo(TimeZone.getTimeZone("Europe/Paris"))) + assertThat(cal[YEAR], equalTo(2019)) + assertThat(cal[MONTH], equalTo(MAY)) + assertThat(cal[DAY_OF_MONTH], equalTo(15)) + assertThat(cal[HOUR_OF_DAY], equalTo(6)) + assertThat(cal[MINUTE], equalTo(30)) + + setSystemTime("Asia/Tokyo", 2021, DECEMBER, 20, 18, 0) + cal = GregorianCalendar() + assertThat(cal.timeZone, equalTo(TimeZone.getTimeZone("Asia/Tokyo"))) + assertThat(cal[YEAR], equalTo(2021)) + assertThat(cal[MONTH], equalTo(DECEMBER)) + assertThat(cal[DAY_OF_MONTH], equalTo(20)) + assertThat(cal[HOUR_OF_DAY], equalTo(18)) + assertThat(cal[MINUTE], equalTo(0)) + } + + @Test + @MediumTest + fun testScheduleShowReminder() { + for (h in habitList) h.setReminder(null) + ReminderReceiver.clearLastReceivedIntent() + + setSystemTime("America/Chicago", 2020, JUNE, 1, 12, 30) + val reminderTime = 1591155900000 // 2020-06-02 22:45:00 (America/Chicago) + + val habit = habitList.getByPosition(0) + val scheduler = appComponent.intentScheduler + assertThat(scheduler.scheduleShowReminder(reminderTime, habit, 0), equalTo(OK)) + + setSystemTime("America/Chicago", 2020, JUNE, 2, 22, 44) + assertNull(ReminderReceiver.getLastReceivedIntent()) + + setSystemTime("America/Chicago", 2020, JUNE, 2, 22, 46) + val intent = ReminderReceiver.getLastReceivedIntent() + assertNotNull(intent) + assertThat(parseId(intent.data!!), equalTo(habit.id)) + } + + @Test + @MediumTest + fun testScheduleWidgetUpdate() { + WidgetReceiver.clearLastReceivedIntent() + + setSystemTime("America/Chicago", 2020, JUNE, 1, 12, 30) + val updateTime = 1591155900000 // 2020-06-02 22:45:00 (America/Chicago) + + val scheduler = appComponent.intentScheduler + assertThat(scheduler.scheduleWidgetUpdate(updateTime), equalTo(OK)) + + setSystemTime("America/Chicago", 2020, JUNE, 2, 22, 44) + assertNull(WidgetReceiver.getLastReceivedIntent()) + + setSystemTime("America/Chicago", 2020, JUNE, 2, 22, 46) + val intent = WidgetReceiver.getLastReceivedIntent() + assertNotNull(intent) + } +} \ No newline at end of file diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/intents/IntentScheduler.kt b/android/uhabits-android/src/main/java/org/isoron/uhabits/intents/IntentScheduler.kt index 2882d3022..f1d404a65 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/intents/IntentScheduler.kt +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/intents/IntentScheduler.kt @@ -27,10 +27,9 @@ import android.os.Build.VERSION.* import android.os.Build.VERSION_CODES.* import android.util.* import org.isoron.androidbase.* -import org.isoron.uhabits.* import org.isoron.uhabits.core.* import org.isoron.uhabits.core.models.* -import org.isoron.uhabits.core.reminders.* +import org.isoron.uhabits.core.reminders.ReminderScheduler.* import org.isoron.uhabits.core.utils.* import java.util.* import javax.inject.* @@ -40,36 +39,37 @@ class IntentScheduler @Inject constructor( @AppContext context: Context, private val pendingIntents: PendingIntentFactory -) : ReminderScheduler.SystemScheduler { +) : SystemScheduler { private val manager = context.getSystemService(ALARM_SERVICE) as AlarmManager - fun schedule(timestamp: Long, intent: PendingIntent, alarmType: Int) { - Log.d("IntentScheduler", - "timestamp=" + timestamp + " current=" + System.currentTimeMillis()) - if (timestamp < System.currentTimeMillis()) { + private fun schedule(timestamp: Long, intent: PendingIntent, alarmType: Int): SchedulerResult { + val now = System.currentTimeMillis() + Log.d("IntentScheduler", "timestamp=$timestamp now=$now") + if (timestamp < now) { Log.e("IntentScheduler", "Ignoring attempt to schedule intent in the past.") - return; + return SchedulerResult.IGNORED } if (SDK_INT >= M) manager.setExactAndAllowWhileIdle(alarmType, timestamp, intent) else manager.setExact(alarmType, timestamp, intent) + return SchedulerResult.OK } override fun scheduleShowReminder(reminderTime: Long, habit: Habit, - timestamp: Long) { + timestamp: Long): SchedulerResult { val intent = pendingIntents.showReminder(habit, reminderTime, timestamp) - schedule(reminderTime, intent, RTC_WAKEUP) logReminderScheduled(habit, reminderTime) + return schedule(reminderTime, intent, RTC_WAKEUP) } - override fun scheduleWidgetUpdate(updateTime: Long) { + override fun scheduleWidgetUpdate(updateTime: Long): SchedulerResult { val intent = pendingIntents.updateWidgets() - schedule(updateTime, intent, RTC) + return schedule(updateTime, intent, RTC) } override fun log(componentName: String, msg: String) { diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderReceiver.java b/android/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderReceiver.java index c6e29a63b..9d39ce433 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderReceiver.java +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderReceiver.java @@ -48,11 +48,14 @@ public class ReminderReceiver extends BroadcastReceiver private static final String TAG = "ReminderReceiver"; + private static Intent lastReceivedIntent = null; + @Override public void onReceive(@Nullable final Context context, @Nullable Intent intent) { if (context == null || intent == null) return; if (intent.getAction() == null) return; + lastReceivedIntent = intent; HabitsApplication app = (HabitsApplication) context.getApplicationContext(); HabitsApplicationComponent appComponent = app.getComponent(); @@ -107,4 +110,14 @@ public class ReminderReceiver extends BroadcastReceiver Log.e(TAG, "could not process intent", e); } } + + public static void clearLastReceivedIntent() + { + lastReceivedIntent = null; + } + + public static Intent getLastReceivedIntent() + { + return lastReceivedIntent; + } } diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/receivers/WidgetReceiver.java b/android/uhabits-android/src/main/java/org/isoron/uhabits/receivers/WidgetReceiver.java index a0af53a7e..ee5ed743d 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/receivers/WidgetReceiver.java +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/receivers/WidgetReceiver.java @@ -58,6 +58,8 @@ public class WidgetReceiver extends BroadcastReceiver private static final String TAG = "WidgetReceiver"; + private static Intent lastReceivedIntent = null; + @Override public void onReceive(final Context context, Intent intent) { @@ -75,6 +77,7 @@ public class WidgetReceiver extends BroadcastReceiver WidgetUpdater widgetUpdater = app.getComponent().getWidgetUpdater(); Log.i(TAG, String.format("Received intent: %s", intent.toString())); + lastReceivedIntent = intent; try { @@ -112,6 +115,7 @@ public class WidgetReceiver extends BroadcastReceiver controller.onRemoveRepetition(data.getHabit(), data.getTimestamp()); break; + case ACTION_SET_NUMERICAL_VALUE: context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); Intent numberSelectorIntent = new Intent(context, NumericalCheckmarkWidgetActivity.class); @@ -121,6 +125,7 @@ public class WidgetReceiver extends BroadcastReceiver parser.copyIntentData(intent,numberSelectorIntent); context.startActivity(numberSelectorIntent); break; + case ACTION_UPDATE_WIDGETS_VALUE: widgetUpdater.updateWidgets(); widgetUpdater.scheduleStartDayWidgetUpdate(); @@ -139,4 +144,14 @@ public class WidgetReceiver extends BroadcastReceiver { WidgetBehavior getWidgetController(); } + + public static Intent getLastReceivedIntent() + { + return lastReceivedIntent; + } + + public static void clearLastReceivedIntent() + { + lastReceivedIntent = null; + } } diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/reminders/ReminderScheduler.java b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/reminders/ReminderScheduler.java index 3e170f25c..fcc562771 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/reminders/ReminderScheduler.java +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/reminders/ReminderScheduler.java @@ -162,10 +162,16 @@ public class ReminderScheduler implements CommandRunner.Listener public interface SystemScheduler { - void scheduleShowReminder(long reminderTime, Habit habit, long timestamp); + SchedulerResult scheduleShowReminder(long reminderTime, Habit habit, long timestamp); - void scheduleWidgetUpdate(long updateTime); + SchedulerResult scheduleWidgetUpdate(long updateTime); void log(String componentName, String msg); } + + public enum SchedulerResult + { + IGNORED, + OK + } }