diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java index e9d10f2da83..67d396990cf 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java +++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java @@ -251,8 +251,7 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll hourlyBatteryLevelsPerDay.getLevels(), hourlyBatteryLevelsPerDay.getTimestamps(), BatteryChartViewModel.AxisLabelPosition.BETWEEN_TRAPEZOIDS, - mHourlyChartLabelTextGenerator.setLatestTimestamp(getLast(getLast( - batteryLevelData.getHourlyBatteryLevelsPerDay()).getTimestamps())))); + mHourlyChartLabelTextGenerator.updateSpecialCaseContext(batteryLevelData))); } refreshUi(); } @@ -640,7 +639,11 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll 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 timestamps, int index) { @@ -648,24 +651,37 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll // 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 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 c6c548f0dce..2c98c4bd249 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java +++ b/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java @@ -280,12 +280,12 @@ public final class ConvertUtils { } /** 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 24571cd0a60..9aee2f79032 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java +++ b/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java @@ -285,7 +285,6 @@ public final class DataProcessor { final Map>>>> resultMap = new ArrayMap<>(); - final long dailySize = hourlyBatteryLevelsPerDay.size(); for (int dailyIndex = 0; dailyIndex < hourlyBatteryLevelsPerDay.size(); dailyIndex++) { final Map>>> dailyMap = new ArrayMap<>(); @@ -294,21 +293,9 @@ public final class DataProcessor { continue; } final List 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 hourlyAppUsageEventList = getAppUsageEventListWithinTimeRangeWithBuffer( @@ -463,11 +450,8 @@ public final class DataProcessor { Collections.sort(rawTimestampList); final long currentTime = getCurrentTimeMillis(); final List 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 @@ public final class DataProcessor { } /** - * 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 getTimestampSlots(final List rawTimestampList, final long currentTime) { @@ -505,19 +490,18 @@ public final class DataProcessor { 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,34 +523,38 @@ public final class DataProcessor { } 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; - } - 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<>(); + for (long timestamp = startTime; timestamp < endTime; + timestamp = TimestampUtils.getNextDayTimestamp(timestamp)) { + dailyTimestampList.add(timestamp); } + dailyTimestampList.add(endTime); return dailyTimestampList; } + @VisibleForTesting + static List> getHourlyTimestamps(final List dailyTimestamps) { + final List> hourlyTimestamps = new ArrayList<>(); + if (dailyTimestamps.size() < MIN_DAILY_DATA_SIZE) { + return hourlyTimestamps; + } + + for (int dailyIndex = 0; dailyIndex < dailyTimestamps.size() - 1; dailyIndex++) { + final List 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 entryList) { if (entryList == null) { @@ -602,30 +590,6 @@ public final class DataProcessor { return results; } - /** - * @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. * @@ -1184,43 +1148,22 @@ public final class DataProcessor { Context context, final List rawTimestampList, final List expectedTimestampSlots, - final long currentTime, - final boolean isFromFullCharge, final Map> batteryHistoryMap, final Map> 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 @@ public final class DataProcessor { final long currentSlot, final List rawTimestampList, final Map> batteryHistoryMap, - final Map> resultMap, - final boolean isStartOrEnd) { + final Map> resultMap) { final long[] nearestTimestamps = findNearestTimestamp(rawTimestampList, currentSlot); final long lowerTimestamp = nearestTimestamps[0]; final long upperTimestamp = nearestTimestamps[1]; @@ -1252,9 +1194,8 @@ public final class DataProcessor { 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 @@ public final class DataProcessor { final long lowerTimestamp, final long upperTimestamp, final Map> batteryHistoryMap, - final Map> resultMap, - final boolean isStartOrEnd) { + final Map> resultMap) { final Map lowerEntryDataMap = batteryHistoryMap.get(lowerTimestamp); final Map upperEntryDataMap = @@ -1278,7 +1218,7 @@ public final class DataProcessor { // 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 @@ public final class DataProcessor { 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> getHourlyTimestamps(final List dailyTimestamps) { - final List> 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 hourlyTimestampsPerDay = new ArrayList<>(); - while (currentTimestamp <= dailyEndTimestamp) { - hourlyTimestampsPerDay.add(currentTimestamp); - currentTimestamp += MIN_TIME_SLOT; - } - hourlyTimestamps.add(hourlyTimestampsPerDay); - } - return hourlyTimestamps; - } - private static List getHourlyPeriodBatteryLevelData( Context context, final Map> processedBatteryHistoryMap, @@ -1465,11 +1341,16 @@ public final class DataProcessor { final Long endTimestamp = hourlyTimestamps.get(hourlyIndex + 1); final long slotDuration = endTimestamp - startTimestamp; List> 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 @@ public final class DataProcessor { 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 00000000000..594a0effa80 --- /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 f18228bd84e..9cebd1958ff 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 @@ public final class BatteryChartPreferenceControllerTest { // 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 @@ public final class BatteryChartPreferenceControllerTest { 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 @@ public final class BatteryChartPreferenceControllerTest { 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 @@ public final class BatteryChartPreferenceControllerTest { 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 final class BatteryChartPreferenceControllerTest { 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"); @@ -343,6 +345,36 @@ public final class BatteryChartPreferenceControllerTest { "Sunday 4 PM - 6 PM"); } + @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; @@ -373,7 +405,7 @@ public final class BatteryChartPreferenceControllerTest { 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 @@ public final class BatteryChartPreferenceControllerTest { final BatteryHistEntry entry = new BatteryHistEntry(values); final Map 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 2a001163485..b610cfbf289 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 @@ public final class DataProcessManagerTest { } @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 @@ public final class DataProcessManagerTest { 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 expectedDailyTimestamps = List.of( + DateUtils.HOUR_IN_MILLIS * 2 - 300L, + DateUtils.HOUR_IN_MILLIS * 2 - 100L); + final List expectedDailyLevels = List.of(100, 66); + final List> expectedHourlyTimestamps = List.of(expectedDailyTimestamps); + final List> expectedHourlyLevels = List.of(expectedDailyLevels); + verifyExpectedBatteryLevelData( + resultData, + expectedDailyTimestamps, + expectedDailyLevels, + expectedHourlyTimestamps, + expectedHourlyLevels); } @Test @@ -297,7 +314,7 @@ public final class DataProcessManagerTest { final List 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 expectedDailyLevels = List.of(100, 66); final List> expectedHourlyTimestamps = List.of(expectedDailyTimestamps); final List> 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 66e48c09246..ea2db86daec 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.BatteryUsageStats; 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 @@ public final class DataProcessorTest { // 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 @@ public final class DataProcessorTest { 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 @@ public final class DataProcessorTest { expectedHourlyLevels2.add(null); expectedHourlyLevels2.add(null); expectedHourlyLevels2.add(null); + expectedHourlyLevels2.add(null); final List expectedHourlyLevels3 = new ArrayList<>(); expectedHourlyLevels3.add(null); expectedHourlyLevels3.add(null); @@ -683,7 +681,8 @@ public final class DataProcessorTest { 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 @@ public final class DataProcessorTest { expectedHourlyLevels2.add(null); expectedHourlyLevels2.add(null); expectedHourlyLevels2.add(null); + expectedHourlyLevels2.add(null); final List expectedHourlyLevels3 = new ArrayList<>(); expectedHourlyLevels3.add(null); expectedHourlyLevels3.add(null); @@ -736,49 +736,95 @@ public final class DataProcessorTest { @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 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 :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 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 :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 timestamps = List.of( + 1640970006000L, // 2022-01-01 01:00:06 + 1640973608000L // 2022-01-01 01:00:08 + ); + + final List 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 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 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 @@ public final class DataProcessorTest { ); final List 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 @@ public final class DataProcessorTest { 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); } @@ -922,27 +970,6 @@ public final class DataProcessorTest { assertThat(results).isEqualTo(new long[] {40L, 0L}); } - @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 hourlyBatteryLevelsPerDay = @@ -2046,24 +2073,16 @@ public final class DataProcessorTest { } 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 expectedTimestamps) { final ArrayList timestampSlots = new ArrayList<>(); - timestampSlots.add(start.getTimeInMillis()); - final List 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 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 00000000000..23787c7dceb --- /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(); + } +}