Merge "AOSP/DeskClock - Add Kotlin for FormattedStringModel, PeriodicCallbackModel"
diff --git a/Android.bp b/Android.bp
index 1406d4d..68ec666 100644
--- a/Android.bp
+++ b/Android.bp
@@ -53,6 +53,8 @@
"src/**/deskclock/settings/*.java",
"src/**/deskclock/stopwatch/*.java",
"src/**/deskclock/timer/*.java",
+ "src/**/deskclock/uidata/FormattedStringModel.java",
+ "src/**/deskclock/uidata/PeriodicCallbackModel.java",
],
product_specific: true,
static_libs: [
diff --git a/src/com/android/deskclock/AlarmClockFragment.java b/src/com/android/deskclock/AlarmClockFragment.java
index 5c02d03..fb2078f 100644
--- a/src/com/android/deskclock/AlarmClockFragment.java
+++ b/src/com/android/deskclock/AlarmClockFragment.java
@@ -196,7 +196,7 @@
// Schedule a runnable to update the "Today/Tomorrow" values displayed for non-repeating
// alarms when midnight passes.
- UiDataModel.getUiDataModel().addMidnightCallback(mMidnightUpdater, 100);
+ UiDataModel.getUiDataModel().addMidnightCallback(mMidnightUpdater);
// Check if another app asked us to create a blank new alarm.
final Intent intent = getActivity().getIntent();
diff --git a/src/com/android/deskclock/ClockFragment.java b/src/com/android/deskclock/ClockFragment.java
index db6cbcf..4bc43d4 100644
--- a/src/com/android/deskclock/ClockFragment.java
+++ b/src/com/android/deskclock/ClockFragment.java
@@ -135,7 +135,7 @@
}
// Schedule a runnable to update the date every quarter hour.
- UiDataModel.getUiDataModel().addQuarterHourCallback(mQuarterHourUpdater, 100);
+ UiDataModel.getUiDataModel().addQuarterHourCallback(mQuarterHourUpdater);
return fragmentView;
}
diff --git a/src/com/android/deskclock/Screensaver.java b/src/com/android/deskclock/Screensaver.java
index 427885e..52b8342 100644
--- a/src/com/android/deskclock/Screensaver.java
+++ b/src/com/android/deskclock/Screensaver.java
@@ -132,7 +132,7 @@
Utils.refreshAlarm(this, mContentView);
startPositionUpdater();
- UiDataModel.getUiDataModel().addMidnightCallback(mMidnightUpdater, 100);
+ UiDataModel.getUiDataModel().addMidnightCallback(mMidnightUpdater);
}
@Override
diff --git a/src/com/android/deskclock/ScreensaverActivity.java b/src/com/android/deskclock/ScreensaverActivity.java
index 656cfc7..49c6c42 100644
--- a/src/com/android/deskclock/ScreensaverActivity.java
+++ b/src/com/android/deskclock/ScreensaverActivity.java
@@ -163,7 +163,7 @@
Utils.refreshAlarm(ScreensaverActivity.this, mContentView);
startPositionUpdater();
- UiDataModel.getUiDataModel().addMidnightCallback(mMidnightUpdater, 100);
+ UiDataModel.getUiDataModel().addMidnightCallback(mMidnightUpdater);
final Intent intent = registerReceiver(null, new IntentFilter(ACTION_BATTERY_CHANGED));
final boolean pluggedIn = intent != null && intent.getIntExtra(EXTRA_PLUGGED, 0) != 0;
diff --git a/src/com/android/deskclock/uidata/FormattedStringModel.kt b/src/com/android/deskclock/uidata/FormattedStringModel.kt
new file mode 100644
index 0000000..f720299
--- /dev/null
+++ b/src/com/android/deskclock/uidata/FormattedStringModel.kt
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.uidata
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.util.ArrayMap
+import android.util.SparseArray
+
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.GregorianCalendar
+import java.util.Locale
+
+/**
+ * All formatted strings that are cached for performance are accessed via this model.
+ */
+internal class FormattedStringModel(context: Context) {
+ /** Clears data structures containing data that is locale-sensitive. */
+ private val mLocaleChangedReceiver: BroadcastReceiver = LocaleChangedReceiver()
+
+ /**
+ * Caches formatted numbers in the current locale padded with zeroes to requested lengths.
+ * The first level of the cache maps length to the second level of the cache.
+ * The second level of the cache maps an integer to a formatted String in the current locale.
+ */
+ private val mNumberFormatCache = SparseArray<SparseArray<String>>(3)
+
+ /** Single-character version of weekday names; e.g.: 'S', 'M', 'T', 'W', 'T', 'F', 'S' */
+ private var mShortWeekdayNames: MutableMap<Int, String>? = null
+
+ /** Full weekday names; e.g.: 'Sunday', 'Monday', 'Tuesday', etc. */
+ private var mLongWeekdayNames: MutableMap<Int, String>? = null
+
+ init {
+ // Clear caches affected by locale when locale changes.
+ val localeBroadcastFilter = IntentFilter(Intent.ACTION_LOCALE_CHANGED)
+ context.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter)
+ }
+
+ /**
+ * This method is intended to be used when formatting numbers occurs in a hotspot such as the
+ * update loop of a timer or stopwatch. It returns cached results when possible in order to
+ * provide speed and limit garbage to be collected by the virtual machine.
+ *
+ * @param value a positive integer to format as a String
+ * @return the `value` formatted as a String in the current locale
+ * @throws IllegalArgumentException if `value` is negative
+ */
+ fun getFormattedNumber(value: Int): String {
+ val length = if (value == 0) 1 else Math.log10(value.toDouble()).toInt() + 1
+ return getFormattedNumber(false, value, length)
+ }
+
+ /**
+ * This method is intended to be used when formatting numbers occurs in a hotspot such as the
+ * update loop of a timer or stopwatch. It returns cached results when possible in order to
+ * provide speed and limit garbage to be collected by the virtual machine.
+ *
+ * @param value a positive integer to format as a String
+ * @param length the length of the String; zeroes are padded to match this length
+ * @return the `value` formatted as a String in the current locale and padded to the
+ * requested `length`
+ * @throws IllegalArgumentException if `value` is negative
+ */
+ fun getFormattedNumber(value: Int, length: Int): String {
+ return getFormattedNumber(false, value, length)
+ }
+
+ /**
+ * This method is intended to be used when formatting numbers occurs in a hotspot such as the
+ * update loop of a timer or stopwatch. It returns cached results when possible in order to
+ * provide speed and limit garbage to be collected by the virtual machine.
+ *
+ * @param negative force a minus sign (-) onto the display, even if `value` is `0`
+ * @param value a positive integer to format as a String
+ * @param length the length of the String; zeroes are padded to match this length. If
+ * `negative` is `true` the return value will contain a minus sign and a total
+ * length of `length + 1`.
+ * @return the `value` formatted as a String in the current locale and padded to the
+ * requested `length`
+ * @throws IllegalArgumentException if `value` is negative
+ */
+ fun getFormattedNumber(negative: Boolean, value: Int, length: Int): String {
+ require(value >= 0) { "value may not be negative: $value" }
+
+ // Look up the value cache using the length; -ve and +ve values are cached separately.
+ val lengthCacheKey = if (negative) -length else length
+ var valueCache = mNumberFormatCache[lengthCacheKey]
+ if (valueCache == null) {
+ valueCache = SparseArray(Math.pow(10.0, length.toDouble()).toInt())
+ mNumberFormatCache.put(lengthCacheKey, valueCache)
+ }
+
+ // Look up the cached formatted value using the value.
+ var formatted = valueCache[value]
+ if (formatted == null) {
+ val sign = if (negative) "−" else ""
+ formatted = String.format(Locale.getDefault(), sign + "%0" + length + "d", value)
+ valueCache.put(value, formatted)
+ }
+
+ return formatted
+ }
+
+ /**
+ * @param calendarDay any of the following values
+ *
+ * * [Calendar.SUNDAY]
+ * * [Calendar.MONDAY]
+ * * [Calendar.TUESDAY]
+ * * [Calendar.WEDNESDAY]
+ * * [Calendar.THURSDAY]
+ * * [Calendar.FRIDAY]
+ * * [Calendar.SATURDAY]
+ *
+ * @return single-character weekday name; e.g.: 'S', 'M', 'T', 'W', 'T', 'F', 'S'
+ */
+ fun getShortWeekday(calendarDay: Int): String? {
+ if (mShortWeekdayNames == null) {
+ mShortWeekdayNames = ArrayMap(7)
+
+ val format = SimpleDateFormat("ccccc", Locale.getDefault())
+ for (i in Calendar.SUNDAY..Calendar.SATURDAY) {
+ val calendar: Calendar = GregorianCalendar(2014, Calendar.JULY, 20 + i - 1)
+ val weekday = format.format(calendar.time)
+ mShortWeekdayNames!![i] = weekday
+ }
+ }
+
+ return mShortWeekdayNames!![calendarDay]
+ }
+
+ /**
+ * @param calendarDay any of the following values
+ *
+ * * [Calendar.SUNDAY]
+ * * [Calendar.MONDAY]
+ * * [Calendar.TUESDAY]
+ * * [Calendar.WEDNESDAY]
+ * * [Calendar.THURSDAY]
+ * * [Calendar.FRIDAY]
+ * * [Calendar.SATURDAY]
+ *
+ * @return full weekday name; e.g.: 'Sunday', 'Monday', 'Tuesday', etc.
+ */
+ fun getLongWeekday(calendarDay: Int): String? {
+ if (mLongWeekdayNames == null) {
+ mLongWeekdayNames = ArrayMap(7)
+
+ val calendar: Calendar = GregorianCalendar(2014, Calendar.JULY, 20)
+ val format = SimpleDateFormat("EEEE", Locale.getDefault())
+ for (i in Calendar.SUNDAY..Calendar.SATURDAY) {
+ val weekday = format.format(calendar.time)
+ mLongWeekdayNames!![i] = weekday
+ calendar.add(Calendar.DAY_OF_YEAR, 1)
+ }
+ }
+
+ return mLongWeekdayNames!![calendarDay]
+ }
+
+ /**
+ * Cached information that is locale-sensitive must be cleared in response to locale changes.
+ */
+ private inner class LocaleChangedReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ mNumberFormatCache.clear()
+ mShortWeekdayNames = null
+ mLongWeekdayNames = null
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/uidata/PeriodicCallbackModel.java b/src/com/android/deskclock/uidata/PeriodicCallbackModel.java
index 4b522d1..b139e7f 100644
--- a/src/com/android/deskclock/uidata/PeriodicCallbackModel.java
+++ b/src/com/android/deskclock/uidata/PeriodicCallbackModel.java
@@ -81,26 +81,29 @@
/**
* @param runnable to be called every quarter-hour
- * @param offset an offset applied to the quarter-hour to control when the callback occurs
*/
- void addQuarterHourCallback(Runnable runnable, long offset) {
- addPeriodicCallback(runnable, Period.QUARTER_HOUR, offset);
+ void addQuarterHourCallback(Runnable runnable) {
+ // Callbacks *can* occur early so pad in an extra 100ms on the quarter-hour callback
+ // to ensure the sampled wallclock time reflects the subsequent quarter-hour.
+ addPeriodicCallback(runnable, Period.QUARTER_HOUR, 100L);
}
/**
* @param runnable to be called every hour
- * @param offset an offset applied to the hour to control when the callback occurs
*/
- void addHourCallback(Runnable runnable, long offset) {
- addPeriodicCallback(runnable, Period.HOUR, offset);
+ void addHourCallback(Runnable runnable) {
+ // Callbacks *can* occur early so pad in an extra 100ms on the hour callback to ensure
+ // the sampled wallclock time reflects the subsequent hour.
+ addPeriodicCallback(runnable, Period.HOUR, 100L);
}
/**
* @param runnable to be called every midnight
- * @param offset an offset applied to the midnight to control when the callback occurs
*/
- void addMidnightCallback(Runnable runnable, long offset) {
- addPeriodicCallback(runnable, Period.MIDNIGHT, offset);
+ void addMidnightCallback(Runnable runnable) {
+ // Callbacks *can* occur early so pad in an extra 100ms on the midnight callback to ensure
+ // the sampled wallclock time reflects the subsequent day.
+ addPeriodicCallback(runnable, Period.MIDNIGHT, 100L);
}
/**
diff --git a/src/com/android/deskclock/uidata/PeriodicCallbackModel.kt b/src/com/android/deskclock/uidata/PeriodicCallbackModel.kt
new file mode 100644
index 0000000..224344e
--- /dev/null
+++ b/src/com/android/deskclock/uidata/PeriodicCallbackModel.kt
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.uidata
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.Handler
+import android.text.format.DateUtils
+import androidx.annotation.VisibleForTesting
+
+import com.android.deskclock.LogUtils
+import com.android.deskclock.Utils
+
+import java.util.concurrent.CopyOnWriteArrayList
+import java.util.Calendar
+
+/**
+ * All callbacks to be delivered at requested times on the main thread if the application is in the
+ * foreground when the callback time passes.
+ */
+internal class PeriodicCallbackModel(context: Context) {
+
+ @VisibleForTesting
+ internal enum class Period {
+ MINUTE, QUARTER_HOUR, HOUR, MIDNIGHT
+ }
+
+ /** Reschedules callbacks when the device time changes. */
+ private val mTimeChangedReceiver: BroadcastReceiver = TimeChangedReceiver()
+
+ private val mPeriodicRunnables: MutableList<PeriodicRunnable> = CopyOnWriteArrayList()
+
+ init {
+ // Reschedules callbacks when the device time changes.
+ val timeChangedBroadcastFilter = IntentFilter()
+ timeChangedBroadcastFilter.addAction(Intent.ACTION_TIME_CHANGED)
+ timeChangedBroadcastFilter.addAction(Intent.ACTION_DATE_CHANGED)
+ timeChangedBroadcastFilter.addAction(Intent.ACTION_TIMEZONE_CHANGED)
+ context.registerReceiver(mTimeChangedReceiver, timeChangedBroadcastFilter)
+ }
+
+ /**
+ * @param runnable to be called every minute
+ * @param offset an offset applied to the minute to control when the callback occurs
+ */
+ fun addMinuteCallback(runnable: Runnable, offset: Long) {
+ addPeriodicCallback(runnable, Period.MINUTE, offset)
+ }
+
+ /**
+ * @param runnable to be called every quarter-hour
+ */
+ fun addQuarterHourCallback(runnable: Runnable) {
+ // Callbacks *can* occur early so pad in an extra 100ms on the quarter-hour callback
+ // to ensure the sampled wallclock time reflects the subsequent quarter-hour.
+ addPeriodicCallback(runnable, Period.QUARTER_HOUR, 100L)
+ }
+
+ /**
+ * @param runnable to be called every hour
+ */
+ fun addHourCallback(runnable: Runnable) {
+ // Callbacks *can* occur early so pad in an extra 100ms on the hour callback to ensure
+ // the sampled wallclock time reflects the subsequent hour.
+ addPeriodicCallback(runnable, Period.HOUR, 100L)
+ }
+
+ /**
+ * @param runnable to be called every midnight
+ */
+ fun addMidnightCallback(runnable: Runnable) {
+ // Callbacks *can* occur early so pad in an extra 100ms on the midnight callback to ensure
+ // the sampled wallclock time reflects the subsequent day.
+ addPeriodicCallback(runnable, Period.MIDNIGHT, 100L)
+ }
+
+ /**
+ * @param runnable to be called periodically
+ */
+ private fun addPeriodicCallback(runnable: Runnable, period: Period, offset: Long) {
+ val periodicRunnable = PeriodicRunnable(runnable, period, offset)
+ mPeriodicRunnables.add(periodicRunnable)
+ periodicRunnable.schedule()
+ }
+
+ /**
+ * @param runnable to no longer be called periodically
+ */
+ fun removePeriodicCallback(runnable: Runnable) {
+ for (periodicRunnable in mPeriodicRunnables) {
+ if (periodicRunnable.mDelegate === runnable) {
+ periodicRunnable.unSchedule()
+ mPeriodicRunnables.remove(periodicRunnable)
+ return
+ }
+ }
+ }
+
+ /**
+ * Schedules the execution of the given delegate Runnable at the next callback time.
+ */
+ private class PeriodicRunnable(
+ val mDelegate: Runnable,
+ private val mPeriod: Period,
+ private val mOffset: Long
+ ) : Runnable {
+ override fun run() {
+ LOGGER.i("Executing periodic callback for %s because the period ended", mPeriod)
+ mDelegate.run()
+ schedule()
+ }
+
+ fun runAndReschedule() {
+ LOGGER.i("Executing periodic callback for %s because the time changed", mPeriod)
+ unSchedule()
+ mDelegate.run()
+ schedule()
+ }
+
+ fun schedule() {
+ val delay = getDelay(System.currentTimeMillis(), mPeriod, mOffset)
+ handler.postDelayed(this, delay)
+ }
+
+ fun unSchedule() {
+ handler.removeCallbacks(this)
+ }
+ }
+
+ /**
+ * Reschedules callbacks when the device time changes.
+ */
+ private inner class TimeChangedReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ for (periodicRunnable in mPeriodicRunnables) {
+ periodicRunnable.runAndReschedule()
+ }
+ }
+ }
+
+ companion object {
+ private val LOGGER = LogUtils.Logger("Periodic")
+
+ private const val QUARTER_HOUR_IN_MILLIS = 15 * DateUtils.MINUTE_IN_MILLIS
+
+ private var sHandler: Handler? = null
+
+ /**
+ * Return the delay until the given `period` elapses adjusted by the given `offset`.
+ *
+ * @param now the current time
+ * @param period the frequency with which callbacks should be given
+ * @param offset an offset to add to the normal period; allows the callback to
+ * be made relative to the normally scheduled period end
+ * @return the time delay from `now` to schedule the callback
+ */
+ @VisibleForTesting
+ @JvmStatic
+ fun getDelay(now: Long, period: Period, offset: Long): Long {
+ val periodStart = now - offset
+
+ return when (period) {
+ Period.MINUTE -> {
+ val lastMinute = periodStart - periodStart % DateUtils.MINUTE_IN_MILLIS
+ val nextMinute = lastMinute + DateUtils.MINUTE_IN_MILLIS
+ nextMinute - now + offset
+ }
+ Period.QUARTER_HOUR -> {
+ val lastQuarterHour = periodStart - periodStart % QUARTER_HOUR_IN_MILLIS
+ val nextQuarterHour = lastQuarterHour + QUARTER_HOUR_IN_MILLIS
+ nextQuarterHour - now + offset
+ }
+ Period.HOUR -> {
+ val lastHour = periodStart - periodStart % DateUtils.HOUR_IN_MILLIS
+ val nextHour = lastHour + DateUtils.HOUR_IN_MILLIS
+ nextHour - now + offset
+ }
+ Period.MIDNIGHT -> {
+ val nextMidnight = Calendar.getInstance()
+ nextMidnight.timeInMillis = periodStart
+ nextMidnight.add(Calendar.DATE, 1)
+ nextMidnight[Calendar.HOUR_OF_DAY] = 0
+ nextMidnight[Calendar.MINUTE] = 0
+ nextMidnight[Calendar.SECOND] = 0
+ nextMidnight[Calendar.MILLISECOND] = 0
+ nextMidnight.timeInMillis - now + offset
+ }
+ }
+ }
+
+ // TODO(b/157255731) Replace deprecated Handler call here
+ private val handler: Handler
+ get() {
+ Utils.enforceMainLooper()
+ if (sHandler == null) {
+ sHandler = Handler()
+ }
+ return sHandler!!
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/uidata/UiDataModel.java b/src/com/android/deskclock/uidata/UiDataModel.java
index f7a6917..eb072bd 100644
--- a/src/com/android/deskclock/uidata/UiDataModel.java
+++ b/src/com/android/deskclock/uidata/UiDataModel.java
@@ -338,29 +338,26 @@
/**
* @param runnable to be called every quarter-hour
- * @param offset an offset applied to the quarter-hour to control when the callback occurs
*/
- public void addQuarterHourCallback(Runnable runnable, long offset) {
+ public void addQuarterHourCallback(Runnable runnable) {
enforceMainLooper();
- mPeriodicCallbackModel.addQuarterHourCallback(runnable, offset);
+ mPeriodicCallbackModel.addQuarterHourCallback(runnable);
}
/**
* @param runnable to be called every hour
- * @param offset an offset applied to the hour to control when the callback occurs
*/
- public void addHourCallback(Runnable runnable, long offset) {
+ public void addHourCallback(Runnable runnable) {
enforceMainLooper();
- mPeriodicCallbackModel.addHourCallback(runnable, offset);
+ mPeriodicCallbackModel.addHourCallback(runnable);
}
/**
* @param runnable to be called every midnight
- * @param offset an offset applied to the midnight to control when the callback occurs
*/
- public void addMidnightCallback(Runnable runnable, long offset) {
+ public void addMidnightCallback(Runnable runnable) {
enforceMainLooper();
- mPeriodicCallbackModel.addMidnightCallback(runnable, offset);
+ mPeriodicCallbackModel.addMidnightCallback(runnable);
}
/**
diff --git a/tests/src/com/android/deskclock/uidata/FormattedStringModelTest.java b/tests/src/com/android/deskclock/uidata/FormattedStringModelTest.java
new file mode 100644
index 0000000..9893545
--- /dev/null
+++ b/tests/src/com/android/deskclock/uidata/FormattedStringModelTest.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.uidata;
+
+import android.content.Context;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+
+@RunWith(AndroidJUnit4ClassRunner.class)
+public class FormattedStringModelTest {
+
+ private FormattedStringModel model;
+
+ @Before
+ public void setUp() {
+ Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ model = new FormattedStringModel(context);
+ }
+
+ @After
+ public void tearDown() {
+ model = null;
+ }
+
+ @Test
+ public void positiveFormattedNumberWithNoPadding() {
+ assertEquals("0", model.getFormattedNumber(0));
+ assertEquals("9", model.getFormattedNumber(9));
+ assertEquals("10", model.getFormattedNumber(10));
+ assertEquals("99", model.getFormattedNumber(99));
+ assertEquals("100", model.getFormattedNumber(100));
+ }
+
+ @Test
+ public void positiveFormattedNumber() {
+ assertEquals("0", model.getFormattedNumber(false, 0, 1));
+ assertEquals("00", model.getFormattedNumber(false, 0, 2));
+ assertEquals("000", model.getFormattedNumber(false, 0, 3));
+
+ assertEquals("9", model.getFormattedNumber(false, 9, 1));
+ assertEquals("09", model.getFormattedNumber(false, 9, 2));
+ assertEquals("009", model.getFormattedNumber(false, 9, 3));
+
+ assertEquals("90", model.getFormattedNumber(false, 90, 2));
+ assertEquals("090", model.getFormattedNumber(false, 90, 3));
+
+ assertEquals("999", model.getFormattedNumber(false, 999, 3));
+ }
+
+ @Test
+ public void negativeFormattedNumber() {
+ assertEquals("−0", model.getFormattedNumber(true, 0, 1));
+ assertEquals("−00", model.getFormattedNumber(true, 0, 2));
+ assertEquals("−000", model.getFormattedNumber(true, 0, 3));
+
+ assertEquals("−9", model.getFormattedNumber(true, 9, 1));
+ assertEquals("−09", model.getFormattedNumber(true, 9, 2));
+ assertEquals("−009", model.getFormattedNumber(true, 9, 3));
+
+ assertEquals("−90", model.getFormattedNumber(true, 90, 2));
+ assertEquals("−090", model.getFormattedNumber(true, 90, 3));
+
+ assertEquals("−999", model.getFormattedNumber(true, 999, 3));
+ }
+}
diff --git a/tests/src/com/android/deskclock/uidata/PeriodicCallbackModelTest.java b/tests/src/com/android/deskclock/uidata/PeriodicCallbackModelTest.java
new file mode 100644
index 0000000..3df7404
--- /dev/null
+++ b/tests/src/com/android/deskclock/uidata/PeriodicCallbackModelTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.uidata;
+
+import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Calendar;
+
+import static com.android.deskclock.uidata.PeriodicCallbackModel.Period.HOUR;
+import static com.android.deskclock.uidata.PeriodicCallbackModel.Period.MIDNIGHT;
+import static com.android.deskclock.uidata.PeriodicCallbackModel.Period.MINUTE;
+import static com.android.deskclock.uidata.PeriodicCallbackModel.Period.QUARTER_HOUR;
+
+import static java.util.Calendar.MILLISECOND;
+import static org.junit.Assert.assertEquals;
+
+@RunWith(AndroidJUnit4ClassRunner.class)
+public class PeriodicCallbackModelTest {
+
+ @Test
+ public void getMinuteDelay() {
+ assertEquals(1, PeriodicCallbackModel.getDelay(56999, MINUTE, -3000));
+ assertEquals(60000, PeriodicCallbackModel.getDelay(57000, MINUTE, -3000));
+ assertEquals(59999, PeriodicCallbackModel.getDelay(57001, MINUTE, -3000));
+
+ assertEquals(1, PeriodicCallbackModel.getDelay(59999, MINUTE, 0));
+ assertEquals(60000, PeriodicCallbackModel.getDelay(60000, MINUTE, 0));
+ assertEquals(59999, PeriodicCallbackModel.getDelay(60001, MINUTE, 0));
+
+ assertEquals(3001, PeriodicCallbackModel.getDelay(59999, MINUTE, 3000));
+ assertEquals(3000, PeriodicCallbackModel.getDelay(60000, MINUTE, 3000));
+ assertEquals(1, PeriodicCallbackModel.getDelay(62999, MINUTE, 3000));
+ assertEquals(60000, PeriodicCallbackModel.getDelay(63000, MINUTE, 3000));
+ assertEquals(59999, PeriodicCallbackModel.getDelay(63001, MINUTE, 3000));
+ }
+
+ @Test
+ public void getQuarterHourDelay() {
+ assertEquals(1, PeriodicCallbackModel.getDelay(896999, QUARTER_HOUR, -3000));
+ assertEquals(900000, PeriodicCallbackModel.getDelay(897000, QUARTER_HOUR, -3000));
+ assertEquals(899999, PeriodicCallbackModel.getDelay(897001, QUARTER_HOUR, -3000));
+
+ assertEquals(1, PeriodicCallbackModel.getDelay(899999, QUARTER_HOUR, 0));
+ assertEquals(900000, PeriodicCallbackModel.getDelay(900000, QUARTER_HOUR, 0));
+ assertEquals(899999, PeriodicCallbackModel.getDelay(900001, QUARTER_HOUR, 0));
+
+ assertEquals(3001, PeriodicCallbackModel.getDelay(899999, QUARTER_HOUR, 3000));
+ assertEquals(3000, PeriodicCallbackModel.getDelay(900000, QUARTER_HOUR, 3000));
+ assertEquals(1, PeriodicCallbackModel.getDelay(902999, QUARTER_HOUR, 3000));
+ assertEquals(900000, PeriodicCallbackModel.getDelay(903000, QUARTER_HOUR, 3000));
+ assertEquals(899999, PeriodicCallbackModel.getDelay(903001, QUARTER_HOUR, 3000));
+ }
+
+ @Test
+ public void getHourDelay() {
+ assertEquals(1, PeriodicCallbackModel.getDelay(3596999, HOUR, -3000));
+ assertEquals(3600000, PeriodicCallbackModel.getDelay(3597000, HOUR, -3000));
+ assertEquals(3599999, PeriodicCallbackModel.getDelay(3597001, HOUR, -3000));
+
+ assertEquals(1, PeriodicCallbackModel.getDelay(3599999, HOUR, 0));
+ assertEquals(3600000, PeriodicCallbackModel.getDelay(3600000, HOUR, 0));
+ assertEquals(3599999, PeriodicCallbackModel.getDelay(3600001, HOUR, 0));
+
+ assertEquals(3001, PeriodicCallbackModel.getDelay(3599999, HOUR, 3000));
+ assertEquals(3000, PeriodicCallbackModel.getDelay(3600000, HOUR, 3000));
+ assertEquals(1, PeriodicCallbackModel.getDelay(3602999, HOUR, 3000));
+ assertEquals(3600000, PeriodicCallbackModel.getDelay(3603000, HOUR, 3000));
+ assertEquals(3599999, PeriodicCallbackModel.getDelay(3603001, HOUR, 3000));
+ }
+
+ @Test
+ public void getMidnightDelay() {
+ final Calendar c = Calendar.getInstance();
+ c.set(2016, 0, 20, 0, 0, 0);
+ c.set(MILLISECOND, 0);
+ final long now = c.getTimeInMillis();
+
+ assertEquals(1, PeriodicCallbackModel.getDelay(now - 3001, MIDNIGHT, -3000));
+ assertEquals(86400000, PeriodicCallbackModel.getDelay(now - 3000, MIDNIGHT, -3000));
+ assertEquals(86399999, PeriodicCallbackModel.getDelay(now - 2999, MIDNIGHT, -3000));
+
+ assertEquals(1, PeriodicCallbackModel.getDelay(now - 1, MIDNIGHT, 0));
+ assertEquals(86400000, PeriodicCallbackModel.getDelay(now, MIDNIGHT, 0));
+ assertEquals(86399999, PeriodicCallbackModel.getDelay(now + 1, MIDNIGHT, 0));
+
+ assertEquals(3001, PeriodicCallbackModel.getDelay(now - 1, MIDNIGHT, 3000));
+ assertEquals(3000, PeriodicCallbackModel.getDelay(now, MIDNIGHT, 3000));
+ assertEquals(1, PeriodicCallbackModel.getDelay(now + 2999, MIDNIGHT, 3000));
+ assertEquals(86400000, PeriodicCallbackModel.getDelay(now + 3000, MIDNIGHT, 3000));
+ assertEquals(86399999, PeriodicCallbackModel.getDelay(now + 3001, MIDNIGHT, 3000));
+ }
+}