Merge "Add DataProcessor to process raw history map for UI."
This commit is contained in:
committed by
Android (Google) Code Review
commit
873a47576d
@@ -16,58 +16,66 @@
|
|||||||
|
|
||||||
package com.android.settings.fuelgauge.batteryusage;
|
package com.android.settings.fuelgauge.batteryusage;
|
||||||
|
|
||||||
import android.util.Log;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.core.util.Preconditions;
|
||||||
import com.google.common.collect.ImmutableList;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** Wraps the battery timestamp and level data used for battery usage chart. */
|
/** Wraps the battery timestamp and level data used for battery usage chart. */
|
||||||
public final class BatteryLevelData {
|
public final class BatteryLevelData {
|
||||||
|
|
||||||
/** A container for the battery timestamp and level data. */
|
/** A container for the battery timestamp and level data. */
|
||||||
public static final class PeriodBatteryLevelData {
|
public static final class PeriodBatteryLevelData {
|
||||||
private static final String TAG = "PeriodBatteryLevelData";
|
// The length of mTimestamps and mLevels must be the same. mLevels[index] might be null when
|
||||||
|
// there is no level data for the corresponding timestamp.
|
||||||
|
private final List<Long> mTimestamps;
|
||||||
|
private final List<Integer> mLevels;
|
||||||
|
|
||||||
private final ImmutableList<Long> mTimestamps;
|
public PeriodBatteryLevelData(
|
||||||
private final ImmutableList<Integer> mLevels;
|
@NonNull List<Long> timestamps, @NonNull List<Integer> levels) {
|
||||||
|
Preconditions.checkArgument(timestamps.size() == levels.size(),
|
||||||
public PeriodBatteryLevelData(List<Long> timestamps, List<Integer> levels) {
|
/* errorMessage= */ "Timestamp: " + timestamps.size() + ", Level: "
|
||||||
if (timestamps.size() != levels.size()) {
|
+ levels.size());
|
||||||
Log.e(TAG, "Different sizes of timestamps and levels. Timestamp: "
|
mTimestamps = timestamps;
|
||||||
+ timestamps.size() + ", Level: " + levels.size());
|
mLevels = levels;
|
||||||
mTimestamps = ImmutableList.of();
|
|
||||||
mLevels = ImmutableList.of();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mTimestamps = ImmutableList.copyOf(timestamps);
|
|
||||||
mLevels = ImmutableList.copyOf(levels);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ImmutableList<Long> getTimestamps() {
|
public List<Long> getTimestamps() {
|
||||||
return mTimestamps;
|
return mTimestamps;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ImmutableList<Integer> getLevels() {
|
public List<Integer> getLevels() {
|
||||||
return mLevels;
|
return mLevels;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* There could be 2 cases for the daily battery levels:
|
||||||
|
* 1) length is 2: The usage data is within 1 day. Only contains start and end data, such as
|
||||||
|
* data of 2022-01-01 06:00 and 2022-01-01 16:00.
|
||||||
|
* 2) length > 2: The usage data is more than 1 days. The data should be the start, end and 0am
|
||||||
|
* data of every day between the start and end, such as data of 2022-01-01 06:00,
|
||||||
|
* 2022-01-02 00:00, 2022-01-03 00:00 and 2022-01-03 08:00.
|
||||||
|
*/
|
||||||
private final PeriodBatteryLevelData mDailyBatteryLevels;
|
private final PeriodBatteryLevelData mDailyBatteryLevels;
|
||||||
private final ImmutableList<PeriodBatteryLevelData> mHourlyBatteryLevelsPerDay;
|
// The size of hourly data must be the size of daily data - 1.
|
||||||
|
private final List<PeriodBatteryLevelData> mHourlyBatteryLevelsPerDay;
|
||||||
|
|
||||||
public BatteryLevelData(
|
public BatteryLevelData(
|
||||||
PeriodBatteryLevelData dailyBatteryLevels,
|
@NonNull PeriodBatteryLevelData dailyBatteryLevels,
|
||||||
List<PeriodBatteryLevelData> hourlyBatteryLevelsPerDay) {
|
@NonNull List<PeriodBatteryLevelData> hourlyBatteryLevelsPerDay) {
|
||||||
|
final long dailySize = dailyBatteryLevels.getTimestamps().size();
|
||||||
|
final long hourlySize = hourlyBatteryLevelsPerDay.size();
|
||||||
|
Preconditions.checkArgument(hourlySize == dailySize - 1,
|
||||||
|
/* errorMessage= */ "DailySize: " + dailySize + ", HourlySize: " + hourlySize);
|
||||||
mDailyBatteryLevels = dailyBatteryLevels;
|
mDailyBatteryLevels = dailyBatteryLevels;
|
||||||
mHourlyBatteryLevelsPerDay = ImmutableList.copyOf(hourlyBatteryLevelsPerDay);
|
mHourlyBatteryLevelsPerDay = hourlyBatteryLevelsPerDay;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PeriodBatteryLevelData getDailyBatteryLevels() {
|
public PeriodBatteryLevelData getDailyBatteryLevels() {
|
||||||
return mDailyBatteryLevels;
|
return mDailyBatteryLevels;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ImmutableList<PeriodBatteryLevelData> getHourlyBatteryLevelsPerDay() {
|
public List<PeriodBatteryLevelData> getHourlyBatteryLevelsPerDay() {
|
||||||
return mHourlyBatteryLevelsPerDay;
|
return mHourlyBatteryLevelsPerDay;
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -0,0 +1,468 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 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.android.settings.fuelgauge.batteryusage.ConvertUtils.utcToLocalTime;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.text.format.DateUtils;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.android.internal.annotations.VisibleForTesting;
|
||||||
|
import com.android.settingslib.fuelgauge.BatteryStatus;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A utility class to process data loaded from database and make the data easy to use for battery
|
||||||
|
* usage UI.
|
||||||
|
*/
|
||||||
|
public final class DataProcessor {
|
||||||
|
private static final boolean DEBUG = false;
|
||||||
|
private static final String TAG = "DataProcessor";
|
||||||
|
private static final int MIN_DAILY_DATA_SIZE = 2;
|
||||||
|
private static final int MIN_TIMESTAMP_DATA_SIZE = 2;
|
||||||
|
|
||||||
|
/** A fake package name to represent no BatteryEntry data. */
|
||||||
|
public static final String FAKE_PACKAGE_NAME = "fake_package";
|
||||||
|
|
||||||
|
private DataProcessor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns battery level data and start async task to compute battery diff usage data
|
||||||
|
* and load app labels + icons.
|
||||||
|
* Returns null if the input is invalid or not having at least 2 hours data.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public static BatteryLevelData getBatteryLevelData(
|
||||||
|
Context context,
|
||||||
|
@Nullable final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) {
|
||||||
|
if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) {
|
||||||
|
Log.d(TAG, "getBatteryLevelData() returns null");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Process raw history map data into hourly timestamps.
|
||||||
|
final Map<Long, Map<String, BatteryHistEntry>> processedBatteryHistoryMap =
|
||||||
|
getHistoryMapWithExpectedTimestamps(context, batteryHistoryMap);
|
||||||
|
// Wrap and processed history map into easy-to-use format for UI rendering.
|
||||||
|
final BatteryLevelData batteryLevelData =
|
||||||
|
getLevelDataThroughProcessedHistoryMap(context, processedBatteryHistoryMap);
|
||||||
|
|
||||||
|
//TODO: Add the async task to compute diff usage data and load labels and icons.
|
||||||
|
|
||||||
|
return batteryLevelData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the processed history map which has interpolated to every hour data.
|
||||||
|
* The start and end timestamp must be the even hours.
|
||||||
|
* The keys of processed history map should contain every hour between the start and end
|
||||||
|
* timestamp. If there's no data in some key, the value will be the empty hashmap.
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
static Map<Long, Map<String, BatteryHistEntry>> getHistoryMapWithExpectedTimestamps(
|
||||||
|
Context context,
|
||||||
|
final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) {
|
||||||
|
final long startTime = System.currentTimeMillis();
|
||||||
|
final List<Long> rawTimestampList = new ArrayList<>(batteryHistoryMap.keySet());
|
||||||
|
final Map<Long, Map<String, BatteryHistEntry>> resultMap = new HashMap();
|
||||||
|
if (rawTimestampList.isEmpty()) {
|
||||||
|
Log.d(TAG, "empty batteryHistoryMap in getHistoryMapWithExpectedTimestamps()");
|
||||||
|
return resultMap;
|
||||||
|
}
|
||||||
|
Collections.sort(rawTimestampList);
|
||||||
|
final List<Long> expectedTimestampList = getTimestampSlots(rawTimestampList);
|
||||||
|
final boolean isFromFullCharge =
|
||||||
|
isFromFullCharge(batteryHistoryMap.get(rawTimestampList.get(0)));
|
||||||
|
interpolateHistory(
|
||||||
|
context, rawTimestampList, expectedTimestampList, isFromFullCharge,
|
||||||
|
batteryHistoryMap, resultMap);
|
||||||
|
Log.d(TAG, String.format("getHistoryMapWithExpectedTimestamps() size=%d in %d/ms",
|
||||||
|
resultMap.size(), (System.currentTimeMillis() - startTime)));
|
||||||
|
return resultMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
@Nullable
|
||||||
|
static BatteryLevelData getLevelDataThroughProcessedHistoryMap(
|
||||||
|
Context context,
|
||||||
|
final Map<Long, Map<String, BatteryHistEntry>> processedBatteryHistoryMap) {
|
||||||
|
final List<Long> timestampList = new ArrayList<>(processedBatteryHistoryMap.keySet());
|
||||||
|
Collections.sort(timestampList);
|
||||||
|
final List<Long> dailyTimestamps = getDailyTimestamps(timestampList);
|
||||||
|
// There should be at least the start and end timestamps. Otherwise, return null to not show
|
||||||
|
// data in usage chart.
|
||||||
|
if (dailyTimestamps.size() < MIN_DAILY_DATA_SIZE) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<List<Long>> hourlyTimestamps = getHourlyTimestamps(dailyTimestamps);
|
||||||
|
final BatteryLevelData.PeriodBatteryLevelData dailyLevelData =
|
||||||
|
getPeriodBatteryLevelData(context, processedBatteryHistoryMap, dailyTimestamps);
|
||||||
|
final List<BatteryLevelData.PeriodBatteryLevelData> hourlyLevelData =
|
||||||
|
getHourlyPeriodBatteryLevelData(
|
||||||
|
context, processedBatteryHistoryMap, hourlyTimestamps);
|
||||||
|
return new BatteryLevelData(dailyLevelData, hourlyLevelData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes expected timestamp slots for last full charge, which will return hourly timestamps
|
||||||
|
* between start and end two even hour values.
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
static List<Long> getTimestampSlots(final List<Long> rawTimestampList) {
|
||||||
|
final List<Long> timestampSlots = new ArrayList<>();
|
||||||
|
final int rawTimestampListSize = rawTimestampList.size();
|
||||||
|
// If timestamp number is smaller than 2, the following computation is not necessary.
|
||||||
|
if (rawTimestampListSize < MIN_TIMESTAMP_DATA_SIZE) {
|
||||||
|
return timestampSlots;
|
||||||
|
}
|
||||||
|
final long rawStartTimestamp = rawTimestampList.get(0);
|
||||||
|
final long rawEndTimestamp = rawTimestampList.get(rawTimestampListSize - 1);
|
||||||
|
// 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 even hour before the raw end timestamp as the end.
|
||||||
|
final long endTimestamp = getLastEvenHourBeforeTimestamp(rawEndTimestamp);
|
||||||
|
// 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(timestamp);
|
||||||
|
}
|
||||||
|
return timestampSlots;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes expected daily timestamp slots.
|
||||||
|
*
|
||||||
|
* The valid result should be composed of 3 parts:
|
||||||
|
* 1) start timestamp
|
||||||
|
* 2) every 0am timestamp (default timezone) between the start and end
|
||||||
|
* 3) end timestamp
|
||||||
|
* Otherwise, returns an empty list.
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
static List<Long> getDailyTimestamps(final List<Long> timestampList) {
|
||||||
|
final List<Long> dailyTimestampList = new ArrayList<>();
|
||||||
|
// If timestamp number is smaller than 2, the following computation is not necessary.
|
||||||
|
if (timestampList.size() < MIN_TIMESTAMP_DATA_SIZE) {
|
||||||
|
return dailyTimestampList;
|
||||||
|
}
|
||||||
|
final long startTime = timestampList.get(0);
|
||||||
|
final long endTime = timestampList.get(timestampList.size() - 1);
|
||||||
|
long nextDay = getTimestampOfNextDay(startTime);
|
||||||
|
dailyTimestampList.add(startTime);
|
||||||
|
while (nextDay < endTime) {
|
||||||
|
dailyTimestampList.add(nextDay);
|
||||||
|
nextDay += DateUtils.DAY_IN_MILLIS;
|
||||||
|
}
|
||||||
|
dailyTimestampList.add(endTime);
|
||||||
|
return dailyTimestampList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
static boolean isFromFullCharge(@Nullable final Map<String, BatteryHistEntry> entryList) {
|
||||||
|
if (entryList == null) {
|
||||||
|
Log.d(TAG, "entryList is nul in isFromFullCharge()");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final List<String> entryKeys = new ArrayList<>(entryList.keySet());
|
||||||
|
if (entryKeys.isEmpty()) {
|
||||||
|
Log.d(TAG, "empty entryList in isFromFullCharge()");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// The hist entries in the same timestamp should have same battery status and level.
|
||||||
|
// Checking the first one should be enough.
|
||||||
|
final BatteryHistEntry firstHistEntry = entryList.get(entryKeys.get(0));
|
||||||
|
return BatteryStatus.isCharged(firstHistEntry.mBatteryStatus, firstHistEntry.mBatteryLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
static long[] findNearestTimestamp(final List<Long> timestamps, final long target) {
|
||||||
|
final long[] results = new long[] {Long.MIN_VALUE, Long.MAX_VALUE};
|
||||||
|
// Searches the nearest lower and upper timestamp value.
|
||||||
|
for (long timestamp : timestamps) {
|
||||||
|
if (timestamp <= target && timestamp > results[0]) {
|
||||||
|
results[0] = timestamp;
|
||||||
|
}
|
||||||
|
if (timestamp >= target && timestamp < results[1]) {
|
||||||
|
results[1] = timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Uses zero value to represent invalid searching result.
|
||||||
|
results[0] = results[0] == Long.MIN_VALUE ? 0 : results[0];
|
||||||
|
results[1] = results[1] == Long.MAX_VALUE ? 0 : results[1];
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the timestamp for 0am 1 day after the given timestamp based on local
|
||||||
|
* timezone.
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
static long getTimestampOfNextDay(long timestamp) {
|
||||||
|
final Calendar nextDayCalendar = Calendar.getInstance();
|
||||||
|
nextDayCalendar.setTimeInMillis(timestamp);
|
||||||
|
nextDayCalendar.add(Calendar.DAY_OF_YEAR, 1);
|
||||||
|
nextDayCalendar.set(Calendar.HOUR_OF_DAY, 0);
|
||||||
|
nextDayCalendar.set(Calendar.MINUTE, 0);
|
||||||
|
nextDayCalendar.set(Calendar.SECOND, 0);
|
||||||
|
return nextDayCalendar.getTimeInMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interpolates history map based on expected timestamp slots and processes the corner case when
|
||||||
|
* the expected start timestamp is earlier than what we have.
|
||||||
|
*/
|
||||||
|
private static void interpolateHistory(
|
||||||
|
Context context,
|
||||||
|
final List<Long> rawTimestampList,
|
||||||
|
final List<Long> expectedTimestampSlots,
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
for (int index = startIndex; index < expectedTimestampSlots.size(); index++) {
|
||||||
|
final long currentSlot = expectedTimestampSlots.get(index);
|
||||||
|
interpolateHistoryForSlot(
|
||||||
|
context, currentSlot, rawTimestampList, batteryHistoryMap, resultMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void interpolateHistoryForSlot(
|
||||||
|
Context context,
|
||||||
|
final long currentSlot,
|
||||||
|
final List<Long> rawTimestampList,
|
||||||
|
final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap,
|
||||||
|
final Map<Long, Map<String, BatteryHistEntry>> resultMap) {
|
||||||
|
final long[] nearestTimestamps = findNearestTimestamp(rawTimestampList, currentSlot);
|
||||||
|
final long lowerTimestamp = nearestTimestamps[0];
|
||||||
|
final long upperTimestamp = nearestTimestamps[1];
|
||||||
|
// Case 1: upper timestamp is zero since scheduler is delayed!
|
||||||
|
if (upperTimestamp == 0) {
|
||||||
|
log(context, "job scheduler is delayed", currentSlot, null);
|
||||||
|
resultMap.put(currentSlot, new HashMap<>());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Case 2: upper timestamp is closed to the current timestamp.
|
||||||
|
if ((upperTimestamp - currentSlot) < 5 * DateUtils.SECOND_IN_MILLIS) {
|
||||||
|
log(context, "force align into the nearest slot", currentSlot, null);
|
||||||
|
resultMap.put(currentSlot, batteryHistoryMap.get(upperTimestamp));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Case 3: lower timestamp is zero before starting to collect data.
|
||||||
|
if (lowerTimestamp == 0) {
|
||||||
|
log(context, "no lower timestamp slot data", currentSlot, null);
|
||||||
|
resultMap.put(currentSlot, new HashMap<>());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
interpolateHistoryForSlot(context,
|
||||||
|
currentSlot, lowerTimestamp, upperTimestamp, batteryHistoryMap, resultMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void interpolateHistoryForSlot(
|
||||||
|
Context context,
|
||||||
|
final long currentSlot,
|
||||||
|
final long lowerTimestamp,
|
||||||
|
final long upperTimestamp,
|
||||||
|
final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap,
|
||||||
|
final Map<Long, Map<String, BatteryHistEntry>> resultMap) {
|
||||||
|
final Map<String, BatteryHistEntry> lowerEntryDataMap =
|
||||||
|
batteryHistoryMap.get(lowerTimestamp);
|
||||||
|
final Map<String, BatteryHistEntry> upperEntryDataMap =
|
||||||
|
batteryHistoryMap.get(upperTimestamp);
|
||||||
|
// Verifies whether the lower data is valid to use or not by checking boot time.
|
||||||
|
final BatteryHistEntry upperEntryDataFirstEntry =
|
||||||
|
upperEntryDataMap.values().stream().findFirst().get();
|
||||||
|
final long upperEntryDataBootTimestamp =
|
||||||
|
upperEntryDataFirstEntry.mTimestamp - upperEntryDataFirstEntry.mBootTimestamp;
|
||||||
|
// Lower data is captured before upper data corresponding device is booting.
|
||||||
|
if (lowerTimestamp < upperEntryDataBootTimestamp) {
|
||||||
|
// 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);
|
||||||
|
resultMap.put(currentSlot, upperEntryDataMap);
|
||||||
|
} else {
|
||||||
|
log(context, "in the different booting section", currentSlot, null);
|
||||||
|
resultMap.put(currentSlot, new HashMap<>());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log(context, "apply interpolation arithmetic", currentSlot, null);
|
||||||
|
final Map<String, BatteryHistEntry> newHistEntryMap = new HashMap<>();
|
||||||
|
final double timestampLength = upperTimestamp - lowerTimestamp;
|
||||||
|
final double timestampDiff = currentSlot - lowerTimestamp;
|
||||||
|
// Applies interpolation arithmetic for each BatteryHistEntry.
|
||||||
|
for (String entryKey : upperEntryDataMap.keySet()) {
|
||||||
|
final BatteryHistEntry lowerEntry = lowerEntryDataMap.get(entryKey);
|
||||||
|
final BatteryHistEntry upperEntry = upperEntryDataMap.get(entryKey);
|
||||||
|
// Checks whether there is any abnormal battery reset conditions.
|
||||||
|
if (lowerEntry != null) {
|
||||||
|
final boolean invalidForegroundUsageTime =
|
||||||
|
lowerEntry.mForegroundUsageTimeInMs > upperEntry.mForegroundUsageTimeInMs;
|
||||||
|
final boolean invalidBackgroundUsageTime =
|
||||||
|
lowerEntry.mBackgroundUsageTimeInMs > upperEntry.mBackgroundUsageTimeInMs;
|
||||||
|
if (invalidForegroundUsageTime || invalidBackgroundUsageTime) {
|
||||||
|
newHistEntryMap.put(entryKey, upperEntry);
|
||||||
|
log(context, "abnormal reset condition is found", currentSlot, upperEntry);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final BatteryHistEntry newEntry =
|
||||||
|
BatteryHistEntry.interpolate(
|
||||||
|
currentSlot,
|
||||||
|
upperTimestamp,
|
||||||
|
/*ratio=*/ timestampDiff / timestampLength,
|
||||||
|
lowerEntry,
|
||||||
|
upperEntry);
|
||||||
|
newHistEntryMap.put(entryKey, newEntry);
|
||||||
|
if (lowerEntry == null) {
|
||||||
|
log(context, "cannot find lower entry data", currentSlot, upperEntry);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 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 += 2 * DateUtils.HOUR_IN_MILLIS;
|
||||||
|
}
|
||||||
|
hourlyTimestamps.add(hourlyTimestampsPerDay);
|
||||||
|
}
|
||||||
|
return hourlyTimestamps;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<BatteryLevelData.PeriodBatteryLevelData> getHourlyPeriodBatteryLevelData(
|
||||||
|
Context context,
|
||||||
|
final Map<Long, Map<String, BatteryHistEntry>> processedBatteryHistoryMap,
|
||||||
|
final List<List<Long>> timestamps) {
|
||||||
|
final List<BatteryLevelData.PeriodBatteryLevelData> levelData = new ArrayList<>();
|
||||||
|
timestamps.forEach(
|
||||||
|
timestampList -> levelData.add(
|
||||||
|
getPeriodBatteryLevelData(
|
||||||
|
context, processedBatteryHistoryMap, timestampList)));
|
||||||
|
return levelData;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BatteryLevelData.PeriodBatteryLevelData getPeriodBatteryLevelData(
|
||||||
|
Context context,
|
||||||
|
final Map<Long, Map<String, BatteryHistEntry>> processedBatteryHistoryMap,
|
||||||
|
final List<Long> timestamps) {
|
||||||
|
final List<Integer> levels = new ArrayList<>();
|
||||||
|
timestamps.forEach(
|
||||||
|
timestamp -> levels.add(getLevel(context, processedBatteryHistoryMap, timestamp)));
|
||||||
|
return new BatteryLevelData.PeriodBatteryLevelData(timestamps, levels);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Integer getLevel(
|
||||||
|
Context context,
|
||||||
|
final Map<Long, Map<String, BatteryHistEntry>> processedBatteryHistoryMap,
|
||||||
|
final long timestamp) {
|
||||||
|
final Map<String, BatteryHistEntry> entryMap = processedBatteryHistoryMap.get(timestamp);
|
||||||
|
if (entryMap == null || entryMap.isEmpty()) {
|
||||||
|
Log.e(TAG, "abnormal entry list in the timestamp:"
|
||||||
|
+ utcToLocalTime(context, timestamp));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Averages the battery level in each time slot to avoid corner conditions.
|
||||||
|
float batteryLevelCounter = 0;
|
||||||
|
for (BatteryHistEntry entry : entryMap.values()) {
|
||||||
|
batteryLevelCounter += entry.mBatteryLevel;
|
||||||
|
}
|
||||||
|
return Math.round(batteryLevelCounter / entryMap.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void log(Context context, String content, long timestamp,
|
||||||
|
BatteryHistEntry entry) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, String.format(entry != null ? "%s %s:\n%s" : "%s %s:%s",
|
||||||
|
utcToLocalTime(context, timestamp), content, entry));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,454 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 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 static org.mockito.Mockito.spy;
|
||||||
|
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.text.format.DateUtils;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.mockito.MockitoAnnotations;
|
||||||
|
import org.robolectric.RobolectricTestRunner;
|
||||||
|
import org.robolectric.RuntimeEnvironment;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner.class)
|
||||||
|
public class DataProcessorTest {
|
||||||
|
private static final String FAKE_ENTRY_KEY = "fake_entry_key";
|
||||||
|
|
||||||
|
private Context mContext;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
MockitoAnnotations.initMocks(this);
|
||||||
|
TimeZone.setDefault(TimeZone.getTimeZone("GMT+8"));
|
||||||
|
|
||||||
|
mContext = spy(RuntimeEnvironment.application);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getBatteryLevelData_emptyHistoryMap_returnNull() {
|
||||||
|
assertThat(DataProcessor.getBatteryLevelData(mContext, null)).isNull();
|
||||||
|
assertThat(DataProcessor.getBatteryLevelData(mContext, new HashMap<>())).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getBatteryLevelData_notEnoughData_returnNull() {
|
||||||
|
// The timestamps are within 1 hour.
|
||||||
|
final long[] timestamps = {1000000L, 2000000L, 3000000L};
|
||||||
|
final int[] levels = {100, 99, 98};
|
||||||
|
final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap =
|
||||||
|
createHistoryMap(timestamps, levels);
|
||||||
|
|
||||||
|
assertThat(DataProcessor.getBatteryLevelData(mContext, batteryHistoryMap)).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getBatteryLevelData_returnExpectedResult() {
|
||||||
|
// Timezone GMT+8: 2022-01-01 00:00:00, 2022-01-01 01:00:00, 2022-01-01 02:00:00
|
||||||
|
final long[] timestamps = {1640966400000L, 1640970000000L, 1640973600000L};
|
||||||
|
final int[] levels = {100, 99, 98};
|
||||||
|
final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap =
|
||||||
|
createHistoryMap(timestamps, levels);
|
||||||
|
|
||||||
|
final BatteryLevelData resultData =
|
||||||
|
DataProcessor.getBatteryLevelData(mContext, batteryHistoryMap);
|
||||||
|
|
||||||
|
final List<Long> expectedDailyTimestamps = List.of(timestamps[0], timestamps[2]);
|
||||||
|
final List<Integer> expectedDailyLevels = List.of(levels[0], levels[2]);
|
||||||
|
final List<List<Long>> expectedHourlyTimestamps = List.of(expectedDailyTimestamps);
|
||||||
|
final List<List<Integer>> expectedHourlyLevels = List.of(expectedDailyLevels);
|
||||||
|
verifyExpectedBatteryLevelData(
|
||||||
|
resultData,
|
||||||
|
expectedDailyTimestamps,
|
||||||
|
expectedDailyLevels,
|
||||||
|
expectedHourlyTimestamps,
|
||||||
|
expectedHourlyLevels);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getHistoryMapWithExpectedTimestamps_emptyHistoryMap_returnEmptyMap() {
|
||||||
|
assertThat(DataProcessor
|
||||||
|
.getHistoryMapWithExpectedTimestamps(mContext, new HashMap<>()))
|
||||||
|
.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getHistoryMapWithExpectedTimestamps_returnExpectedMap() {
|
||||||
|
// Timezone GMT+8
|
||||||
|
final long[] timestamps = {
|
||||||
|
1640966700000L, // 2022-01-01 00:05:00
|
||||||
|
1640970180000L, // 2022-01-01 01:03:00
|
||||||
|
1640973840000L, // 2022-01-01 02:04:00
|
||||||
|
1640978100000L, // 2022-01-01 03:15:00
|
||||||
|
1640981400000L // 2022-01-01 04:10:00
|
||||||
|
};
|
||||||
|
final int[] levels = {100, 94, 90, 82, 50};
|
||||||
|
final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap =
|
||||||
|
createHistoryMap(timestamps, levels);
|
||||||
|
|
||||||
|
final Map<Long, Map<String, BatteryHistEntry>> resultMap =
|
||||||
|
DataProcessor.getHistoryMapWithExpectedTimestamps(mContext, batteryHistoryMap);
|
||||||
|
|
||||||
|
// Timezone GMT+8
|
||||||
|
final long[] expectedTimestamps = {
|
||||||
|
1640966400000L, // 2022-01-01 00:00: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
|
||||||
|
};
|
||||||
|
final int[] expectedLevels = {100, 94, 90, 84, 56};
|
||||||
|
assertThat(resultMap).hasSize(expectedLevels.length);
|
||||||
|
for (int index = 0; index < expectedLevels.length; index++) {
|
||||||
|
assertThat(resultMap.get(expectedTimestamps[index]).get(FAKE_ENTRY_KEY).mBatteryLevel)
|
||||||
|
.isEqualTo(expectedLevels[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getLevelDataThroughProcessedHistoryMap_notEnoughData_returnNull() {
|
||||||
|
final long[] timestamps = {100L};
|
||||||
|
final int[] levels = {100};
|
||||||
|
final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap =
|
||||||
|
createHistoryMap(timestamps, levels);
|
||||||
|
|
||||||
|
assertThat(
|
||||||
|
DataProcessor.getLevelDataThroughProcessedHistoryMap(mContext, batteryHistoryMap))
|
||||||
|
.isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getLevelDataThroughProcessedHistoryMap_OneDayData_returnExpectedResult() {
|
||||||
|
// Timezone GMT+8
|
||||||
|
final long[] timestamps = {
|
||||||
|
1640966400000L, // 2022-01-01 00:00: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
|
||||||
|
};
|
||||||
|
final int[] levels = {100, 94, 90, 82, 50};
|
||||||
|
final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap =
|
||||||
|
createHistoryMap(timestamps, levels);
|
||||||
|
|
||||||
|
final BatteryLevelData resultData =
|
||||||
|
DataProcessor.getLevelDataThroughProcessedHistoryMap(mContext, batteryHistoryMap);
|
||||||
|
|
||||||
|
final List<Long> expectedDailyTimestamps = List.of(timestamps[0], timestamps[4]);
|
||||||
|
final List<Integer> expectedDailyLevels = List.of(levels[0], levels[4]);
|
||||||
|
final List<List<Long>> expectedHourlyTimestamps = List.of(
|
||||||
|
List.of(timestamps[0], timestamps[2], timestamps[4])
|
||||||
|
);
|
||||||
|
final List<List<Integer>> expectedHourlyLevels = List.of(
|
||||||
|
List.of(levels[0], levels[2], levels[4])
|
||||||
|
);
|
||||||
|
verifyExpectedBatteryLevelData(
|
||||||
|
resultData,
|
||||||
|
expectedDailyTimestamps,
|
||||||
|
expectedDailyLevels,
|
||||||
|
expectedHourlyTimestamps,
|
||||||
|
expectedHourlyLevels);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getLevelDataThroughProcessedHistoryMap_MultipleDaysData_returnExpectedResult() {
|
||||||
|
// Timezone GMT+8
|
||||||
|
final long[] timestamps = {
|
||||||
|
1641038400000L, // 2022-01-01 20:00:00
|
||||||
|
1641060000000L, // 2022-01-02 02:00:00
|
||||||
|
1641067200000L, // 2022-01-02 04:00:00
|
||||||
|
1641081600000L, // 2022-01-02 08:00:00
|
||||||
|
};
|
||||||
|
final int[] levels = {100, 94, 90, 82};
|
||||||
|
final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap =
|
||||||
|
createHistoryMap(timestamps, levels);
|
||||||
|
|
||||||
|
final BatteryLevelData resultData =
|
||||||
|
DataProcessor.getLevelDataThroughProcessedHistoryMap(mContext, batteryHistoryMap);
|
||||||
|
|
||||||
|
final List<Long> expectedDailyTimestamps = List.of(
|
||||||
|
1641038400000L, // 2022-01-01 20:00:00
|
||||||
|
1641052800000L, // 2022-01-02 00:00:00
|
||||||
|
1641081600000L // 2022-01-02 08:00:00
|
||||||
|
);
|
||||||
|
final List<Integer> expectedDailyLevels = new ArrayList<>();
|
||||||
|
expectedDailyLevels.add(100);
|
||||||
|
expectedDailyLevels.add(null);
|
||||||
|
expectedDailyLevels.add(82);
|
||||||
|
final List<List<Long>> expectedHourlyTimestamps = List.of(
|
||||||
|
List.of(
|
||||||
|
1641038400000L, // 2022-01-01 20:00:00
|
||||||
|
1641045600000L, // 2022-01-01 22:00:00
|
||||||
|
1641052800000L // 2022-01-02 00:00:00
|
||||||
|
),
|
||||||
|
List.of(
|
||||||
|
1641052800000L, // 2022-01-02 00:00:00
|
||||||
|
1641060000000L, // 2022-01-02 02:00:00
|
||||||
|
1641067200000L, // 2022-01-02 04:00:00
|
||||||
|
1641074400000L, // 2022-01-02 06:00:00
|
||||||
|
1641081600000L // 2022-01-02 08:00:00
|
||||||
|
)
|
||||||
|
);
|
||||||
|
final List<Integer> expectedHourlyLevels1 = new ArrayList<>();
|
||||||
|
expectedHourlyLevels1.add(100);
|
||||||
|
expectedHourlyLevels1.add(null);
|
||||||
|
expectedHourlyLevels1.add(null);
|
||||||
|
final List<Integer> expectedHourlyLevels2 = new ArrayList<>();
|
||||||
|
expectedHourlyLevels2.add(null);
|
||||||
|
expectedHourlyLevels2.add(94);
|
||||||
|
expectedHourlyLevels2.add(90);
|
||||||
|
expectedHourlyLevels2.add(null);
|
||||||
|
expectedHourlyLevels2.add(82);
|
||||||
|
final List<List<Integer>> expectedHourlyLevels = List.of(
|
||||||
|
expectedHourlyLevels1,
|
||||||
|
expectedHourlyLevels2
|
||||||
|
);
|
||||||
|
verifyExpectedBatteryLevelData(
|
||||||
|
resultData,
|
||||||
|
expectedDailyTimestamps,
|
||||||
|
expectedDailyLevels,
|
||||||
|
expectedHourlyTimestamps,
|
||||||
|
expectedHourlyLevels);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getTimestampSlots_emptyRawList_returnEmptyList() {
|
||||||
|
final List<Long> resultList =
|
||||||
|
DataProcessor.getTimestampSlots(new ArrayList<>());
|
||||||
|
assertThat(resultList).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getTimestampSlots_startWithEvenHour_returnExpectedResult() {
|
||||||
|
final Calendar startCalendar = Calendar.getInstance();
|
||||||
|
startCalendar.set(2022, 6, 5, 6, 30, 50); // 2022-07-05 06:30:50
|
||||||
|
final Calendar endCalendar = Calendar.getInstance();
|
||||||
|
endCalendar.set(2022, 6, 5, 22, 30, 50); // 2022-07-05 22:30:50
|
||||||
|
|
||||||
|
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, 5, 22, 0, 0); // 2022-07-05 22:00:00
|
||||||
|
verifyExpectedTimestampSlots(
|
||||||
|
startCalendar, endCalendar, expectedStartCalendar, expectedEndCalendar);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getTimestampSlots_startWithOddHour_returnExpectedResult() {
|
||||||
|
final Calendar startCalendar = Calendar.getInstance();
|
||||||
|
startCalendar.set(2022, 6, 5, 5, 0, 50); // 2022-07-05 05:00:50
|
||||||
|
final Calendar endCalendar = Calendar.getInstance();
|
||||||
|
endCalendar.set(2022, 6, 6, 21, 00, 50); // 2022-07-06 21:00:50
|
||||||
|
|
||||||
|
final Calendar expectedStartCalendar = Calendar.getInstance();
|
||||||
|
expectedStartCalendar.set(2022, 6, 5, 6, 00, 00); // 2022-07-05 06:00:00
|
||||||
|
final Calendar expectedEndCalendar = Calendar.getInstance();
|
||||||
|
expectedEndCalendar.set(2022, 6, 6, 20, 00, 00); // 2022-07-06 20:00:00
|
||||||
|
verifyExpectedTimestampSlots(
|
||||||
|
startCalendar, endCalendar, expectedStartCalendar, expectedEndCalendar);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getDailyTimestamps_notEnoughData_returnEmptyList() {
|
||||||
|
assertThat(DataProcessor.getDailyTimestamps(new ArrayList<>())).isEmpty();
|
||||||
|
assertThat(DataProcessor.getDailyTimestamps(List.of(100L))).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getDailyTimestamps_OneDayData_returnExpectedList() {
|
||||||
|
// Timezone GMT+8
|
||||||
|
final List<Long> timestamps = List.of(
|
||||||
|
1640966400000L, // 2022-01-01 00:00: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
|
||||||
|
);
|
||||||
|
|
||||||
|
final List<Long> expectedTimestamps = List.of(
|
||||||
|
1640966400000L, // 2022-01-01 00:00:00
|
||||||
|
1640980800000L // 2022-01-01 04:00:00
|
||||||
|
);
|
||||||
|
assertThat(DataProcessor.getDailyTimestamps(timestamps)).isEqualTo(expectedTimestamps);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getDailyTimestamps_MultipleDaysData_returnExpectedList() {
|
||||||
|
// Timezone GMT+8
|
||||||
|
final List<Long> timestamps = List.of(
|
||||||
|
1640988000000L, // 2022-01-01 06:00:00
|
||||||
|
1641060000000L, // 2022-01-02 02:00:00
|
||||||
|
1641160800000L, // 2022-01-03 06:00:00
|
||||||
|
1641254400000L // 2022-01-04 08:00:00
|
||||||
|
);
|
||||||
|
|
||||||
|
final List<Long> expectedTimestamps = List.of(
|
||||||
|
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
|
||||||
|
1641254400000L // 2022-01-04 08:00:00
|
||||||
|
);
|
||||||
|
assertThat(DataProcessor.getDailyTimestamps(timestamps)).isEqualTo(expectedTimestamps);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void isFromFullCharge_emptyData_returnFalse() {
|
||||||
|
assertThat(DataProcessor.isFromFullCharge(null)).isFalse();
|
||||||
|
assertThat(DataProcessor.isFromFullCharge(new HashMap<>())).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void isFromFullCharge_notChargedData_returnFalse() {
|
||||||
|
final Map<String, BatteryHistEntry> entryMap = new HashMap<>();
|
||||||
|
final ContentValues values = new ContentValues();
|
||||||
|
values.put("batteryLevel", 98);
|
||||||
|
final BatteryHistEntry entry = new BatteryHistEntry(values);
|
||||||
|
entryMap.put(FAKE_ENTRY_KEY, entry);
|
||||||
|
|
||||||
|
assertThat(DataProcessor.isFromFullCharge(entryMap)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void isFromFullCharge_chargedData_returnTrue() {
|
||||||
|
final Map<String, BatteryHistEntry> entryMap = new HashMap<>();
|
||||||
|
final ContentValues values = new ContentValues();
|
||||||
|
values.put("batteryLevel", 100);
|
||||||
|
final BatteryHistEntry entry = new BatteryHistEntry(values);
|
||||||
|
entryMap.put(FAKE_ENTRY_KEY, entry);
|
||||||
|
|
||||||
|
assertThat(DataProcessor.isFromFullCharge(entryMap)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void findNearestTimestamp_returnExpectedResult() {
|
||||||
|
long[] results = DataProcessor.findNearestTimestamp(
|
||||||
|
Arrays.asList(10L, 20L, 30L, 40L), /*target=*/ 15L);
|
||||||
|
assertThat(results).isEqualTo(new long[] {10L, 20L});
|
||||||
|
|
||||||
|
results = DataProcessor.findNearestTimestamp(
|
||||||
|
Arrays.asList(10L, 20L, 30L, 40L), /*target=*/ 10L);
|
||||||
|
assertThat(results).isEqualTo(new long[] {10L, 10L});
|
||||||
|
|
||||||
|
results = DataProcessor.findNearestTimestamp(
|
||||||
|
Arrays.asList(10L, 20L, 30L, 40L), /*target=*/ 5L);
|
||||||
|
assertThat(results).isEqualTo(new long[] {0L, 10L});
|
||||||
|
|
||||||
|
results = DataProcessor.findNearestTimestamp(
|
||||||
|
Arrays.asList(10L, 20L, 30L, 40L), /*target=*/ 50L);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<Long, Map<String, BatteryHistEntry>> createHistoryMap(
|
||||||
|
final long[] timestamps, final int[] levels) {
|
||||||
|
final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap = new HashMap<>();
|
||||||
|
for (int index = 0; index < timestamps.length; index++) {
|
||||||
|
final Map<String, BatteryHistEntry> entryMap = new HashMap<>();
|
||||||
|
final ContentValues values = new ContentValues();
|
||||||
|
values.put(BatteryHistEntry.KEY_BATTERY_LEVEL, levels[index]);
|
||||||
|
final BatteryHistEntry entry = new BatteryHistEntry(values);
|
||||||
|
entryMap.put(FAKE_ENTRY_KEY, entry);
|
||||||
|
batteryHistoryMap.put(timestamps[index], entryMap);
|
||||||
|
}
|
||||||
|
return batteryHistoryMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void verifyExpectedBatteryLevelData(
|
||||||
|
final BatteryLevelData resultData,
|
||||||
|
final List<Long> expectedDailyTimestamps,
|
||||||
|
final List<Integer> expectedDailyLevels,
|
||||||
|
final List<List<Long>> expectedHourlyTimestamps,
|
||||||
|
final List<List<Integer>> expectedHourlyLevels) {
|
||||||
|
final BatteryLevelData.PeriodBatteryLevelData dailyResultData =
|
||||||
|
resultData.getDailyBatteryLevels();
|
||||||
|
final List<BatteryLevelData.PeriodBatteryLevelData> hourlyResultData =
|
||||||
|
resultData.getHourlyBatteryLevelsPerDay();
|
||||||
|
verifyExpectedDailyBatteryLevelData(
|
||||||
|
dailyResultData, expectedDailyTimestamps, expectedDailyLevels);
|
||||||
|
verifyExpectedHourlyBatteryLevelData(
|
||||||
|
hourlyResultData, expectedHourlyTimestamps, expectedHourlyLevels);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void verifyExpectedDailyBatteryLevelData(
|
||||||
|
final BatteryLevelData.PeriodBatteryLevelData dailyResultData,
|
||||||
|
final List<Long> expectedDailyTimestamps,
|
||||||
|
final List<Integer> expectedDailyLevels) {
|
||||||
|
assertThat(dailyResultData.getTimestamps()).isEqualTo(expectedDailyTimestamps);
|
||||||
|
assertThat(dailyResultData.getLevels()).isEqualTo(expectedDailyLevels);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void verifyExpectedHourlyBatteryLevelData(
|
||||||
|
final List<BatteryLevelData.PeriodBatteryLevelData> hourlyResultData,
|
||||||
|
final List<List<Long>> expectedHourlyTimestamps,
|
||||||
|
final List<List<Integer>> expectedHourlyLevels) {
|
||||||
|
final int expectedHourlySize = expectedHourlyTimestamps.size();
|
||||||
|
assertThat(hourlyResultData).hasSize(expectedHourlySize);
|
||||||
|
for (int dailyIndex = 0; dailyIndex < expectedHourlySize; dailyIndex++) {
|
||||||
|
assertThat(hourlyResultData.get(dailyIndex).getTimestamps())
|
||||||
|
.isEqualTo(expectedHourlyTimestamps.get(dailyIndex));
|
||||||
|
assertThat(hourlyResultData.get(dailyIndex).getLevels())
|
||||||
|
.isEqualTo(expectedHourlyLevels.get(dailyIndex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void verifyExpectedTimestampSlots(
|
||||||
|
final Calendar start,
|
||||||
|
final Calendar end,
|
||||||
|
final Calendar expectedStart,
|
||||||
|
final Calendar expectedEnd) {
|
||||||
|
expectedStart.set(Calendar.MILLISECOND, 0);
|
||||||
|
expectedEnd.set(Calendar.MILLISECOND, 0);
|
||||||
|
final ArrayList<Long> timestampSlots = new ArrayList<>();
|
||||||
|
timestampSlots.add(start.getTimeInMillis());
|
||||||
|
timestampSlots.add(end.getTimeInMillis());
|
||||||
|
final List<Long> resultList =
|
||||||
|
DataProcessor.getTimestampSlots(timestampSlots);
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Reference in New Issue
Block a user