Support battery usage chart start index label is the detailed last full charge time
https://screenshot.googleplex.com/5dYp7kJpNPMU6Jx.png
https://screenshot.googleplex.com/7ZSbWyCGRFSym6A.png
https://screenshot.googleplex.com/9GsPTa9zd2XttDX.png
Bug: 271214926
Fix: 271214926
Test: manual
Change-Id: Ic869653f66866964f34e111dd2ee8c052313e119
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java
index e9d10f2..67d3969 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java
@@ -251,8 +251,7 @@
hourlyBatteryLevelsPerDay.getLevels(),
hourlyBatteryLevelsPerDay.getTimestamps(),
BatteryChartViewModel.AxisLabelPosition.BETWEEN_TRAPEZOIDS,
- mHourlyChartLabelTextGenerator.setLatestTimestamp(getLast(getLast(
- batteryLevelData.getHourlyBatteryLevelsPerDay()).getTimestamps()))));
+ mHourlyChartLabelTextGenerator.updateSpecialCaseContext(batteryLevelData)));
}
refreshUi();
}
@@ -640,7 +639,11 @@
private final class HourlyChartLabelTextGenerator implements
BatteryChartViewModel.LabelTextGenerator {
- private Long mLatestTimestamp;
+ private static final int FULL_CHARGE_BATTERY_LEVEL = 100;
+
+ private boolean mIsFromFullCharge;
+ private long mFistTimestamp;
+ private long mLatestTimestamp;
@Override
public String generateText(List<Long> timestamps, int index) {
@@ -648,24 +651,37 @@
// Replaces the latest timestamp text to "now".
return mContext.getString(R.string.battery_usage_chart_label_now);
}
- return ConvertUtils.utcToLocalTimeHour(mContext, timestamps.get(index),
- mIs24HourFormat);
+ long timestamp = timestamps.get(index);
+ boolean showMinute = false;
+ if (Objects.equal(timestamp, mFistTimestamp)) {
+ if (mIsFromFullCharge) {
+ showMinute = true;
+ } else {
+ // starts from 7 days ago
+ timestamp = TimestampUtils.getLastEvenHourTimestamp(timestamp);
+ }
+ }
+ return ConvertUtils.utcToLocalTimeHour(
+ mContext, timestamp, mIs24HourFormat, showMinute);
}
@Override
public String generateFullText(List<Long> timestamps, int index) {
- if (Objects.equal(timestamps.get(index), mLatestTimestamp)) {
- // Replaces the latest timestamp text to "now".
- return mContext.getString(R.string.battery_usage_chart_label_now);
- }
return index == timestamps.size() - 1
? generateText(timestamps, index)
: mContext.getString(R.string.battery_usage_timestamps_hyphen,
generateText(timestamps, index), generateText(timestamps, index + 1));
}
- public HourlyChartLabelTextGenerator setLatestTimestamp(Long latestTimestamp) {
- this.mLatestTimestamp = latestTimestamp;
+ HourlyChartLabelTextGenerator updateSpecialCaseContext(
+ @NonNull final BatteryLevelData batteryLevelData) {
+ BatteryLevelData.PeriodBatteryLevelData firstDayLevelData =
+ batteryLevelData.getHourlyBatteryLevelsPerDay().get(0);
+ this.mIsFromFullCharge =
+ firstDayLevelData.getLevels().get(0) == FULL_CHARGE_BATTERY_LEVEL;
+ this.mFistTimestamp = firstDayLevelData.getTimestamps().get(0);
+ this.mLatestTimestamp = getLast(getLast(
+ batteryLevelData.getHourlyBatteryLevelsPerDay()).getTimestamps());
return this;
}
}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java b/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java
index c6c548f..2c98c4b 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java
@@ -280,12 +280,12 @@
}
/** Converts UTC timestamp to local time hour data. */
- public static String utcToLocalTimeHour(
- final Context context, final long timestamp, final boolean is24HourFormat) {
+ public static String utcToLocalTimeHour(final Context context, final long timestamp,
+ final boolean is24HourFormat, final boolean showMinute) {
final Locale locale = getLocale(context);
// e.g. for 12-hour format: 9 PM
// e.g. for 24-hour format: 09:00
- final String skeleton = is24HourFormat ? "HHm" : "ha";
+ final String skeleton = is24HourFormat ? "HHm" : (showMinute ? "hma" : "ha");
final String pattern = DateFormat.getBestDateTimePattern(locale, skeleton);
return DateFormat.format(pattern, timestamp).toString();
}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java b/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java
index 24571cd..9aee2f7 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java
@@ -285,7 +285,6 @@
final Map<Integer, Map<Integer, Map<Long, Map<String, List<AppUsagePeriod>>>>> resultMap =
new ArrayMap<>();
- final long dailySize = hourlyBatteryLevelsPerDay.size();
for (int dailyIndex = 0; dailyIndex < hourlyBatteryLevelsPerDay.size(); dailyIndex++) {
final Map<Integer, Map<Long, Map<String, List<AppUsagePeriod>>>> dailyMap =
new ArrayMap<>();
@@ -294,21 +293,9 @@
continue;
}
final List<Long> timestamps = hourlyBatteryLevelsPerDay.get(dailyIndex).getTimestamps();
- final long hourlySize = timestamps.size() - 1;
for (int hourlyIndex = 0; hourlyIndex < timestamps.size() - 1; hourlyIndex++) {
- // The start slot timestape is the near hour timestamp instead of the last full
- // charge time. So use rawStartTimestamp instead of reading the timestamp from
- // hourlyBatteryLevelsPerDay here.
- final long startTimestamp =
- dailyIndex == 0 && hourlyIndex == 0 && !sDebug
- ? rawStartTimestamp : timestamps.get(hourlyIndex);
- // The final slot is to show the data from last even hour until now but the
- // timestamp in hourlyBatteryLevelsPerDay is not the real value. So use current
- // timestamp instead of reading the timestamp from hourlyBatteryLevelsPerDay here.
- final long endTimestamp =
- dailyIndex == dailySize - 1 && hourlyIndex == hourlySize - 1 && !sDebug
- ? System.currentTimeMillis() : timestamps.get(hourlyIndex + 1);
-
+ final long startTimestamp = timestamps.get(hourlyIndex);
+ final long endTimestamp = timestamps.get(hourlyIndex + 1);
// Gets the app usage event list for this hourly slot first.
final List<AppUsageEvent> hourlyAppUsageEventList =
getAppUsageEventListWithinTimeRangeWithBuffer(
@@ -463,11 +450,8 @@
Collections.sort(rawTimestampList);
final long currentTime = getCurrentTimeMillis();
final List<Long> expectedTimestampList = getTimestampSlots(rawTimestampList, currentTime);
- final boolean isFromFullCharge =
- isFromFullCharge(batteryHistoryMap.get(rawTimestampList.get(0)));
interpolateHistory(
- context, rawTimestampList, expectedTimestampList, currentTime, isFromFullCharge,
- batteryHistoryMap, resultMap);
+ context, rawTimestampList, expectedTimestampList, batteryHistoryMap, resultMap);
Log.d(TAG, String.format("getHistoryMapWithExpectedTimestamps() size=%d in %d/ms",
resultMap.size(), (System.currentTimeMillis() - startTime)));
return resultMap;
@@ -496,8 +480,9 @@
}
/**
- * Computes expected timestamp slots for last full charge, which will return hourly timestamps
- * between start and end two even hour values.
+ * Computes expected timestamp slots. The start timestamp is the last full charge time.
+ * The end timestamp is current time. The middle timestamps are the sharp hour timestamps
+ * between the start and end timestamps.
*/
@VisibleForTesting
static List<Long> getTimestampSlots(final List<Long> rawTimestampList, final long currentTime) {
@@ -505,19 +490,18 @@
if (rawTimestampList.isEmpty()) {
return timestampSlots;
}
- final long rawStartTimestamp = rawTimestampList.get(0);
- // No matter the start is from last full charge or 6 days ago, use the nearest even hour.
- final long startTimestamp = getNearestEvenHourTimestamp(rawStartTimestamp);
- // Use the first even hour after the current time as the end.
- final long endTimestamp = getFirstEvenHourAfterTimestamp(currentTime);
+ final long startTimestamp = rawTimestampList.get(0);
+ final long endTimestamp = currentTime;
// If the start timestamp is later or equal the end one, return the empty list.
if (startTimestamp >= endTimestamp) {
return timestampSlots;
}
- for (long timestamp = startTimestamp; timestamp <= endTimestamp;
- timestamp += DateUtils.HOUR_IN_MILLIS) {
+ timestampSlots.add(startTimestamp);
+ for (long timestamp = TimestampUtils.getNextHourTimestamp(startTimestamp);
+ timestamp < endTimestamp; timestamp += DateUtils.HOUR_IN_MILLIS) {
timestampSlots.add(timestamp);
}
+ timestampSlots.add(endTimestamp);
return timestampSlots;
}
@@ -539,35 +523,39 @@
}
final long startTime = timestampList.get(0);
final long endTime = timestampList.get(timestampList.size() - 1);
- // If the timestamp diff is smaller than MIN_TIME_SLOT, returns the empty list directly.
- if (endTime - startTime < MIN_TIME_SLOT) {
- return dailyTimestampList;
+ for (long timestamp = startTime; timestamp < endTime;
+ timestamp = TimestampUtils.getNextDayTimestamp(timestamp)) {
+ dailyTimestampList.add(timestamp);
}
- long nextDay = getTimestampOfNextDay(startTime);
- // Only if the timestamp diff in the first day is bigger than MIN_TIME_SLOT, start from the
- // first day. Otherwise, start from the second day.
- if (nextDay - startTime >= MIN_TIME_SLOT) {
- dailyTimestampList.add(startTime);
- }
- while (nextDay < endTime) {
- dailyTimestampList.add(nextDay);
- nextDay = getTimestampOfNextDay(nextDay);
- }
- final long lastDailyTimestamp = dailyTimestampList.get(dailyTimestampList.size() - 1);
- // Only if the timestamp diff in the last day is bigger than MIN_TIME_SLOT, add the
- // last day.
- if (endTime - lastDailyTimestamp >= MIN_TIME_SLOT) {
- dailyTimestampList.add(endTime);
- }
- // The dailyTimestampList must have the start and end timestamp, otherwise, return an empty
- // list.
- if (dailyTimestampList.size() < MIN_TIMESTAMP_DATA_SIZE) {
- return new ArrayList<>();
- }
+ dailyTimestampList.add(endTime);
return dailyTimestampList;
}
@VisibleForTesting
+ static List<List<Long>> getHourlyTimestamps(final List<Long> dailyTimestamps) {
+ final List<List<Long>> hourlyTimestamps = new ArrayList<>();
+ if (dailyTimestamps.size() < MIN_DAILY_DATA_SIZE) {
+ return hourlyTimestamps;
+ }
+
+ for (int dailyIndex = 0; dailyIndex < dailyTimestamps.size() - 1; dailyIndex++) {
+ final List<Long> hourlyTimestampsPerDay = new ArrayList<>();
+ final long startTime = dailyTimestamps.get(dailyIndex);
+ final long endTime = dailyTimestamps.get(dailyIndex + 1);
+
+ hourlyTimestampsPerDay.add(startTime);
+ for (long timestamp = TimestampUtils.getNextEvenHourTimestamp(startTime);
+ timestamp < endTime; timestamp += MIN_TIME_SLOT) {
+ hourlyTimestampsPerDay.add(timestamp);
+ }
+ hourlyTimestampsPerDay.add(endTime);
+
+ hourlyTimestamps.add(hourlyTimestampsPerDay);
+ }
+ return hourlyTimestamps;
+ }
+
+ @VisibleForTesting
static boolean isFromFullCharge(@Nullable final Map<String, BatteryHistEntry> entryList) {
if (entryList == null) {
Log.d(TAG, "entryList is null in isFromFullCharge()");
@@ -603,30 +591,6 @@
}
/**
- * @return Returns the timestamp for 00:00 1 day after the given timestamp based on local
- * timezone.
- */
- @VisibleForTesting
- static long getTimestampOfNextDay(long timestamp) {
- return getTimestampWithDayDiff(timestamp, /*dayDiff=*/ 1);
- }
-
- /**
- * Returns whether currentSlot will be used in daily chart.
- */
- @VisibleForTesting
- static boolean isForDailyChart(final boolean isStartOrEnd, final long currentSlot) {
- // The start and end timestamps will always be used in daily chart.
- if (isStartOrEnd) {
- return true;
- }
-
- // The timestamps for 00:00 will be used in daily chart.
- final long startOfTheDay = getTimestampWithDayDiff(currentSlot, /*dayDiff=*/ 0);
- return currentSlot == startOfTheDay;
- }
-
- /**
* @return Returns the indexed battery usage data for each corresponding time slot.
*
* <p>There could be 2 cases of the returned value:</p>
@@ -1184,43 +1148,22 @@
Context context,
final List<Long> rawTimestampList,
final List<Long> expectedTimestampSlots,
- final long currentTime,
- final boolean isFromFullCharge,
final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap,
final Map<Long, Map<String, BatteryHistEntry>> resultMap) {
if (rawTimestampList.isEmpty() || expectedTimestampSlots.isEmpty()) {
return;
}
- final long expectedStartTimestamp = expectedTimestampSlots.get(0);
- final long rawStartTimestamp = rawTimestampList.get(0);
- int startIndex = 0;
- // If the expected start timestamp is full charge or earlier than what we have, use the
- // first data of what we have directly. This should be OK because the expected start
- // timestamp is the nearest even hour of the raw start timestamp, their time diff is no
- // more than 1 hour.
- if (isFromFullCharge || expectedStartTimestamp < rawStartTimestamp) {
- startIndex = 1;
- resultMap.put(expectedStartTimestamp, batteryHistoryMap.get(rawStartTimestamp));
- }
final int expectedTimestampSlotsSize = expectedTimestampSlots.size();
- for (int index = startIndex; index < expectedTimestampSlotsSize; index++) {
- final long currentSlot = expectedTimestampSlots.get(index);
- if (currentSlot > currentTime) {
- // The slot timestamp is greater than the current time. Puts a placeholder first,
- // then in the async task, loads the real time battery usage data from the battery
- // stats service.
- // If current time is odd hour, one placeholder is added. If the current hour is
- // even hour, two placeholders are added. This is because the method
- // insertHourlyUsageDiffDataPerSlot() requires continuing three hours data.
- resultMap.put(currentSlot,
- Map.of(CURRENT_TIME_BATTERY_HISTORY_PLACEHOLDER, EMPTY_BATTERY_HIST_ENTRY));
- continue;
- }
- final boolean isStartOrEnd = index == 0 || index == expectedTimestampSlotsSize - 1;
- interpolateHistoryForSlot(
- context, currentSlot, rawTimestampList, batteryHistoryMap, resultMap,
- isStartOrEnd);
+ final long startTimestamp = expectedTimestampSlots.get(0);
+ final long endTimestamp = expectedTimestampSlots.get(expectedTimestampSlotsSize - 1);
+
+ resultMap.put(startTimestamp, batteryHistoryMap.get(startTimestamp));
+ for (int index = 1; index < expectedTimestampSlotsSize - 1; index++) {
+ interpolateHistoryForSlot(context, expectedTimestampSlots.get(index), rawTimestampList,
+ batteryHistoryMap, resultMap);
}
+ resultMap.put(endTimestamp,
+ Map.of(CURRENT_TIME_BATTERY_HISTORY_PLACEHOLDER, EMPTY_BATTERY_HIST_ENTRY));
}
private static void interpolateHistoryForSlot(
@@ -1228,8 +1171,7 @@
final long currentSlot,
final List<Long> rawTimestampList,
final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap,
- final Map<Long, Map<String, BatteryHistEntry>> resultMap,
- final boolean isStartOrEnd) {
+ final Map<Long, Map<String, BatteryHistEntry>> resultMap) {
final long[] nearestTimestamps = findNearestTimestamp(rawTimestampList, currentSlot);
final long lowerTimestamp = nearestTimestamps[0];
final long upperTimestamp = nearestTimestamps[1];
@@ -1252,9 +1194,8 @@
resultMap.put(currentSlot, new ArrayMap<>());
return;
}
- interpolateHistoryForSlot(context,
- currentSlot, lowerTimestamp, upperTimestamp, batteryHistoryMap, resultMap,
- isStartOrEnd);
+ interpolateHistoryForSlot(
+ context, currentSlot, lowerTimestamp, upperTimestamp, batteryHistoryMap, resultMap);
}
private static void interpolateHistoryForSlot(
@@ -1263,8 +1204,7 @@
final long lowerTimestamp,
final long upperTimestamp,
final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap,
- final Map<Long, Map<String, BatteryHistEntry>> resultMap,
- final boolean isStartOrEnd) {
+ final Map<Long, Map<String, BatteryHistEntry>> resultMap) {
final Map<String, BatteryHistEntry> lowerEntryDataMap =
batteryHistoryMap.get(lowerTimestamp);
final Map<String, BatteryHistEntry> upperEntryDataMap =
@@ -1278,7 +1218,7 @@
// Skips the booting-specific logics and always does interpolation for daily chart level
// data.
if (lowerTimestamp < upperEntryDataBootTimestamp
- && !isForDailyChart(isStartOrEnd, currentSlot)) {
+ && !TimestampUtils.isMidnight(currentSlot)) {
// Provides an opportunity to force align the slot directly.
if ((upperTimestamp - currentSlot) < 10 * DateUtils.MINUTE_IN_MILLIS) {
log(context, "force align into the nearest slot", currentSlot, null);
@@ -1325,70 +1265,6 @@
resultMap.put(currentSlot, newHistEntryMap);
}
- /**
- * @return Returns the nearest even hour timestamp of the given timestamp.
- */
- private static long getNearestEvenHourTimestamp(long rawTimestamp) {
- // If raw hour is even, the nearest even hour should be the even hour before raw
- // start. The hour doesn't need to change and just set the minutes and seconds to 0.
- // Otherwise, the nearest even hour should be raw hour + 1.
- // For example, the nearest hour of 14:30:50 should be 14:00:00. While the nearest
- // hour of 15:30:50 should be 16:00:00.
- return getEvenHourTimestamp(rawTimestamp, /*addHourOfDay*/ 1);
- }
-
- /**
- * @return Returns the fist even hour timestamp after the given timestamp.
- */
- private static long getFirstEvenHourAfterTimestamp(long rawTimestamp) {
- return getLastEvenHourBeforeTimestamp(rawTimestamp + DateUtils.HOUR_IN_MILLIS * 2);
- }
-
- /**
- * @return Returns the last even hour timestamp before the given timestamp.
- */
- private static long getLastEvenHourBeforeTimestamp(long rawTimestamp) {
- // If raw hour is even, the hour doesn't need to change as well.
- // Otherwise, the even hour before raw end should be raw hour - 1.
- // For example, the even hour before 14:30:50 should be 14:00:00. While the even
- // hour before 15:30:50 should be 14:00:00.
- return getEvenHourTimestamp(rawTimestamp, /*addHourOfDay*/ -1);
- }
-
- private static long getEvenHourTimestamp(long rawTimestamp, int addHourOfDay) {
- final Calendar evenHourCalendar = Calendar.getInstance();
- evenHourCalendar.setTimeInMillis(rawTimestamp);
- // Before computing the evenHourCalendar, record raw hour based on local timezone.
- final int rawHour = evenHourCalendar.get(Calendar.HOUR_OF_DAY);
- if (rawHour % 2 != 0) {
- evenHourCalendar.add(Calendar.HOUR_OF_DAY, addHourOfDay);
- }
- evenHourCalendar.set(Calendar.MINUTE, 0);
- evenHourCalendar.set(Calendar.SECOND, 0);
- evenHourCalendar.set(Calendar.MILLISECOND, 0);
- return evenHourCalendar.getTimeInMillis();
- }
-
- private static List<List<Long>> getHourlyTimestamps(final List<Long> dailyTimestamps) {
- final List<List<Long>> hourlyTimestamps = new ArrayList<>();
- if (dailyTimestamps.size() < MIN_DAILY_DATA_SIZE) {
- return hourlyTimestamps;
- }
-
- for (int dailyStartIndex = 0; dailyStartIndex < dailyTimestamps.size() - 1;
- dailyStartIndex++) {
- long currentTimestamp = dailyTimestamps.get(dailyStartIndex);
- final long dailyEndTimestamp = dailyTimestamps.get(dailyStartIndex + 1);
- final List<Long> hourlyTimestampsPerDay = new ArrayList<>();
- while (currentTimestamp <= dailyEndTimestamp) {
- hourlyTimestampsPerDay.add(currentTimestamp);
- currentTimestamp += MIN_TIME_SLOT;
- }
- hourlyTimestamps.add(hourlyTimestampsPerDay);
- }
- return hourlyTimestamps;
- }
-
private static List<BatteryLevelData.PeriodBatteryLevelData> getHourlyPeriodBatteryLevelData(
Context context,
final Map<Long, Map<String, BatteryHistEntry>> processedBatteryHistoryMap,
@@ -1465,11 +1341,16 @@
final Long endTimestamp = hourlyTimestamps.get(hourlyIndex + 1);
final long slotDuration = endTimestamp - startTimestamp;
List<Map<String, BatteryHistEntry>> slotBatteryHistoryList = new ArrayList<>();
- for (Long timestamp = startTimestamp; timestamp <= endTimestamp;
- timestamp += DateUtils.HOUR_IN_MILLIS) {
+ slotBatteryHistoryList.add(
+ batteryHistoryMap.getOrDefault(startTimestamp, EMPTY_BATTERY_MAP));
+ for (Long timestamp = TimestampUtils.getNextHourTimestamp(startTimestamp);
+ timestamp < endTimestamp; timestamp += DateUtils.HOUR_IN_MILLIS) {
slotBatteryHistoryList.add(
batteryHistoryMap.getOrDefault(timestamp, EMPTY_BATTERY_MAP));
}
+ slotBatteryHistoryList.add(
+ batteryHistoryMap.getOrDefault(endTimestamp, EMPTY_BATTERY_MAP));
+
final BatteryDiffData hourlyBatteryDiffData =
insertHourlyUsageDiffDataPerSlot(
context,
@@ -1942,16 +1823,6 @@
return true;
}
- private static long getTimestampWithDayDiff(final long timestamp, final int dayDiff) {
- final Calendar calendar = Calendar.getInstance();
- calendar.setTimeInMillis(timestamp);
- calendar.add(Calendar.DAY_OF_YEAR, dayDiff);
- calendar.set(Calendar.HOUR_OF_DAY, 0);
- calendar.set(Calendar.MINUTE, 0);
- calendar.set(Calendar.SECOND, 0);
- return calendar.getTimeInMillis();
- }
-
private static long getDiffValue(long v1, long v2) {
return v2 > v1 ? v2 - v1 : 0;
}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/TimestampUtils.java b/src/com/android/settings/fuelgauge/batteryusage/TimestampUtils.java
new file mode 100644
index 0000000..594a0ef
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/batteryusage/TimestampUtils.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2023 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.settings.fuelgauge.batteryusage;
+
+import java.util.Calendar;
+
+/** A utility class for timestamp operations. */
+final class TimestampUtils {
+
+ static long getNextHourTimestamp(final long timestamp) {
+ final Calendar calendar = getSharpHourCalendar(timestamp);
+ calendar.add(Calendar.HOUR_OF_DAY, 1);
+ return calendar.getTimeInMillis();
+ }
+
+ static long getNextEvenHourTimestamp(final long timestamp) {
+ final Calendar calendar = getSharpHourCalendar(timestamp);
+ final int hour = calendar.get(Calendar.HOUR_OF_DAY);
+ calendar.add(Calendar.HOUR_OF_DAY, hour % 2 == 0 ? 2 : 1);
+ return calendar.getTimeInMillis();
+ }
+
+ static long getLastEvenHourTimestamp(final long timestamp) {
+ final Calendar calendar = getSharpHourCalendar(timestamp);
+ final int hour = calendar.get(Calendar.HOUR_OF_DAY);
+ calendar.add(Calendar.HOUR_OF_DAY, hour % 2 == 0 ? 0 : -1);
+ return calendar.getTimeInMillis();
+ }
+
+ static long getNextDayTimestamp(final long timestamp) {
+ final Calendar calendar = getSharpHourCalendar(timestamp);
+ calendar.add(Calendar.DAY_OF_YEAR, 1);
+ calendar.set(Calendar.HOUR_OF_DAY, 0);
+ return calendar.getTimeInMillis();
+ }
+
+ static boolean isMidnight(final long timestamp) {
+ final Calendar calendar = Calendar.getInstance();
+ calendar.setTimeInMillis(timestamp);
+ return calendar.get(Calendar.HOUR_OF_DAY) == 0
+ && calendar.get(Calendar.MINUTE) == 0
+ && calendar.get(Calendar.SECOND) == 0
+ && calendar.get(Calendar.MILLISECOND) == 0;
+ }
+
+ private static Calendar getSharpHourCalendar(final long timestamp) {
+ final Calendar calendar = Calendar.getInstance();
+ calendar.setTimeInMillis(timestamp);
+ calendar.set(Calendar.MINUTE, 0);
+ calendar.set(Calendar.SECOND, 0);
+ calendar.set(Calendar.MILLISECOND, 0);
+ return calendar;
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceControllerTest.java
index f18228b..9cebd19 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceControllerTest.java
@@ -150,11 +150,12 @@
// Ignore fast refresh ui from the data processor callback.
verify(mHourlyChartView, atLeast(0)).setViewModel(null);
verify(mHourlyChartView, atLeastOnce()).setViewModel(new BatteryChartViewModel(
- List.of(100, 97, 95, 66),
- List.of(1619251200000L /* 8 AM */,
+ List.of(100, 99, 97, 95, 66),
+ List.of(1619247660000L /* 7:01 AM */,
+ 1619251200000L /* 8 AM */,
1619258400000L /* 10 AM */,
1619265600000L /* 12 PM */,
- 1619272800000L /* 2 PM */),
+ 1619265720000L /* now (12:02 PM) */),
BatteryChartViewModel.AxisLabelPosition.BETWEEN_TRAPEZOIDS,
mBatteryChartPreferenceController.mHourlyChartLabelTextGenerator));
}
@@ -168,10 +169,10 @@
BatteryChartViewModel expectedDailyViewModel = new BatteryChartViewModel(
List.of(100, 83, 59, 66),
// "Sat", "Sun", "Mon", "Mon"
- List.of(1619251200000L /* Sat */,
+ List.of(1619247660000L /* Sat */,
1619308800000L /* Sun */,
1619395200000L /* Mon */,
- 1619467200000L /* Mon */),
+ 1619460120000L /* Mon */),
BatteryChartViewModel.AxisLabelPosition.CENTER_OF_TRAPEZOIDS,
mBatteryChartPreferenceController.mDailyChartLabelTextGenerator);
@@ -194,8 +195,9 @@
expectedDailyViewModel.setSelectedIndex(0);
verify(mDailyChartView).setViewModel(expectedDailyViewModel);
verify(mHourlyChartView).setViewModel(new BatteryChartViewModel(
- List.of(100, 97, 95, 93, 91, 89, 87, 85, 83),
- List.of(1619251200000L /* 8 AM */,
+ List.of(100, 99, 97, 95, 93, 91, 89, 87, 85, 83),
+ List.of(1619247660000L /* 7:01 AM */,
+ 1619251200000L /* 8 AM */,
1619258400000L /* 10 AM */,
1619265600000L /* 12 PM */,
1619272800000L /* 2 PM */,
@@ -262,7 +264,7 @@
1619445600000L /* 2 PM */,
1619452800000L /* 4 PM */,
1619460000000L /* 6 PM */,
- 1619467200000L /* 8 PM */),
+ 1619460120000L /* now (6:02 PM) */),
BatteryChartViewModel.AxisLabelPosition.BETWEEN_TRAPEZOIDS,
mBatteryChartPreferenceController.mHourlyChartLabelTextGenerator));
@@ -327,7 +329,7 @@
public void selectedSlotText_onlyOneDayDataSelectAnHour_onlyHourText() {
mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(6));
mBatteryChartPreferenceController.mDailyChartIndex = 0;
- mBatteryChartPreferenceController.mHourlyChartIndex = 1;
+ mBatteryChartPreferenceController.mHourlyChartIndex = 2;
assertThat(mBatteryChartPreferenceController.getSlotInformation()).isEqualTo(
"10 AM - 12 PM");
@@ -344,6 +346,36 @@
}
@Test
+ public void selectedSlotText_selectFirstSlot_withMinuteText() {
+ mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(6));
+ mBatteryChartPreferenceController.mDailyChartIndex = 0;
+ mBatteryChartPreferenceController.mHourlyChartIndex = 0;
+
+ assertThat(mBatteryChartPreferenceController.getSlotInformation()).isEqualTo(
+ "7:01 AM - 8 AM");
+ }
+
+ @Test
+ public void selectedSlotText_selectLastSlot_withNowText() {
+ mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(6));
+ mBatteryChartPreferenceController.mDailyChartIndex = 0;
+ mBatteryChartPreferenceController.mHourlyChartIndex = 3;
+
+ assertThat(mBatteryChartPreferenceController.getSlotInformation()).isEqualTo(
+ "12 PM - now");
+ }
+
+ @Test
+ public void selectedSlotText_selectOnlySlot_withMinuteAndNowText() {
+ mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(1));
+ mBatteryChartPreferenceController.mDailyChartIndex = 0;
+ mBatteryChartPreferenceController.mHourlyChartIndex = 0;
+
+ assertThat(mBatteryChartPreferenceController.getSlotInformation()).isEqualTo(
+ "7:01 AM - now");
+ }
+
+ @Test
public void onSaveInstanceState_restoreSelectedIndexAndExpandState() {
final int expectedDailyIndex = 1;
final int expectedHourlyIndex = 2;
@@ -373,7 +405,7 @@
final int totalHour = BatteryChartPreferenceController.getTotalHours(batteryLevelData);
// Only calculate the even hours.
- assertThat(totalHour).isEqualTo(60);
+ assertThat(totalHour).isEqualTo(59);
}
private static Long generateTimestamp(int index) {
@@ -403,10 +435,14 @@
final BatteryHistEntry entry = new BatteryHistEntry(values);
final Map<String, BatteryHistEntry> entryMap = new HashMap<>();
entryMap.put("fake_entry_key" + index, entry);
- batteryHistoryMap.put(generateTimestamp(index), entryMap);
+ long timestamp = generateTimestamp(index);
+ if (index == 0) {
+ timestamp += DateUtils.MINUTE_IN_MILLIS;
+ }
+ batteryHistoryMap.put(timestamp, entryMap);
}
DataProcessor.sTestCurrentTimeMillis =
- generateTimestamp(numOfHours - 1) + DateUtils.MINUTE_IN_MILLIS;
+ generateTimestamp(numOfHours - 1) + DateUtils.MINUTE_IN_MILLIS * 2;
return batteryHistoryMap;
}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessManagerTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessManagerTest.java
index 2a00116..b610cfb 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessManagerTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessManagerTest.java
@@ -263,7 +263,7 @@
}
@Test
- public void getBatteryLevelData_notEnoughData_returnNull() {
+ public void getBatteryLevelData_allDataInOneHour_returnExpectedResult() {
// The timestamps and the current time are within half hour before an even hour.
final long[] timestamps = {
DateUtils.HOUR_IN_MILLIS * 2 - 300L,
@@ -274,9 +274,26 @@
createHistoryMap(timestamps, levels);
DataProcessor.sTestCurrentTimeMillis = timestamps[timestamps.length - 1];
- assertThat(DataProcessManager.getBatteryLevelData(
- mContext, /*handler=*/ null, batteryHistoryMap, /*asyncResponseDelegate=*/ null))
- .isNull();
+ final BatteryLevelData resultData =
+ DataProcessManager.getBatteryLevelData(
+ mContext,
+ /*handler=*/ null,
+ batteryHistoryMap,
+ /*asyncResponseDelegate=*/ null);
+
+
+ final List<Long> expectedDailyTimestamps = List.of(
+ DateUtils.HOUR_IN_MILLIS * 2 - 300L,
+ DateUtils.HOUR_IN_MILLIS * 2 - 100L);
+ final List<Integer> expectedDailyLevels = List.of(100, 66);
+ final List<List<Long>> expectedHourlyTimestamps = List.of(expectedDailyTimestamps);
+ final List<List<Integer>> expectedHourlyLevels = List.of(expectedDailyLevels);
+ verifyExpectedBatteryLevelData(
+ resultData,
+ expectedDailyTimestamps,
+ expectedDailyLevels,
+ expectedHourlyTimestamps,
+ expectedHourlyLevels);
}
@Test
@@ -297,7 +314,7 @@
final List<Long> expectedDailyTimestamps = List.of(
1640966400000L, // 2022-01-01 00:00:00
- 1640973600000L); // 2022-01-01 02:00:00
+ 1640970000000L); // 2022-01-01 01:00:00
final List<Integer> expectedDailyLevels = List.of(100, 66);
final List<List<Long>> expectedHourlyTimestamps = List.of(expectedDailyTimestamps);
final List<List<Integer>> expectedHourlyLevels = List.of(expectedDailyLevels);
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java
index 66e48c0..ea2db86 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java
@@ -42,7 +42,6 @@
import android.os.Parcel;
import android.os.RemoteException;
import android.os.UserManager;
-import android.text.format.DateUtils;
import com.android.settings.fuelgauge.PowerUsageFeatureProvider;
import com.android.settings.testutils.FakeFeatureFactory;
@@ -409,24 +408,21 @@
// Timezone GMT+8
final long[] expectedTimestamps = {
- 1640966400000L, // 2022-01-01 00:00:00
+ 1640966700000L, // 2022-01-01 00:05:00
1640970000000L, // 2022-01-01 01:00:00
1640973600000L, // 2022-01-01 02:00:00
1640977200000L, // 2022-01-01 03:00:00
1640980800000L, // 2022-01-01 04:00:00
- 1640984400000L, // 2022-01-01 05:00:00
- 1640988000000L // 2022-01-01 06:00:00
+ 1640981400000L // 2022-01-01 04:10:00
};
- final int[] expectedLevels = {100, 94, 90, 84, 56, 98, 98};
+ final int[] expectedLevels = {100, 94, 90, 84, 56, 98};
assertThat(resultMap).hasSize(expectedLevels.length);
- for (int index = 0; index < 5; index++) {
+ for (int index = 0; index < expectedLevels.length - 1; index++) {
assertThat(resultMap.get(expectedTimestamps[index]).get(FAKE_ENTRY_KEY).mBatteryLevel)
.isEqualTo(expectedLevels[index]);
}
- for (int index = 5; index < 7; index++) {
- assertThat(resultMap.get(expectedTimestamps[index]).containsKey(
- DataProcessor.CURRENT_TIME_BATTERY_HISTORY_PLACEHOLDER)).isTrue();
- }
+ assertThat(resultMap.get(expectedTimestamps[expectedLevels.length - 1]).containsKey(
+ DataProcessor.CURRENT_TIME_BATTERY_HISTORY_PLACEHOLDER)).isTrue();
}
@Test
@@ -589,7 +585,8 @@
1667782800000L, // 2022-11-06 17:00:00
1667790000000L, // 2022-11-06 19:00:00
1667797200000L, // 2022-11-06 21:00:00
- 1667804400000L // 2022-11-06 23:00:00
+ 1667804400000L, // 2022-11-06 23:00:00
+ 1667808000000L // 2022-11-07 00:00:00
),
List.of(
1667808000000L, // 2022-11-07 00:00:00
@@ -621,6 +618,7 @@
expectedHourlyLevels2.add(null);
expectedHourlyLevels2.add(null);
expectedHourlyLevels2.add(null);
+ expectedHourlyLevels2.add(null);
final List<Integer> expectedHourlyLevels3 = new ArrayList<>();
expectedHourlyLevels3.add(null);
expectedHourlyLevels3.add(null);
@@ -683,7 +681,8 @@
1647216000000L, // 2022-03-13 17:00:00
1647223200000L, // 2022-03-13 19:00:00
1647230400000L, // 2022-03-13 21:00:00
- 1647237600000L // 2022-03-13 23:00:00
+ 1647237600000L, // 2022-03-13 23:00:00
+ 1647241200000L // 2022-03-14 00:00:00
),
List.of(
1647241200000L, // 2022-03-14 00:00:00
@@ -708,6 +707,7 @@
expectedHourlyLevels2.add(null);
expectedHourlyLevels2.add(null);
expectedHourlyLevels2.add(null);
+ expectedHourlyLevels2.add(null);
final List<Integer> expectedHourlyLevels3 = new ArrayList<>();
expectedHourlyLevels3.add(null);
expectedHourlyLevels3.add(null);
@@ -736,49 +736,95 @@
@Test
public void getTimestampSlots_startWithEvenHour_returnExpectedResult() {
final Calendar startCalendar = Calendar.getInstance();
+ startCalendar.clear();
startCalendar.set(2022, 6, 5, 6, 30, 50); // 2022-07-05 06:30:50
+ final long startTimestamp = startCalendar.getTimeInMillis();
final Calendar endCalendar = Calendar.getInstance();
+ endCalendar.clear();
endCalendar.set(2022, 6, 5, 22, 30, 50); // 2022-07-05 22:30:50
+ final long endTimestamp = endCalendar.getTimeInMillis();
- final Calendar expectedStartCalendar = Calendar.getInstance();
- expectedStartCalendar.set(2022, 6, 5, 6, 0, 0); // 2022-07-05 06:00:00
- final Calendar expectedEndCalendar = Calendar.getInstance();
- expectedEndCalendar.set(2022, 6, 6, 0, 0, 0); // 2022-07-05 22:00:00
- verifyExpectedTimestampSlots(
- startCalendar, endCalendar, expectedStartCalendar, expectedEndCalendar);
+ final Calendar calendar = Calendar.getInstance();
+ List<Long> expectedTimestamps = new ArrayList<>();
+ calendar.clear();
+ calendar.set(2022, 6, 5, 6, 30, 50); // 2022-07-05 06:30:50
+ expectedTimestamps.add(calendar.getTimeInMillis());
+ for (int hour = 7; hour <= 22; hour++) {
+ calendar.clear();
+ calendar.set(2022, 6, 5, hour, 0, 0); // 2022-07-05 <hour>:00:00
+ expectedTimestamps.add(calendar.getTimeInMillis());
+ }
+ calendar.clear();
+ calendar.set(2022, 6, 5, 22, 30, 50); // 2022-07-05 22:30:50
+ expectedTimestamps.add(calendar.getTimeInMillis());
+
+ verifyExpectedTimestampSlots(startTimestamp, endTimestamp, expectedTimestamps);
}
@Test
public void getTimestampSlots_startWithOddHour_returnExpectedResult() {
final Calendar startCalendar = Calendar.getInstance();
+ startCalendar.clear();
startCalendar.set(2022, 6, 5, 5, 0, 50); // 2022-07-05 05:00:50
+ final long startTimestamp = startCalendar.getTimeInMillis();
final Calendar endCalendar = Calendar.getInstance();
- endCalendar.set(2022, 6, 6, 21, 0, 50); // 2022-07-06 21:00:50
+ endCalendar.clear();
+ endCalendar.set(2022, 6, 5, 21, 0, 50); // 2022-07-05 21:00:50
+ final long endTimestamp = endCalendar.getTimeInMillis();
- final Calendar expectedStartCalendar = Calendar.getInstance();
- expectedStartCalendar.set(2022, 6, 5, 6, 0, 0); // 2022-07-05 06:00:00
- final Calendar expectedEndCalendar = Calendar.getInstance();
- expectedEndCalendar.set(2022, 6, 6, 22, 0, 0); // 2022-07-06 20:00:00
- verifyExpectedTimestampSlots(
- startCalendar, endCalendar, expectedStartCalendar, expectedEndCalendar);
+ final Calendar calendar = Calendar.getInstance();
+ List<Long> expectedTimestamps = new ArrayList<>();
+ calendar.clear();
+ calendar.set(2022, 6, 5, 5, 0, 50); // 2022-07-05 05:00:50
+ expectedTimestamps.add(calendar.getTimeInMillis());
+ for (int hour = 6; hour <= 21; hour++) {
+ calendar.clear();
+ calendar.set(2022, 6, 5, hour, 0, 0); // 2022-07-05 <hour>:00:00
+ expectedTimestamps.add(calendar.getTimeInMillis());
+ }
+ calendar.clear();
+ calendar.set(2022, 6, 5, 21, 0, 50); // 2022-07-05 21:00:50
+ expectedTimestamps.add(calendar.getTimeInMillis());
+
+ verifyExpectedTimestampSlots(startTimestamp, endTimestamp, expectedTimestamps);
}
@Test
public void getDailyTimestamps_notEnoughData_returnEmptyList() {
assertThat(DataProcessor.getDailyTimestamps(new ArrayList<>())).isEmpty();
assertThat(DataProcessor.getDailyTimestamps(List.of(100L))).isEmpty();
- assertThat(DataProcessor.getDailyTimestamps(List.of(100L, 5400000L))).isEmpty();
}
@Test
- public void getDailyTimestamps_OneHourDataPerDay_returnEmptyList() {
+ public void getDailyTimestamps_allDataInOneHour_returnExpectedList() {
+ // Timezone GMT+8
+ final List<Long> timestamps = List.of(
+ 1640970006000L, // 2022-01-01 01:00:06
+ 1640973608000L // 2022-01-01 01:00:08
+ );
+
+ final List<Long> expectedTimestamps = List.of(
+ 1640970006000L, // 2022-01-01 01:00:06
+ 1640973608000L // 2022-01-01 01:00:08
+ );
+ assertThat(DataProcessor.getDailyTimestamps(timestamps)).isEqualTo(expectedTimestamps);
+ }
+
+ @Test
+ public void getDailyTimestamps_OneHourDataPerDay_returnExpectedList() {
// Timezone GMT+8
final List<Long> timestamps = List.of(
1641049200000L, // 2022-01-01 23:00:00
1641052800000L, // 2022-01-02 00:00:00
1641056400000L // 2022-01-02 01:00:00
);
- assertThat(DataProcessor.getDailyTimestamps(timestamps)).isEmpty();
+
+ final List<Long> expectedTimestamps = List.of(
+ 1641049200000L, // 2022-01-01 23:00:00
+ 1641052800000L, // 2022-01-02 00:00:00
+ 1641056400000L // 2022-01-02 01:00:00
+ );
+ assertThat(DataProcessor.getDailyTimestamps(timestamps)).isEqualTo(expectedTimestamps);
}
@Test
@@ -830,6 +876,7 @@
);
final List<Long> expectedTimestamps = List.of(
+ 1641049200000L, // 2022-01-01 23:00:00
1641052800000L, // 2022-01-02 00:00:00
1641139200000L, // 2022-01-03 00:00:00
1641225600000L, // 2022-01-04 00:00:00
@@ -871,7 +918,8 @@
1640988000000L, // 2022-01-01 06:00:00
1641052800000L, // 2022-01-02 00:00:00
1641139200000L, // 2022-01-03 00:00:00
- 1641225600000L // 2022-01-04 00:00:00
+ 1641225600000L, // 2022-01-04 00:00:00
+ 1641229200000L // 2022-01-04 01:00:00
);
assertThat(DataProcessor.getDailyTimestamps(timestamps)).isEqualTo(expectedTimestamps);
}
@@ -923,27 +971,6 @@
}
@Test
- public void getTimestampOfNextDay_returnExpectedResult() {
- // 2021-02-28 06:00:00 => 2021-03-01 00:00:00
- assertThat(DataProcessor.getTimestampOfNextDay(1614463200000L))
- .isEqualTo(1614528000000L);
- // 2021-12-31 16:00:00 => 2022-01-01 00:00:00
- assertThat(DataProcessor.getTimestampOfNextDay(1640937600000L))
- .isEqualTo(1640966400000L);
- }
-
- @Test
- public void isForDailyChart_returnExpectedResult() {
- assertThat(DataProcessor.isForDailyChart(/*isStartOrEnd=*/ true, 0L)).isTrue();
- // 2022-01-01 00:00:00
- assertThat(DataProcessor.isForDailyChart(/*isStartOrEnd=*/ false, 1640966400000L))
- .isTrue();
- // 2022-01-01 01:00:05
- assertThat(DataProcessor.isForDailyChart(/*isStartOrEnd=*/ false, 1640970005000L))
- .isFalse();
- }
-
- @Test
public void getBatteryUsageMap_emptyHistoryMap_returnNull() {
final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay =
new ArrayList<>();
@@ -2046,24 +2073,16 @@
}
private static void verifyExpectedTimestampSlots(
- final Calendar start,
- final Calendar current,
- final Calendar expectedStart,
- final Calendar expectedEnd) {
- expectedStart.set(Calendar.MILLISECOND, 0);
- expectedEnd.set(Calendar.MILLISECOND, 0);
+ final long startTimestamp,
+ final long currentTimestamp,
+ final List<Long> expectedTimestamps) {
final ArrayList<Long> timestampSlots = new ArrayList<>();
- timestampSlots.add(start.getTimeInMillis());
- final List<Long> resultList =
- DataProcessor.getTimestampSlots(timestampSlots, current.getTimeInMillis());
+ timestampSlots.add(startTimestamp);
- for (int index = 0; index < resultList.size(); index++) {
- final long expectedTimestamp =
- expectedStart.getTimeInMillis() + index * DateUtils.HOUR_IN_MILLIS;
- assertThat(resultList.get(index)).isEqualTo(expectedTimestamp);
- }
- assertThat(resultList.get(resultList.size() - 1))
- .isEqualTo(expectedEnd.getTimeInMillis());
+ final List<Long> resultList =
+ DataProcessor.getTimestampSlots(timestampSlots, currentTimestamp);
+
+ assertThat(resultList).isEqualTo(expectedTimestamps);
}
private static void assertBatteryDiffEntry(
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/TimestampUtilsTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/TimestampUtilsTest.java
new file mode 100644
index 0000000..23787c7
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/TimestampUtilsTest.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2023 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.settings.fuelgauge.batteryusage;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.TimeZone;
+
+@RunWith(RobolectricTestRunner.class)
+public class TimestampUtilsTest {
+
+ @Before
+ public void setUp() {
+ TimeZone.setDefault(TimeZone.getTimeZone("GMT+8"));
+ }
+
+ @Test
+ public void getNextHourTimestamp_returnExpectedResult() {
+ // 2021-02-28 06:00:00 => 2021-02-28 07:00:00
+ assertThat(TimestampUtils.getNextHourTimestamp(1614463200000L))
+ .isEqualTo(1614466800000L);
+ // 2021-12-31 23:59:59 => 2022-01-01 00:00:00
+ assertThat(TimestampUtils.getNextHourTimestamp(16409663999999L))
+ .isEqualTo(16409664000000L);
+ }
+
+ @Test
+ public void getNextEvenHourTimestamp_returnExpectedResult() {
+ // 2021-02-28 06:00:00 => 2021-02-28 08:00:00
+ assertThat(TimestampUtils.getNextEvenHourTimestamp(1614463200000L))
+ .isEqualTo(1614470400000L);
+ // 2021-12-31 23:59:59 => 2022-01-01 00:00:00
+ assertThat(TimestampUtils.getNextEvenHourTimestamp(16409663999999L))
+ .isEqualTo(16409664000000L);
+ }
+
+ @Test
+ public void getLastEvenHourTimestamp_returnExpectedResult() {
+ // 2021-02-28 06:00:06 => 2021-02-28 06:00:00
+ assertThat(TimestampUtils.getLastEvenHourTimestamp(1614463206000L))
+ .isEqualTo(1614463200000L);
+ // 2021-12-31 23:59:59 => 2021-12-31 22:00:00
+ assertThat(TimestampUtils.getLastEvenHourTimestamp(16409663999999L))
+ .isEqualTo(16409656800000L);
+ }
+
+ @Test
+ public void getTimestampOfNextDay_returnExpectedResult() {
+ // 2021-02-28 06:00:00 => 2021-03-01 00:00:00
+ assertThat(TimestampUtils.getNextDayTimestamp(1614463200000L))
+ .isEqualTo(1614528000000L);
+ // 2021-12-31 16:00:00 => 2022-01-01 00:00:00
+ assertThat(TimestampUtils.getNextDayTimestamp(1640937600000L))
+ .isEqualTo(1640966400000L);
+ }
+
+ @Test
+ public void isMidnight_returnExpectedResult() {
+ // 2022-01-01 00:00:00
+ assertThat(TimestampUtils.isMidnight(1640966400000L)).isTrue();
+ // 2022-01-01 01:00:05
+ assertThat(TimestampUtils.isMidnight(1640970005000L)).isFalse();
+ }
+}