This cl is a merge of the following 36 cls: ag/19250259 Duplicate BatteryChartPreferenceController and BatteryChartView into new files for better diff review purpose ag/19279660 Use Mockito 4.6.1 API for BatteryChartPreferenceControllerV2Test ag/19267975 Add class BatteryLevelData used to parcel the battery timestamps and levels. It behaves as an interface between UI and data. ag/19289086 Refactor BatteryChartView X-axis labels. Instead of only timestamps, also support any string[] labels. ag/19238586 Add interpolation for the history data since last full charge loaded from database. ag/19331746 Return raw history map in function getHistoryMapSinceLastFullCharge. ag/19308838 In BatteryChartViewV2, use levels.length-1 to replace mTrapezoidCount. So the chartview could show any number of slots as the given levels length-1. ag/19332266 Add class BatteryDiffData used to parcel battery usage data ag/19331467 Refactor Battery Chart View State Controll ag/19358207 Add DataProcessor to process raw history map for UI. ag/19332276 Add battery chart view model. ag/19394744 Update trapezoid validation in battery chart view. ag/19379730 Support daily and hourly battery chartview. ag/19428426 Improve X axis labels in battery chart (1) ag/19446215 Improve X axis labels in battery chart (2) ag/19394745 Add the async task to compute diff usage data and load labels and icons. ag/19447624 Support showing app usage list for two battery charts ag/19500907 Updates battery usage messages from last 24hr to last full charge. (Part1: V2 files) ag/19505324 Update the selected period message in battery chart ag/19500905 Updates battery usage messages from last 24hr to last full charge. (Part2: non-V2 files) ag/19510363 Update usage data for EBS app usage list and App usage detail from 24 hours to last full charge. ag/19523184 Update usage data for EBS app usage list and App usage detail from 24 hours to last full charge. ag/19534864 Add margin between battery daily and hourly charts ag/19491093 Always do interpolation for battery level data in daily chart. ag/19565630 Avoid NullPointerException when batteryLevelData is null. ag/19561239 Fix b/241872474 Battery usage page will crash when selecting the last hour chart bar, going to app detail page, and going back ag/19565633 Fix b/241885070: Unexpected texts moving when going back to battery usage page ag/19534850 New way to draw battery chart axis labels ag/19561240 Switch Battery Usage Chart from V1 to V2. ag/19561338 Switch Battery Usage Chart from V1 to V2. ag/19600174 Fix b/242254055 Battery usage initial screen improvements (long data loading time) ag/19600284 Fix b/242252080: Add padding space on the top of the battery chart ag/19647338 Consider usage map valid even if [all][all] is null. ag/19634227 Use new content uri everytime to avoid cache ag/19600177 Fix b/242009481: Refine the battery usage chart timestamp label rule ag/19647337 Fix b/242809981 Charge battery to 100% when battery usage page opened, the chart will refresh, but the app list isn't refreshed in that case. Test: manual Bug: 239491373 Bug: 236101166 Bug: 236101687 Fix: 236101166 Change-Id: I7de8d9dcee14627da10752534991f1ec9f616020 Merged-In: I9142c0d4e00dea3771777ba9aedeab07b635fa1a
1059 lines
49 KiB
Java
1059 lines
49 KiB
Java
/*
|
|
* 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.ContentValues;
|
|
import android.content.Context;
|
|
import android.os.AsyncTask;
|
|
import android.os.Handler;
|
|
import android.os.Looper;
|
|
import android.os.UserHandle;
|
|
import android.os.UserManager;
|
|
import android.text.TextUtils;
|
|
import android.text.format.DateUtils;
|
|
import android.util.ArraySet;
|
|
import android.util.Log;
|
|
|
|
import androidx.annotation.Nullable;
|
|
|
|
import com.android.internal.annotations.VisibleForTesting;
|
|
import com.android.settings.Utils;
|
|
import com.android.settings.fuelgauge.BatteryUtils;
|
|
import com.android.settings.overlay.FeatureFactory;
|
|
import com.android.settingslib.fuelgauge.BatteryStatus;
|
|
|
|
import java.time.Duration;
|
|
import java.util.ArrayList;
|
|
import java.util.Calendar;
|
|
import java.util.Collection;
|
|
import java.util.Collections;
|
|
import java.util.HashMap;
|
|
import java.util.Iterator;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
|
|
/**
|
|
* 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;
|
|
private static final int MAX_DIFF_SECONDS_OF_UPPER_TIMESTAMP = 5;
|
|
// Maximum total time value for each hourly slot cumulative data at most 2 hours.
|
|
private static final float TOTAL_HOURLY_TIME_THRESHOLD = DateUtils.HOUR_IN_MILLIS * 2;
|
|
private static final Map<String, BatteryHistEntry> EMPTY_BATTERY_MAP = new HashMap<>();
|
|
private static final BatteryHistEntry EMPTY_BATTERY_HIST_ENTRY =
|
|
new BatteryHistEntry(new ContentValues());
|
|
|
|
@VisibleForTesting
|
|
static final double PERCENTAGE_OF_TOTAL_THRESHOLD = 1f;
|
|
@VisibleForTesting
|
|
static final int SELECTED_INDEX_ALL = BatteryChartViewModel.SELECTED_INDEX_ALL;
|
|
|
|
/** A fake package name to represent no BatteryEntry data. */
|
|
public static final String FAKE_PACKAGE_NAME = "fake_package";
|
|
|
|
/** A callback listener when battery usage loading async task is executed. */
|
|
public interface UsageMapAsyncResponse {
|
|
/** The callback function when batteryUsageMap is loaded. */
|
|
void onBatteryUsageMapLoaded(
|
|
Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageMap);
|
|
}
|
|
|
|
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 Handler handler,
|
|
@Nullable final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap,
|
|
final UsageMapAsyncResponse asyncResponseDelegate) {
|
|
if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) {
|
|
Log.d(TAG, "getBatteryLevelData() returns null");
|
|
return null;
|
|
}
|
|
handler = handler != null ? handler : new Handler(Looper.getMainLooper());
|
|
// 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);
|
|
|
|
// Start the async task to compute diff usage data and load labels and icons.
|
|
if (batteryLevelData != null) {
|
|
new ComputeUsageMapAndLoadItemsTask(
|
|
context,
|
|
handler,
|
|
asyncResponseDelegate,
|
|
batteryLevelData.getHourlyBatteryLevelsPerDay(),
|
|
processedBatteryHistoryMap).execute();
|
|
}
|
|
|
|
return batteryLevelData;
|
|
}
|
|
|
|
/**
|
|
* @return Returns battery usage data of different entries.
|
|
* Returns null if the input is invalid or there is no enough data.
|
|
*/
|
|
@Nullable
|
|
public static Map<Integer, Map<Integer, BatteryDiffData>> getBatteryUsageData(
|
|
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);
|
|
return batteryLevelData == null
|
|
? null
|
|
: getBatteryUsageMap(
|
|
context,
|
|
batteryLevelData.getHourlyBatteryLevelsPerDay(),
|
|
processedBatteryHistoryMap);
|
|
}
|
|
|
|
/**
|
|
* @return Returns whether the target is in the CharSequence array.
|
|
*/
|
|
public static boolean contains(String target, CharSequence[] packageNames) {
|
|
if (target != null && packageNames != null) {
|
|
for (CharSequence packageName : packageNames) {
|
|
if (TextUtils.equals(target, packageName)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @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 00:00 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 null 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.
|
|
timestamps.forEach(timestamp -> {
|
|
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 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.
|
|
*
|
|
* There could be 2 cases of the returned value:
|
|
* 1) null: empty or invalid data.
|
|
* 2) non-null: must be a 2d map and composed by 3 parts:
|
|
* 1 - [SELECTED_INDEX_ALL][SELECTED_INDEX_ALL]
|
|
* 2 - [0][SELECTED_INDEX_ALL] ~ [maxDailyIndex][SELECTED_INDEX_ALL]
|
|
* 3 - [0][0] ~ [maxDailyIndex][maxHourlyIndex]
|
|
*/
|
|
@VisibleForTesting
|
|
@Nullable
|
|
static Map<Integer, Map<Integer, BatteryDiffData>> getBatteryUsageMap(
|
|
final Context context,
|
|
final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay,
|
|
final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) {
|
|
if (batteryHistoryMap.isEmpty()) {
|
|
return null;
|
|
}
|
|
final Map<Integer, Map<Integer, BatteryDiffData>> resultMap = new HashMap<>();
|
|
// Insert diff data from [0][0] to [maxDailyIndex][maxHourlyIndex].
|
|
insertHourlyUsageDiffData(
|
|
context, hourlyBatteryLevelsPerDay, batteryHistoryMap, resultMap);
|
|
// Insert diff data from [0][SELECTED_INDEX_ALL] to [maxDailyIndex][SELECTED_INDEX_ALL].
|
|
insertDailyUsageDiffData(hourlyBatteryLevelsPerDay, resultMap);
|
|
// Insert diff data [SELECTED_INDEX_ALL][SELECTED_INDEX_ALL].
|
|
insertAllUsageDiffData(resultMap);
|
|
purgeLowPercentageAndFakeData(context, resultMap);
|
|
if (!isUsageMapValid(resultMap, hourlyBatteryLevelsPerDay)) {
|
|
return null;
|
|
}
|
|
return resultMap;
|
|
}
|
|
|
|
/**
|
|
* 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));
|
|
}
|
|
final int expectedTimestampSlotsSize = expectedTimestampSlots.size();
|
|
for (int index = startIndex; index < expectedTimestampSlotsSize; index++) {
|
|
final long currentSlot = expectedTimestampSlots.get(index);
|
|
final boolean isStartOrEnd = index == 0 || index == expectedTimestampSlotsSize - 1;
|
|
interpolateHistoryForSlot(
|
|
context, currentSlot, rawTimestampList, batteryHistoryMap, resultMap,
|
|
isStartOrEnd);
|
|
}
|
|
}
|
|
|
|
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 boolean isStartOrEnd) {
|
|
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)
|
|
< MAX_DIFF_SECONDS_OF_UPPER_TIMESTAMP * 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,
|
|
isStartOrEnd);
|
|
}
|
|
|
|
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 boolean isStartOrEnd) {
|
|
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.
|
|
// Skips the booting-specific logics and always does interpolation for daily chart level
|
|
// data.
|
|
if (lowerTimestamp < upperEntryDataBootTimestamp
|
|
&& !isForDailyChart(isStartOrEnd, 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);
|
|
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 insertHourlyUsageDiffData(
|
|
Context context,
|
|
final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay,
|
|
final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap,
|
|
final Map<Integer, Map<Integer, BatteryDiffData>> resultMap) {
|
|
final int currentUserId = context.getUserId();
|
|
final UserHandle userHandle =
|
|
Utils.getManagedProfile(context.getSystemService(UserManager.class));
|
|
final int workProfileUserId =
|
|
userHandle != null ? userHandle.getIdentifier() : Integer.MIN_VALUE;
|
|
// Each time slot usage diff data =
|
|
// Math.abs(timestamp[i+2] data - timestamp[i+1] data) +
|
|
// Math.abs(timestamp[i+1] data - timestamp[i] data);
|
|
// since we want to aggregate every two hours data into a single time slot.
|
|
for (int dailyIndex = 0; dailyIndex < hourlyBatteryLevelsPerDay.size(); dailyIndex++) {
|
|
final Map<Integer, BatteryDiffData> dailyDiffMap = new HashMap<>();
|
|
resultMap.put(dailyIndex, dailyDiffMap);
|
|
if (hourlyBatteryLevelsPerDay.get(dailyIndex) == null) {
|
|
continue;
|
|
}
|
|
final List<Long> timestamps = hourlyBatteryLevelsPerDay.get(dailyIndex).getTimestamps();
|
|
for (int hourlyIndex = 0; hourlyIndex < timestamps.size() - 1; hourlyIndex++) {
|
|
final BatteryDiffData hourlyBatteryDiffData =
|
|
insertHourlyUsageDiffDataPerSlot(
|
|
context,
|
|
currentUserId,
|
|
workProfileUserId,
|
|
hourlyIndex,
|
|
timestamps,
|
|
batteryHistoryMap);
|
|
dailyDiffMap.put(hourlyIndex, hourlyBatteryDiffData);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void insertDailyUsageDiffData(
|
|
final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay,
|
|
final Map<Integer, Map<Integer, BatteryDiffData>> resultMap) {
|
|
for (int index = 0; index < hourlyBatteryLevelsPerDay.size(); index++) {
|
|
Map<Integer, BatteryDiffData> dailyUsageMap = resultMap.get(index);
|
|
if (dailyUsageMap == null) {
|
|
dailyUsageMap = new HashMap<>();
|
|
resultMap.put(index, dailyUsageMap);
|
|
}
|
|
dailyUsageMap.put(
|
|
SELECTED_INDEX_ALL,
|
|
getAccumulatedUsageDiffData(dailyUsageMap.values()));
|
|
}
|
|
}
|
|
|
|
private static void insertAllUsageDiffData(
|
|
final Map<Integer, Map<Integer, BatteryDiffData>> resultMap) {
|
|
final List<BatteryDiffData> diffDataList = new ArrayList<>();
|
|
resultMap.keySet().forEach(
|
|
key -> diffDataList.add(resultMap.get(key).get(SELECTED_INDEX_ALL)));
|
|
final Map<Integer, BatteryDiffData> allUsageMap = new HashMap<>();
|
|
allUsageMap.put(SELECTED_INDEX_ALL, getAccumulatedUsageDiffData(diffDataList));
|
|
resultMap.put(SELECTED_INDEX_ALL, allUsageMap);
|
|
}
|
|
|
|
@Nullable
|
|
private static BatteryDiffData insertHourlyUsageDiffDataPerSlot(
|
|
Context context,
|
|
final int currentUserId,
|
|
final int workProfileUserId,
|
|
final int currentIndex,
|
|
final List<Long> timestamps,
|
|
final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) {
|
|
final List<BatteryDiffEntry> appEntries = new ArrayList<>();
|
|
final List<BatteryDiffEntry> systemEntries = new ArrayList<>();
|
|
|
|
final Long currentTimestamp = timestamps.get(currentIndex);
|
|
final Long nextTimestamp = currentTimestamp + DateUtils.HOUR_IN_MILLIS;
|
|
final Long nextTwoTimestamp = nextTimestamp + DateUtils.HOUR_IN_MILLIS;
|
|
// Fetches BatteryHistEntry data from corresponding time slot.
|
|
final Map<String, BatteryHistEntry> currentBatteryHistMap =
|
|
batteryHistoryMap.getOrDefault(currentTimestamp, EMPTY_BATTERY_MAP);
|
|
final Map<String, BatteryHistEntry> nextBatteryHistMap =
|
|
batteryHistoryMap.getOrDefault(nextTimestamp, EMPTY_BATTERY_MAP);
|
|
final Map<String, BatteryHistEntry> nextTwoBatteryHistMap =
|
|
batteryHistoryMap.getOrDefault(nextTwoTimestamp, EMPTY_BATTERY_MAP);
|
|
// We should not get the empty list since we have at least one fake data to record
|
|
// the battery level and status in each time slot, the empty list is used to
|
|
// represent there is no enough data to apply interpolation arithmetic.
|
|
if (currentBatteryHistMap.isEmpty()
|
|
|| nextBatteryHistMap.isEmpty()
|
|
|| nextTwoBatteryHistMap.isEmpty()) {
|
|
return null;
|
|
}
|
|
|
|
// Collects all keys in these three time slot records as all populations.
|
|
final Set<String> allBatteryHistEntryKeys = new ArraySet<>();
|
|
allBatteryHistEntryKeys.addAll(currentBatteryHistMap.keySet());
|
|
allBatteryHistEntryKeys.addAll(nextBatteryHistMap.keySet());
|
|
allBatteryHistEntryKeys.addAll(nextTwoBatteryHistMap.keySet());
|
|
|
|
double totalConsumePower = 0.0;
|
|
double consumePowerFromOtherUsers = 0f;
|
|
// Calculates all packages diff usage data in a specific time slot.
|
|
for (String key : allBatteryHistEntryKeys) {
|
|
final BatteryHistEntry currentEntry =
|
|
currentBatteryHistMap.getOrDefault(key, EMPTY_BATTERY_HIST_ENTRY);
|
|
final BatteryHistEntry nextEntry =
|
|
nextBatteryHistMap.getOrDefault(key, EMPTY_BATTERY_HIST_ENTRY);
|
|
final BatteryHistEntry nextTwoEntry =
|
|
nextTwoBatteryHistMap.getOrDefault(key, EMPTY_BATTERY_HIST_ENTRY);
|
|
// Cumulative values is a specific time slot for a specific app.
|
|
long foregroundUsageTimeInMs =
|
|
getDiffValue(
|
|
currentEntry.mForegroundUsageTimeInMs,
|
|
nextEntry.mForegroundUsageTimeInMs,
|
|
nextTwoEntry.mForegroundUsageTimeInMs);
|
|
long backgroundUsageTimeInMs =
|
|
getDiffValue(
|
|
currentEntry.mBackgroundUsageTimeInMs,
|
|
nextEntry.mBackgroundUsageTimeInMs,
|
|
nextTwoEntry.mBackgroundUsageTimeInMs);
|
|
double consumePower =
|
|
getDiffValue(
|
|
currentEntry.mConsumePower,
|
|
nextEntry.mConsumePower,
|
|
nextTwoEntry.mConsumePower);
|
|
// Excludes entry since we don't have enough data to calculate.
|
|
if (foregroundUsageTimeInMs == 0
|
|
&& backgroundUsageTimeInMs == 0
|
|
&& consumePower == 0) {
|
|
continue;
|
|
}
|
|
final BatteryHistEntry selectedBatteryEntry =
|
|
selectBatteryHistEntry(currentEntry, nextEntry, nextTwoEntry);
|
|
if (selectedBatteryEntry == null) {
|
|
continue;
|
|
}
|
|
// Forces refine the cumulative value since it may introduce deviation error since we
|
|
// will apply the interpolation arithmetic.
|
|
final float totalUsageTimeInMs =
|
|
foregroundUsageTimeInMs + backgroundUsageTimeInMs;
|
|
if (totalUsageTimeInMs > TOTAL_HOURLY_TIME_THRESHOLD) {
|
|
final float ratio = TOTAL_HOURLY_TIME_THRESHOLD / totalUsageTimeInMs;
|
|
if (DEBUG) {
|
|
Log.w(TAG, String.format("abnormal usage time %d|%d for:\n%s",
|
|
Duration.ofMillis(foregroundUsageTimeInMs).getSeconds(),
|
|
Duration.ofMillis(backgroundUsageTimeInMs).getSeconds(),
|
|
currentEntry));
|
|
}
|
|
foregroundUsageTimeInMs =
|
|
Math.round(foregroundUsageTimeInMs * ratio);
|
|
backgroundUsageTimeInMs =
|
|
Math.round(backgroundUsageTimeInMs * ratio);
|
|
consumePower = consumePower * ratio;
|
|
}
|
|
totalConsumePower += consumePower;
|
|
|
|
final boolean isFromOtherUsers = isConsumedFromOtherUsers(
|
|
currentUserId, workProfileUserId, selectedBatteryEntry);
|
|
if (isFromOtherUsers) {
|
|
consumePowerFromOtherUsers += consumePower;
|
|
} else {
|
|
final BatteryDiffEntry currentBatteryDiffEntry = new BatteryDiffEntry(
|
|
context,
|
|
foregroundUsageTimeInMs,
|
|
backgroundUsageTimeInMs,
|
|
consumePower,
|
|
selectedBatteryEntry);
|
|
if (currentBatteryDiffEntry.isSystemEntry()) {
|
|
systemEntries.add(currentBatteryDiffEntry);
|
|
} else {
|
|
appEntries.add(currentBatteryDiffEntry);
|
|
}
|
|
}
|
|
}
|
|
if (consumePowerFromOtherUsers != 0) {
|
|
systemEntries.add(createOtherUsersEntry(context, consumePowerFromOtherUsers));
|
|
}
|
|
|
|
// If there is no data, return null instead of empty item.
|
|
if (appEntries.isEmpty() && systemEntries.isEmpty()) {
|
|
return null;
|
|
}
|
|
|
|
final BatteryDiffData resultDiffData =
|
|
new BatteryDiffData(appEntries, systemEntries, totalConsumePower);
|
|
return resultDiffData;
|
|
}
|
|
|
|
private static boolean isConsumedFromOtherUsers(
|
|
final int currentUserId,
|
|
final int workProfileUserId,
|
|
final BatteryHistEntry batteryHistEntry) {
|
|
return batteryHistEntry.mConsumerType == ConvertUtils.CONSUMER_TYPE_UID_BATTERY
|
|
&& batteryHistEntry.mUserId != currentUserId
|
|
&& batteryHistEntry.mUserId != workProfileUserId;
|
|
}
|
|
|
|
@Nullable
|
|
private static BatteryDiffData getAccumulatedUsageDiffData(
|
|
final Collection<BatteryDiffData> diffEntryListData) {
|
|
double totalConsumePower = 0f;
|
|
final Map<String, BatteryDiffEntry> diffEntryMap = new HashMap<>();
|
|
final List<BatteryDiffEntry> appEntries = new ArrayList<>();
|
|
final List<BatteryDiffEntry> systemEntries = new ArrayList<>();
|
|
|
|
for (BatteryDiffData diffEntryList : diffEntryListData) {
|
|
if (diffEntryList == null) {
|
|
continue;
|
|
}
|
|
for (BatteryDiffEntry entry : diffEntryList.getAppDiffEntryList()) {
|
|
computeUsageDiffDataPerEntry(entry, diffEntryMap);
|
|
totalConsumePower += entry.mConsumePower;
|
|
}
|
|
for (BatteryDiffEntry entry : diffEntryList.getSystemDiffEntryList()) {
|
|
computeUsageDiffDataPerEntry(entry, diffEntryMap);
|
|
totalConsumePower += entry.mConsumePower;
|
|
}
|
|
}
|
|
|
|
final Collection<BatteryDiffEntry> diffEntryList = diffEntryMap.values();
|
|
for (BatteryDiffEntry entry : diffEntryList) {
|
|
// Sets total daily consume power data into all BatteryDiffEntry.
|
|
entry.setTotalConsumePower(totalConsumePower);
|
|
if (entry.isSystemEntry()) {
|
|
systemEntries.add(entry);
|
|
} else {
|
|
appEntries.add(entry);
|
|
}
|
|
}
|
|
|
|
return diffEntryList.isEmpty() ? null : new BatteryDiffData(appEntries, systemEntries);
|
|
}
|
|
|
|
private static void computeUsageDiffDataPerEntry(
|
|
final BatteryDiffEntry entry,
|
|
final Map<String, BatteryDiffEntry> diffEntryMap) {
|
|
final String key = entry.mBatteryHistEntry.getKey();
|
|
final BatteryDiffEntry oldBatteryDiffEntry = diffEntryMap.get(key);
|
|
// Creates new BatteryDiffEntry if we don't have it.
|
|
if (oldBatteryDiffEntry == null) {
|
|
diffEntryMap.put(key, entry.clone());
|
|
} else {
|
|
// Sums up some field data into the existing one.
|
|
oldBatteryDiffEntry.mForegroundUsageTimeInMs +=
|
|
entry.mForegroundUsageTimeInMs;
|
|
oldBatteryDiffEntry.mBackgroundUsageTimeInMs +=
|
|
entry.mBackgroundUsageTimeInMs;
|
|
oldBatteryDiffEntry.mConsumePower += entry.mConsumePower;
|
|
}
|
|
}
|
|
|
|
// Removes low percentage data and fake usage data, which will be zero value.
|
|
private static void purgeLowPercentageAndFakeData(
|
|
final Context context,
|
|
final Map<Integer, Map<Integer, BatteryDiffData>> resultMap) {
|
|
final Set<CharSequence> backgroundUsageTimeHideList =
|
|
FeatureFactory.getFactory(context)
|
|
.getPowerUsageFeatureProvider(context)
|
|
.getHideBackgroundUsageTimeSet(context);
|
|
final CharSequence[] notAllowShowEntryPackages =
|
|
FeatureFactory.getFactory(context)
|
|
.getPowerUsageFeatureProvider(context)
|
|
.getHideApplicationEntries(context);
|
|
resultMap.keySet().forEach(dailyKey -> {
|
|
final Map<Integer, BatteryDiffData> dailyUsageMap = resultMap.get(dailyKey);
|
|
dailyUsageMap.values().forEach(diffEntryLists -> {
|
|
if (diffEntryLists == null) {
|
|
return;
|
|
}
|
|
purgeLowPercentageAndFakeData(
|
|
diffEntryLists.getAppDiffEntryList(), backgroundUsageTimeHideList,
|
|
notAllowShowEntryPackages);
|
|
purgeLowPercentageAndFakeData(
|
|
diffEntryLists.getSystemDiffEntryList(), backgroundUsageTimeHideList,
|
|
notAllowShowEntryPackages);
|
|
});
|
|
});
|
|
}
|
|
|
|
private static void purgeLowPercentageAndFakeData(
|
|
final List<BatteryDiffEntry> entries,
|
|
final Set<CharSequence> backgroundUsageTimeHideList,
|
|
final CharSequence[] notAllowShowEntryPackages) {
|
|
final Iterator<BatteryDiffEntry> iterator = entries.iterator();
|
|
while (iterator.hasNext()) {
|
|
final BatteryDiffEntry entry = iterator.next();
|
|
final String packageName = entry.getPackageName();
|
|
if (entry.getPercentOfTotal() < PERCENTAGE_OF_TOTAL_THRESHOLD
|
|
|| FAKE_PACKAGE_NAME.equals(packageName)
|
|
|| contains(packageName, notAllowShowEntryPackages)) {
|
|
iterator.remove();
|
|
}
|
|
if (packageName != null
|
|
&& !backgroundUsageTimeHideList.isEmpty()
|
|
&& contains(packageName, backgroundUsageTimeHideList)) {
|
|
entry.mBackgroundUsageTimeInMs = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static boolean isUsageMapValid(
|
|
final Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageMap,
|
|
final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay) {
|
|
if (batteryUsageMap.get(SELECTED_INDEX_ALL) == null
|
|
|| !batteryUsageMap.get(SELECTED_INDEX_ALL).containsKey(SELECTED_INDEX_ALL)) {
|
|
Log.e(TAG, "no [SELECTED_INDEX_ALL][SELECTED_INDEX_ALL] in batteryUsageMap");
|
|
return false;
|
|
}
|
|
for (int dailyIndex = 0; dailyIndex < hourlyBatteryLevelsPerDay.size(); dailyIndex++) {
|
|
if (batteryUsageMap.get(dailyIndex) == null
|
|
|| !batteryUsageMap.get(dailyIndex).containsKey(SELECTED_INDEX_ALL)) {
|
|
Log.e(TAG, "no [" + dailyIndex + "][SELECTED_INDEX_ALL] in batteryUsageMap, "
|
|
+ "daily size is: " + hourlyBatteryLevelsPerDay.size());
|
|
return false;
|
|
}
|
|
if (hourlyBatteryLevelsPerDay.get(dailyIndex) == null) {
|
|
continue;
|
|
}
|
|
final List<Long> timestamps = hourlyBatteryLevelsPerDay.get(dailyIndex).getTimestamps();
|
|
// Length of hourly usage map should be the length of hourly level data - 1.
|
|
for (int hourlyIndex = 0; hourlyIndex < timestamps.size() - 1; hourlyIndex++) {
|
|
if (!batteryUsageMap.get(dailyIndex).containsKey(hourlyIndex)) {
|
|
Log.e(TAG, "no [" + dailyIndex + "][" + hourlyIndex + "] in batteryUsageMap, "
|
|
+ "hourly size is: " + (timestamps.size() - 1));
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
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 boolean contains(String target, Set<CharSequence> packageNames) {
|
|
if (target != null && packageNames != null) {
|
|
for (CharSequence packageName : packageNames) {
|
|
if (TextUtils.equals(target, packageName)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static long getDiffValue(long v1, long v2, long v3) {
|
|
return (v2 > v1 ? v2 - v1 : 0) + (v3 > v2 ? v3 - v2 : 0);
|
|
}
|
|
|
|
private static double getDiffValue(double v1, double v2, double v3) {
|
|
return (v2 > v1 ? v2 - v1 : 0) + (v3 > v2 ? v3 - v2 : 0);
|
|
}
|
|
|
|
@Nullable
|
|
private static BatteryHistEntry selectBatteryHistEntry(
|
|
final BatteryHistEntry... batteryHistEntries) {
|
|
for (BatteryHistEntry entry : batteryHistEntries) {
|
|
if (entry != null && entry != EMPTY_BATTERY_HIST_ENTRY) {
|
|
return entry;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static BatteryDiffEntry createOtherUsersEntry(
|
|
Context context, final double consumePower) {
|
|
final ContentValues values = new ContentValues();
|
|
values.put(BatteryHistEntry.KEY_UID, BatteryUtils.UID_OTHER_USERS);
|
|
values.put(BatteryHistEntry.KEY_USER_ID, BatteryUtils.UID_OTHER_USERS);
|
|
values.put(BatteryHistEntry.KEY_CONSUMER_TYPE, ConvertUtils.CONSUMER_TYPE_UID_BATTERY);
|
|
// We will show the percentage for the "other users" item only, the aggregated
|
|
// running time information is useless for users to identify individual apps.
|
|
final BatteryDiffEntry batteryDiffEntry = new BatteryDiffEntry(
|
|
context,
|
|
/*foregroundUsageTimeInMs=*/ 0,
|
|
/*backgroundUsageTimeInMs=*/ 0,
|
|
consumePower,
|
|
new BatteryHistEntry(values));
|
|
return batteryDiffEntry;
|
|
}
|
|
|
|
private static void log(Context context, final String content, final long timestamp,
|
|
final BatteryHistEntry entry) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, String.format(entry != null ? "%s %s:\n%s" : "%s %s:%s",
|
|
utcToLocalTime(context, timestamp), content, entry));
|
|
}
|
|
}
|
|
|
|
// Compute diff map and loads all items (icon and label) in the background.
|
|
private static final class ComputeUsageMapAndLoadItemsTask
|
|
extends AsyncTask<Void, Void, Map<Integer, Map<Integer, BatteryDiffData>>> {
|
|
|
|
private Context mApplicationContext;
|
|
private Handler mHandler;
|
|
private UsageMapAsyncResponse mAsyncResponseDelegate;
|
|
private List<BatteryLevelData.PeriodBatteryLevelData> mHourlyBatteryLevelsPerDay;
|
|
private Map<Long, Map<String, BatteryHistEntry>> mBatteryHistoryMap;
|
|
|
|
private ComputeUsageMapAndLoadItemsTask(
|
|
Context context,
|
|
Handler handler,
|
|
final UsageMapAsyncResponse asyncResponseDelegate,
|
|
final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay,
|
|
final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) {
|
|
mApplicationContext = context.getApplicationContext();
|
|
mHandler = handler;
|
|
mAsyncResponseDelegate = asyncResponseDelegate;
|
|
mHourlyBatteryLevelsPerDay = hourlyBatteryLevelsPerDay;
|
|
mBatteryHistoryMap = batteryHistoryMap;
|
|
}
|
|
|
|
@Override
|
|
protected Map<Integer, Map<Integer, BatteryDiffData>> doInBackground(Void... voids) {
|
|
if (mApplicationContext == null
|
|
|| mHandler == null
|
|
|| mAsyncResponseDelegate == null
|
|
|| mBatteryHistoryMap == null
|
|
|| mHourlyBatteryLevelsPerDay == null) {
|
|
Log.e(TAG, "invalid input for ComputeUsageMapAndLoadItemsTask()");
|
|
return null;
|
|
}
|
|
final long startTime = System.currentTimeMillis();
|
|
final Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageMap =
|
|
getBatteryUsageMap(
|
|
mApplicationContext, mHourlyBatteryLevelsPerDay, mBatteryHistoryMap);
|
|
if (batteryUsageMap != null) {
|
|
// Pre-loads each BatteryDiffEntry relative icon and label for all slots.
|
|
final BatteryDiffData batteryUsageMapForAll =
|
|
batteryUsageMap.get(SELECTED_INDEX_ALL).get(SELECTED_INDEX_ALL);
|
|
if (batteryUsageMapForAll != null) {
|
|
batteryUsageMapForAll.getAppDiffEntryList().forEach(
|
|
entry -> entry.loadLabelAndIcon());
|
|
batteryUsageMapForAll.getSystemDiffEntryList().forEach(
|
|
entry -> entry.loadLabelAndIcon());
|
|
}
|
|
}
|
|
Log.d(TAG, String.format("execute ComputeUsageMapAndLoadItemsTask in %d/ms",
|
|
(System.currentTimeMillis() - startTime)));
|
|
return batteryUsageMap;
|
|
}
|
|
|
|
@Override
|
|
protected void onPostExecute(
|
|
final Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageMap) {
|
|
mApplicationContext = null;
|
|
mHourlyBatteryLevelsPerDay = null;
|
|
mBatteryHistoryMap = null;
|
|
// Post results back to main thread to refresh UI.
|
|
if (mHandler != null && mAsyncResponseDelegate != null) {
|
|
mHandler.post(() -> {
|
|
mAsyncResponseDelegate.onBatteryUsageMapLoaded(batteryUsageMap);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|