From 0dc8d58de58e6bb750740b70b4a120ccebe7c23f Mon Sep 17 00:00:00 2001 From: Kuan Wang Date: Tue, 26 Jul 2022 13:22:39 +0800 Subject: [PATCH] Add the async task to compute diff usage data and load labels and icons. Bug: 236101687 Test: make RunSettingsRoboTests Change-Id: Ie24ea89fa6cfd351c73e64de40e2c9315867af9a --- .../BatteryChartPreferenceControllerV2.java | 3 +- .../batteryusage/BatteryDiffData.java | 34 +- .../fuelgauge/batteryusage/DataProcessor.java | 550 +++++++++++++++++- .../batteryusage/DataProcessorTest.java | 504 +++++++++++++++- 4 files changed, 1072 insertions(+), 19 deletions(-) diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceControllerV2.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceControllerV2.java index f7470fbd848..2932984b550 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceControllerV2.java +++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceControllerV2.java @@ -266,8 +266,9 @@ public class BatteryChartPreferenceControllerV2 extends AbstractPreferenceContro final Map> batteryHistoryMap) { Log.d(TAG, "setBatteryHistoryMap() " + (batteryHistoryMap == null ? "null" : ("size=" + batteryHistoryMap.size()))); + // TODO: implement the callback function. final BatteryLevelData batteryLevelData = - DataProcessor.getBatteryLevelData(mContext, batteryHistoryMap); + DataProcessor.getBatteryLevelData(mContext, mHandler, batteryHistoryMap, null); Log.d(TAG, "getBatteryLevelData: " + batteryLevelData); if (batteryLevelData == null) { mDailyViewModel = null; diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffData.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffData.java index 5743cac6017..b5d4dde883c 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffData.java +++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffData.java @@ -16,7 +16,9 @@ package com.android.settings.fuelgauge.batteryusage; -import java.util.ArrayList; +import androidx.annotation.NonNull; + +import java.util.Collections; import java.util.List; /** Wraps the battery usage diff data for each entry used for battery usage app list. */ @@ -24,10 +26,24 @@ public class BatteryDiffData { private final List mAppEntries; private final List mSystemEntries; + /** Constructor for the diff entries which already have totalConsumePower value. */ public BatteryDiffData( - List appDiffEntries, List systemDiffEntries) { - mAppEntries = appDiffEntries == null ? new ArrayList<>() : appDiffEntries; - mSystemEntries = systemDiffEntries == null ? new ArrayList<>() : systemDiffEntries; + @NonNull List appDiffEntries, + @NonNull List systemDiffEntries) { + mAppEntries = appDiffEntries; + mSystemEntries = systemDiffEntries; + sortEntries(); + } + + /** Constructor for the diff entries which have not set totalConsumePower value. */ + public BatteryDiffData( + @NonNull List appDiffEntries, + @NonNull List systemDiffEntries, + final double totalConsumePower) { + mAppEntries = appDiffEntries; + mSystemEntries = systemDiffEntries; + setTotalConsumePowerForAllEntries(totalConsumePower); + sortEntries(); } public List getAppDiffEntryList() { @@ -38,9 +54,15 @@ public class BatteryDiffData { return mSystemEntries; } - /** Sets total consume power for each entry. */ - public void setTotalConsumePowerForAllEntries(double totalConsumePower) { + // Sets total consume power for each entry. + private void setTotalConsumePowerForAllEntries(final double totalConsumePower) { mAppEntries.forEach(diffEntry -> diffEntry.setTotalConsumePower(totalConsumePower)); mSystemEntries.forEach(diffEntry -> diffEntry.setTotalConsumePower(totalConsumePower)); } + + // Sorts entries based on consumed percentage. + private void sortEntries() { + Collections.sort(mAppEntries, BatteryDiffEntry.COMPARATOR); + Collections.sort(mSystemEntries, BatteryDiffEntry.COMPARATOR); + } } diff --git a/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java b/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java index d8650a651eb..a004a519423 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java +++ b/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java @@ -18,21 +18,36 @@ 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 @@ -43,10 +58,27 @@ public final class DataProcessor { private static final String TAG = "DataProcessor"; private static final int MIN_DAILY_DATA_SIZE = 2; private static final int MIN_TIMESTAMP_DATA_SIZE = 2; + // 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 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> batteryUsageMap); + } + private DataProcessor() { } @@ -58,11 +90,14 @@ public final class DataProcessor { @Nullable public static BatteryLevelData getBatteryLevelData( Context context, - @Nullable final Map> batteryHistoryMap) { + @Nullable Handler handler, + @Nullable final Map> 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> processedBatteryHistoryMap = getHistoryMapWithExpectedTimestamps(context, batteryHistoryMap); @@ -70,11 +105,33 @@ public final class DataProcessor { final BatteryLevelData batteryLevelData = getLevelDataThroughProcessedHistoryMap(context, processedBatteryHistoryMap); - //TODO: Add the async task to compute diff usage data and load labels and icons. + // 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 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. @@ -187,7 +244,7 @@ public final class DataProcessor { @VisibleForTesting static boolean isFromFullCharge(@Nullable final Map entryList) { if (entryList == null) { - Log.d(TAG, "entryList is nul in isFromFullCharge()"); + Log.d(TAG, "entryList is null in isFromFullCharge()"); return false; } final List entryKeys = new ArrayList<>(entryList.keySet()); @@ -205,14 +262,14 @@ public final class DataProcessor { static long[] findNearestTimestamp(final List 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) { + 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]; @@ -224,7 +281,7 @@ public final class DataProcessor { * timezone. */ @VisibleForTesting - static long getTimestampOfNextDay(long timestamp) { + static long getTimestampOfNextDay(long timestamp) { final Calendar nextDayCalendar = Calendar.getInstance(); nextDayCalendar.setTimeInMillis(timestamp); nextDayCalendar.add(Calendar.DAY_OF_YEAR, 1); @@ -234,6 +291,40 @@ public final class DataProcessor { return nextDayCalendar.getTimeInMillis(); } + /** + * @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> getBatteryUsageMap( + final Context context, + final List hourlyBatteryLevelsPerDay, + final Map> batteryHistoryMap) { + if (batteryHistoryMap.isEmpty()) { + return null; + } + final Map> 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. @@ -458,11 +549,454 @@ public final class DataProcessor { return Math.round(batteryLevelCounter / entryMap.size()); } - private static void log(Context context, String content, long timestamp, - BatteryHistEntry entry) { + private static void insertHourlyUsageDiffData( + Context context, + final List hourlyBatteryLevelsPerDay, + final Map> batteryHistoryMap, + final Map> 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 dailyDiffMap = new HashMap<>(); + resultMap.put(dailyIndex, dailyDiffMap); + if (hourlyBatteryLevelsPerDay.get(dailyIndex) == null) { + continue; + } + final List 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 hourlyBatteryLevelsPerDay, + final Map> resultMap) { + for (int index = 0; index < hourlyBatteryLevelsPerDay.size(); index++) { + Map 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> resultMap) { + final List diffDataList = new ArrayList<>(); + resultMap.keySet().forEach( + key -> diffDataList.add(resultMap.get(key).get(SELECTED_INDEX_ALL))); + final Map 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 timestamps, + final Map> batteryHistoryMap) { + final List appEntries = new ArrayList<>(); + final List 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 currentBatteryHistMap = + batteryHistoryMap.getOrDefault(currentTimestamp, EMPTY_BATTERY_MAP); + final Map nextBatteryHistMap = + batteryHistoryMap.getOrDefault(nextTimestamp, EMPTY_BATTERY_MAP); + final Map 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 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 diffEntryListData) { + double totalConsumePower = 0f; + final Map diffEntryMap = new HashMap<>(); + final List appEntries = new ArrayList<>(); + final List 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 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 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> resultMap) { + final Set backgroundUsageTimeHideList = + FeatureFactory.getFactory(context) + .getPowerUsageFeatureProvider(context) + .getHideBackgroundUsageTimeSet(context); + final CharSequence[] notAllowShowEntryPackages = + FeatureFactory.getFactory(context) + .getPowerUsageFeatureProvider(context) + .getHideApplicationEntries(context); + resultMap.keySet().forEach(dailyKey -> { + final Map 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 entries, + final Set backgroundUsageTimeHideList, + final CharSequence[] notAllowShowEntryPackages) { + final Iterator 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> batteryUsageMap, + final List hourlyBatteryLevelsPerDay) { + if (batteryUsageMap.get(SELECTED_INDEX_ALL) == null + || batteryUsageMap.get(SELECTED_INDEX_ALL).get(SELECTED_INDEX_ALL) == null) { + 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 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 boolean contains(String target, Set 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>> { + + private Context mApplicationContext; + private Handler mHandler; + private UsageMapAsyncResponse mAsyncResponseDelegate; + private List mHourlyBatteryLevelsPerDay; + private Map> mBatteryHistoryMap; + + private ComputeUsageMapAndLoadItemsTask( + Context context, + Handler handler, + final UsageMapAsyncResponse asyncResponseDelegate, + final List hourlyBatteryLevelsPerDay, + final Map> batteryHistoryMap) { + mApplicationContext = context.getApplicationContext(); + mHandler = handler; + mAsyncResponseDelegate = asyncResponseDelegate; + mHourlyBatteryLevelsPerDay = hourlyBatteryLevelsPerDay; + mBatteryHistoryMap = batteryHistoryMap; + } + + @Override + protected Map> 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> 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> 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); + }); + } + } + } } diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java index 0306c4b2afa..1b1e469d1d9 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java @@ -19,11 +19,16 @@ package com.android.settings.fuelgauge.batteryusage; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; import android.content.ContentValues; import android.content.Context; import android.text.format.DateUtils; +import com.android.settings.fuelgauge.BatteryUtils; +import com.android.settings.fuelgauge.PowerUsageFeatureProvider; +import com.android.settings.testutils.FakeFeatureFactory; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -35,6 +40,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.TimeZone; @@ -45,18 +51,30 @@ public class DataProcessorTest { private Context mContext; + private FakeFeatureFactory mFeatureFactory; + private PowerUsageFeatureProvider mPowerUsageFeatureProvider; + @Before public void setUp() { MockitoAnnotations.initMocks(this); TimeZone.setDefault(TimeZone.getTimeZone("GMT+8")); mContext = spy(RuntimeEnvironment.application); + mFeatureFactory = FakeFeatureFactory.setupForTest(); + mPowerUsageFeatureProvider = mFeatureFactory.powerUsageFeatureProvider; } @Test public void getBatteryLevelData_emptyHistoryMap_returnNull() { - assertThat(DataProcessor.getBatteryLevelData(mContext, null)).isNull(); - assertThat(DataProcessor.getBatteryLevelData(mContext, new HashMap<>())).isNull(); + assertThat(DataProcessor.getBatteryLevelData( + mContext, + /*handler=*/ null, + /*batteryHistoryMap=*/ null, + /*asyncResponseDelegate=*/ null)) + .isNull(); + assertThat(DataProcessor.getBatteryLevelData( + mContext, /*handler=*/ null, new HashMap<>(), /*asyncResponseDelegate=*/ null)) + .isNull(); } @Test @@ -67,7 +85,9 @@ public class DataProcessorTest { final Map> batteryHistoryMap = createHistoryMap(timestamps, levels); - assertThat(DataProcessor.getBatteryLevelData(mContext, batteryHistoryMap)).isNull(); + assertThat(DataProcessor.getBatteryLevelData( + mContext, /*handler=*/ null, batteryHistoryMap, /*asyncResponseDelegate=*/ null)) + .isNull(); } @Test @@ -79,7 +99,11 @@ public class DataProcessorTest { createHistoryMap(timestamps, levels); final BatteryLevelData resultData = - DataProcessor.getBatteryLevelData(mContext, batteryHistoryMap); + DataProcessor.getBatteryLevelData( + mContext, + /*handler=*/ null, + batteryHistoryMap, + /*asyncResponseDelegate=*/ null); final List expectedDailyTimestamps = List.of(timestamps[0], timestamps[2]); final List expectedDailyLevels = List.of(levels[0], levels[2]); @@ -377,6 +401,450 @@ public class DataProcessorTest { .isEqualTo(1640966400000L); } + @Test + public void getBatteryUsageMap_emptyHistoryMap_returnNull() { + final List hourlyBatteryLevelsPerDay = + new ArrayList<>(); + hourlyBatteryLevelsPerDay.add( + new BatteryLevelData.PeriodBatteryLevelData(new ArrayList<>(), new ArrayList<>())); + + assertThat(DataProcessor.getBatteryUsageMap( + mContext, hourlyBatteryLevelsPerDay, new HashMap<>())).isNull(); + } + + @Test + public void getBatteryUsageMap_emptyHourlyData_returnNull() { + final long[] timestamps = {1000000L, 2000000L}; + final int[] levels = {100, 99}; + final Map> batteryHistoryMap = + createHistoryMap(timestamps, levels); + + assertThat(DataProcessor.getBatteryUsageMap( + mContext, new ArrayList<>(), batteryHistoryMap)).isNull(); + } + + @Test + public void getBatteryUsageMap_returnsExpectedResult() { + final long[] batteryHistoryKeys = new long[]{ + 1641045600000L, // 2022-01-01 22:00:00 + 1641049200000L, // 2022-01-01 23:00:00 + 1641052800000L, // 2022-01-02 00:00:00 + 1641056400000L, // 2022-01-02 01:00:00 + 1641060000000L, // 2022-01-02 02:00:00 + }; + final Map> batteryHistoryMap = new HashMap<>(); + final int currentUserId = mContext.getUserId(); + final BatteryHistEntry fakeEntry = createBatteryHistEntry( + ConvertUtils.FAKE_PACKAGE_NAME, "fake_label", /*consumePower=*/ 0, /*uid=*/ 0L, + currentUserId, ConvertUtils.CONSUMER_TYPE_UID_BATTERY, + /*foregroundUsageTimeInMs=*/ 0L, /*backgroundUsageTimeInMs=*/ 0L); + // Adds the index = 0 data. + Map entryMap = new HashMap<>(); + BatteryHistEntry entry = createBatteryHistEntry( + "package1", "label1", /*consumePower=*/ 5.0, /*uid=*/ 1L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 10L, + /*backgroundUsageTimeInMs=*/ 20L); + entryMap.put(entry.getKey(), entry); + entryMap.put(fakeEntry.getKey(), fakeEntry); + batteryHistoryMap.put(batteryHistoryKeys[0], entryMap); + // Adds the index = 1 data. + entryMap = new HashMap<>(); + entryMap.put(fakeEntry.getKey(), fakeEntry); + batteryHistoryMap.put(batteryHistoryKeys[1], entryMap); + // Adds the index = 2 data. + entryMap = new HashMap<>(); + entry = createBatteryHistEntry( + "package2", "label2", /*consumePower=*/ 20.0, /*uid=*/ 2L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 15L, + 25L); + entryMap.put(entry.getKey(), entry); + entryMap.put(fakeEntry.getKey(), fakeEntry); + batteryHistoryMap.put(batteryHistoryKeys[2], entryMap); + // Adds the index = 3 data. + entryMap = new HashMap<>(); + entry = createBatteryHistEntry( + "package2", "label2", /*consumePower=*/ 40.0, /*uid=*/ 2L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 25L, + /*backgroundUsageTimeInMs=*/ 35L); + entryMap.put(entry.getKey(), entry); + entry = createBatteryHistEntry( + "package2", "label2", /*consumePower=*/ 10.0, /*uid=*/ 3L, currentUserId, + ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY, /*foregroundUsageTimeInMs=*/ 40L, + /*backgroundUsageTimeInMs=*/ 50L); + entryMap.put(entry.getKey(), entry); + entry = createBatteryHistEntry( + "package3", "label3", /*consumePower=*/ 15.0, /*uid=*/ 4L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 5L, + /*backgroundUsageTimeInMs=*/ 5L); + entryMap.put(entry.getKey(), entry); + entryMap.put(fakeEntry.getKey(), fakeEntry); + batteryHistoryMap.put(batteryHistoryKeys[3], entryMap); + // Adds the index = 4 data. + entryMap = new HashMap<>(); + entry = createBatteryHistEntry( + "package2", "label2", /*consumePower=*/ 40.0, /*uid=*/ 2L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 30L, + /*backgroundUsageTimeInMs=*/ 40L); + entryMap.put(entry.getKey(), entry); + entry = createBatteryHistEntry( + "package2", "label2", /*consumePower=*/ 20.0, /*uid=*/ 3L, currentUserId, + ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY, /*foregroundUsageTimeInMs=*/ 50L, + /*backgroundUsageTimeInMs=*/ 60L); + entryMap.put(entry.getKey(), entry); + entry = createBatteryHistEntry( + "package3", "label3", /*consumePower=*/ 40.0, /*uid=*/ 4L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 5L, + /*backgroundUsageTimeInMs=*/ 5L); + entryMap.put(entry.getKey(), entry); + entryMap.put(fakeEntry.getKey(), fakeEntry); + batteryHistoryMap.put(batteryHistoryKeys[4], entryMap); + final List hourlyBatteryLevelsPerDay = + new ArrayList<>(); + // Adds the day 1 data. + List timestamps = + List.of(batteryHistoryKeys[0], batteryHistoryKeys[2]); + final List levels = List.of(100, 100); + hourlyBatteryLevelsPerDay.add( + new BatteryLevelData.PeriodBatteryLevelData(timestamps, levels)); + // Adds the day 2 data. + timestamps = List.of(batteryHistoryKeys[2], batteryHistoryKeys[4]); + hourlyBatteryLevelsPerDay.add( + new BatteryLevelData.PeriodBatteryLevelData(timestamps, levels)); + + final Map> resultMap = + DataProcessor.getBatteryUsageMap( + mContext, hourlyBatteryLevelsPerDay, batteryHistoryMap); + + BatteryDiffData resultDiffData = + resultMap + .get(DataProcessor.SELECTED_INDEX_ALL) + .get(DataProcessor.SELECTED_INDEX_ALL); + assertBatteryDiffEntry( + resultDiffData.getAppDiffEntryList().get(0), currentUserId, /*uid=*/ 2L, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*consumePercentage=*/ 40.0, + /*foregroundUsageTimeInMs=*/ 30, /*backgroundUsageTimeInMs=*/ 40); + assertBatteryDiffEntry( + resultDiffData.getAppDiffEntryList().get(1), currentUserId, /*uid=*/ 4L, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*consumePercentage=*/ 40.0, + /*foregroundUsageTimeInMs=*/ 5, /*backgroundUsageTimeInMs=*/ 5); + assertBatteryDiffEntry( + resultDiffData.getSystemDiffEntryList().get(0), currentUserId, /*uid=*/ 3L, + ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY, /*consumePercentage=*/ 20.0, + /*foregroundUsageTimeInMs=*/ 50, /*backgroundUsageTimeInMs=*/ 60); + resultDiffData = resultMap.get(0).get(DataProcessor.SELECTED_INDEX_ALL); + assertBatteryDiffEntry( + resultDiffData.getAppDiffEntryList().get(0), currentUserId, /*uid=*/ 2L, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*consumePercentage=*/ 100.0, + /*foregroundUsageTimeInMs=*/ 15, /*backgroundUsageTimeInMs=*/ 25); + resultDiffData = resultMap.get(1).get(DataProcessor.SELECTED_INDEX_ALL); + assertBatteryDiffEntry( + resultDiffData.getAppDiffEntryList().get(0), currentUserId, /*uid=*/ 4L, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*consumePercentage=*/ 50.0, + /*foregroundUsageTimeInMs=*/ 5, /*backgroundUsageTimeInMs=*/ 5); + assertBatteryDiffEntry( + resultDiffData.getAppDiffEntryList().get(1), currentUserId, /*uid=*/ 2L, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*consumePercentage=*/ 25.0, + /*foregroundUsageTimeInMs=*/ 15, /*backgroundUsageTimeInMs=*/ 15); + assertBatteryDiffEntry( + resultDiffData.getSystemDiffEntryList().get(0), currentUserId, /*uid=*/ 3L, + ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY, /*consumePercentage=*/ 25.0, + /*foregroundUsageTimeInMs=*/ 50, /*backgroundUsageTimeInMs=*/ 60); + } + + @Test + public void getBatteryUsageMap_multipleUsers_returnsExpectedResult() { + final long[] batteryHistoryKeys = new long[]{ + 1641052800000L, // 2022-01-02 00:00:00 + 1641056400000L, // 2022-01-02 01:00:00 + 1641060000000L // 2022-01-02 02:00:00 + }; + final Map> batteryHistoryMap = new HashMap<>(); + final int currentUserId = mContext.getUserId(); + // Adds the index = 0 data. + Map entryMap = new HashMap<>(); + BatteryHistEntry entry = createBatteryHistEntry( + "package1", "label1", /*consumePower=*/ 5.0, /*uid=*/ 1L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 10L, + /*backgroundUsageTimeInMs=*/ 20L); + entryMap.put(entry.getKey(), entry); + entry = createBatteryHistEntry( + "package1", "label1", /*consumePower=*/ 10.0, /*uid=*/ 2L, currentUserId + 1, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 10L, + /*backgroundUsageTimeInMs=*/ 20L); + entryMap.put(entry.getKey(), entry); + entry = createBatteryHistEntry( + "package2", "label2", /*consumePower=*/ 5.0, /*uid=*/ 3L, currentUserId + 2, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 20L, + /*backgroundUsageTimeInMs=*/ 30L); + entryMap.put(entry.getKey(), entry); + batteryHistoryMap.put(batteryHistoryKeys[0], entryMap); + // Adds the index = 1 data. + entryMap = new HashMap<>(); + entry = createBatteryHistEntry( + "package1", "label1", /*consumePower=*/ 15.0, /*uid=*/ 1L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 20L, + /*backgroundUsageTimeInMs=*/ 30L); + entryMap.put(entry.getKey(), entry); + entry = createBatteryHistEntry( + "package1", "label1", /*consumePower=*/ 30.0, /*uid=*/ 2L, currentUserId + 1, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 10L, + /*backgroundUsageTimeInMs=*/ 20L); + entryMap.put(entry.getKey(), entry); + entry = createBatteryHistEntry( + "package2", "label2", /*consumePower=*/ 15.0, /*uid=*/ 3L, currentUserId + 2, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 30L, + /*backgroundUsageTimeInMs=*/ 30L); + entryMap.put(entry.getKey(), entry); + batteryHistoryMap.put(batteryHistoryKeys[1], entryMap); + // Adds the index = 2 data. + entryMap = new HashMap<>(); + entry = createBatteryHistEntry( + "package1", "label1", /*consumePower=*/ 25.0, /*uid=*/ 1L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 20L, + /*backgroundUsageTimeInMs=*/ 30L); + entryMap.put(entry.getKey(), entry); + entry = createBatteryHistEntry( + "package1", "label1", /*consumePower=*/ 50.0, /*uid=*/ 2L, currentUserId + 1, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 20L, + /*backgroundUsageTimeInMs=*/ 20L); + entryMap.put(entry.getKey(), entry); + entry = createBatteryHistEntry( + "package2", "label2", /*consumePower=*/ 25.0, /*uid=*/ 3L, currentUserId + 2, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 30L, + /*backgroundUsageTimeInMs=*/ 30L); + entryMap.put(entry.getKey(), entry); + batteryHistoryMap.put(batteryHistoryKeys[2], entryMap); + final List hourlyBatteryLevelsPerDay = + new ArrayList<>(); + List timestamps = List.of(batteryHistoryKeys[0], batteryHistoryKeys[2]); + final List levels = List.of(100, 100); + hourlyBatteryLevelsPerDay.add( + new BatteryLevelData.PeriodBatteryLevelData(timestamps, levels)); + + final Map> resultMap = + DataProcessor.getBatteryUsageMap( + mContext, hourlyBatteryLevelsPerDay, batteryHistoryMap); + + final BatteryDiffData resultDiffData = + resultMap + .get(DataProcessor.SELECTED_INDEX_ALL) + .get(DataProcessor.SELECTED_INDEX_ALL); + assertBatteryDiffEntry( + resultDiffData.getAppDiffEntryList().get(0), currentUserId, /*uid=*/ 1L, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*consumePercentage=*/ 25.0, + /*foregroundUsageTimeInMs=*/ 10, /*backgroundUsageTimeInMs=*/ 10); + assertBatteryDiffEntry( + resultDiffData.getSystemDiffEntryList().get(0), BatteryUtils.UID_OTHER_USERS, + /*uid=*/ BatteryUtils.UID_OTHER_USERS, ConvertUtils.CONSUMER_TYPE_UID_BATTERY, + /*consumePercentage=*/ 75.0, /*foregroundUsageTimeInMs=*/ 0, + /*backgroundUsageTimeInMs=*/ 0); + assertThat(resultMap.get(0).get(0)).isNotNull(); + assertThat(resultMap.get(0).get(DataProcessor.SELECTED_INDEX_ALL)).isNotNull(); + } + + @Test + public void getBatteryUsageMap_usageTimeExceed_returnsExpectedResult() { + final long[] batteryHistoryKeys = new long[]{ + 1641052800000L, // 2022-01-02 00:00:00 + 1641056400000L, // 2022-01-02 01:00:00 + 1641060000000L // 2022-01-02 02:00:00 + }; + final Map> batteryHistoryMap = new HashMap<>(); + final int currentUserId = mContext.getUserId(); + // Adds the index = 0 data. + Map entryMap = new HashMap<>(); + BatteryHistEntry entry = createBatteryHistEntry( + "package1", "label1", /*consumePower=*/ 0, /*uid=*/ 1L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 0L, + /*backgroundUsageTimeInMs=*/ 0L); + entryMap.put(entry.getKey(), entry); + batteryHistoryMap.put(batteryHistoryKeys[0], entryMap); + // Adds the index = 1 data. + entryMap = new HashMap<>(); + entry = createBatteryHistEntry( + "package1", "label1", /*consumePower=*/ 0, /*uid=*/ 1L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 0L, + /*backgroundUsageTimeInMs=*/ 0L); + entryMap.put(entry.getKey(), entry); + batteryHistoryMap.put(batteryHistoryKeys[1], entryMap); + // Adds the index = 2 data. + entryMap = new HashMap<>(); + entry = createBatteryHistEntry( + "package1", "label1", /*consumePower=*/ 500.0, /*uid=*/ 1L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 3600000L, + /*backgroundUsageTimeInMs=*/ 7200000L); + entryMap.put(entry.getKey(), entry); + batteryHistoryMap.put(batteryHistoryKeys[2], entryMap); + final List hourlyBatteryLevelsPerDay = + new ArrayList<>(); + List timestamps = List.of(batteryHistoryKeys[0], batteryHistoryKeys[2]); + final List levels = List.of(100, 100); + hourlyBatteryLevelsPerDay.add( + new BatteryLevelData.PeriodBatteryLevelData(timestamps, levels)); + + final Map> resultMap = + DataProcessor.getBatteryUsageMap( + mContext, hourlyBatteryLevelsPerDay, batteryHistoryMap); + + final BatteryDiffData resultDiffData = + resultMap + .get(DataProcessor.SELECTED_INDEX_ALL) + .get(DataProcessor.SELECTED_INDEX_ALL); + // Verifies the clipped usage time. + final float ratio = (float) (7200) / (float) (3600 + 7200); + final BatteryDiffEntry resultEntry = resultDiffData.getAppDiffEntryList().get(0); + assertThat(resultEntry.mForegroundUsageTimeInMs) + .isEqualTo(Math.round(entry.mForegroundUsageTimeInMs * ratio)); + assertThat(resultEntry.mBackgroundUsageTimeInMs) + .isEqualTo(Math.round(entry.mBackgroundUsageTimeInMs * ratio)); + assertThat(resultEntry.mConsumePower) + .isEqualTo(entry.mConsumePower * ratio); + assertThat(resultMap.get(0).get(0)).isNotNull(); + assertThat(resultMap.get(0).get(DataProcessor.SELECTED_INDEX_ALL)).isNotNull(); + } + + @Test + public void getBatteryUsageMap_hideApplicationEntries_returnsExpectedResult() { + final long[] batteryHistoryKeys = new long[]{ + 1641052800000L, // 2022-01-02 00:00:00 + 1641056400000L, // 2022-01-02 01:00:00 + 1641060000000L // 2022-01-02 02:00:00 + }; + final Map> batteryHistoryMap = new HashMap<>(); + final int currentUserId = mContext.getUserId(); + // Adds the index = 0 data. + Map entryMap = new HashMap<>(); + BatteryHistEntry entry = createBatteryHistEntry( + "package1", "label1", /*consumePower=*/ 0, /*uid=*/ 1L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 0L, + /*backgroundUsageTimeInMs=*/ 0L); + entryMap.put(entry.getKey(), entry); + entry = createBatteryHistEntry( + "package2", "label2", /*consumePower=*/ 0, /*uid=*/ 2L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 0L, + /*backgroundUsageTimeInMs=*/ 0L); + entryMap.put(entry.getKey(), entry); + batteryHistoryMap.put(batteryHistoryKeys[0], entryMap); + // Adds the index = 1 data. + entryMap = new HashMap<>(); + entry = createBatteryHistEntry( + "package1", "label1", /*consumePower=*/ 0, /*uid=*/ 1L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 0L, + /*backgroundUsageTimeInMs=*/ 0L); + entryMap.put(entry.getKey(), entry); + entry = createBatteryHistEntry( + "package2", "label2", /*consumePower=*/ 0, /*uid=*/ 2L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 0L, + /*backgroundUsageTimeInMs=*/ 0L); + entryMap.put(entry.getKey(), entry); + batteryHistoryMap.put(batteryHistoryKeys[1], entryMap); + // Adds the index = 2 data. + entryMap = new HashMap<>(); + entry = createBatteryHistEntry( + "package1", "label1", /*consumePower=*/ 10.0, /*uid=*/ 1L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 10L, + /*backgroundUsageTimeInMs=*/ 20L); + entryMap.put(entry.getKey(), entry); + entry = createBatteryHistEntry( + "package2", "label2", /*consumePower=*/ 10.0, /*uid=*/ 2L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 10L, + /*backgroundUsageTimeInMs=*/ 20L); + entryMap.put(entry.getKey(), entry); + batteryHistoryMap.put(batteryHistoryKeys[2], entryMap); + final List hourlyBatteryLevelsPerDay = + new ArrayList<>(); + List timestamps = List.of(batteryHistoryKeys[0], batteryHistoryKeys[2]); + final List levels = List.of(100, 100); + hourlyBatteryLevelsPerDay.add( + new BatteryLevelData.PeriodBatteryLevelData(timestamps, levels)); + when(mPowerUsageFeatureProvider.getHideApplicationEntries(mContext)) + .thenReturn(new CharSequence[]{"package1"}); + + final Map> resultMap = + DataProcessor.getBatteryUsageMap( + mContext, hourlyBatteryLevelsPerDay, batteryHistoryMap); + + final BatteryDiffData resultDiffData = + resultMap + .get(DataProcessor.SELECTED_INDEX_ALL) + .get(DataProcessor.SELECTED_INDEX_ALL); + assertBatteryDiffEntry( + resultDiffData.getAppDiffEntryList().get(0), currentUserId, /*uid=*/ 2L, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*consumePercentage=*/ 50.0, + /*foregroundUsageTimeInMs=*/ 10, /*backgroundUsageTimeInMs=*/ 20); + } + + @Test + public void getBatteryUsageMap_hideBackgroundUsageTime_returnsExpectedResult() { + final long[] batteryHistoryKeys = new long[]{ + 1641052800000L, // 2022-01-02 00:00:00 + 1641056400000L, // 2022-01-02 01:00:00 + 1641060000000L // 2022-01-02 02:00:00 + }; + final Map> batteryHistoryMap = new HashMap<>(); + final int currentUserId = mContext.getUserId(); + // Adds the index = 0 data. + Map entryMap = new HashMap<>(); + BatteryHistEntry entry = createBatteryHistEntry( + "package1", "label1", /*consumePower=*/ 0, /*uid=*/ 1L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 0L, + /*backgroundUsageTimeInMs=*/ 0L); + entryMap.put(entry.getKey(), entry); + entry = createBatteryHistEntry( + "package2", "label2", /*consumePower=*/ 0, /*uid=*/ 2L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 0L, + /*backgroundUsageTimeInMs=*/ 0L); + entryMap.put(entry.getKey(), entry); + batteryHistoryMap.put(batteryHistoryKeys[0], entryMap); + // Adds the index = 1 data. + entryMap = new HashMap<>(); + entry = createBatteryHistEntry( + "package1", "label1", /*consumePower=*/ 0, /*uid=*/ 1L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 0L, + /*backgroundUsageTimeInMs=*/ 0L); + entryMap.put(entry.getKey(), entry); + entry = createBatteryHistEntry( + "package2", "label2", /*consumePower=*/ 0, /*uid=*/ 2L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 0L, + /*backgroundUsageTimeInMs=*/ 0L); + entryMap.put(entry.getKey(), entry); + batteryHistoryMap.put(batteryHistoryKeys[1], entryMap); + // Adds the index = 2 data. + entryMap = new HashMap<>(); + entry = createBatteryHistEntry( + "package1", "label1", /*consumePower=*/ 10.0, /*uid=*/ 1L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 10L, + /*backgroundUsageTimeInMs=*/ 20L); + entryMap.put(entry.getKey(), entry); + entry = createBatteryHistEntry( + "package2", "label2", /*consumePower=*/ 10.0, /*uid=*/ 2L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 10L, + /*backgroundUsageTimeInMs=*/ 20L); + entryMap.put(entry.getKey(), entry); + batteryHistoryMap.put(batteryHistoryKeys[2], entryMap); + final List hourlyBatteryLevelsPerDay = + new ArrayList<>(); + List timestamps = List.of(batteryHistoryKeys[0], batteryHistoryKeys[2]); + final List levels = List.of(100, 100); + hourlyBatteryLevelsPerDay.add( + new BatteryLevelData.PeriodBatteryLevelData(timestamps, levels)); + when(mPowerUsageFeatureProvider.getHideBackgroundUsageTimeSet(mContext)) + .thenReturn(new HashSet(Arrays.asList((CharSequence) "package2"))); + + final Map> resultMap = + DataProcessor.getBatteryUsageMap( + mContext, hourlyBatteryLevelsPerDay, batteryHistoryMap); + + final BatteryDiffData resultDiffData = + resultMap + .get(DataProcessor.SELECTED_INDEX_ALL) + .get(DataProcessor.SELECTED_INDEX_ALL); + BatteryDiffEntry resultEntry = resultDiffData.getAppDiffEntryList().get(0); + assertThat(resultEntry.mBackgroundUsageTimeInMs).isEqualTo(20); + resultEntry = resultDiffData.getAppDiffEntryList().get(1); + assertThat(resultEntry.mBackgroundUsageTimeInMs).isEqualTo(0); + } + private static Map> createHistoryMap( final long[] timestamps, final int[] levels) { final Map> batteryHistoryMap = new HashMap<>(); @@ -391,6 +859,23 @@ public class DataProcessorTest { return batteryHistoryMap; } + private static BatteryHistEntry createBatteryHistEntry( + final String packageName, final String appLabel, final double consumePower, + final long uid, final long userId, final int consumerType, + final long foregroundUsageTimeInMs, final long backgroundUsageTimeInMs) { + // Only insert required fields. + final ContentValues values = new ContentValues(); + values.put(BatteryHistEntry.KEY_PACKAGE_NAME, packageName); + values.put(BatteryHistEntry.KEY_APP_LABEL, appLabel); + values.put(BatteryHistEntry.KEY_UID, uid); + values.put(BatteryHistEntry.KEY_USER_ID, userId); + values.put(BatteryHistEntry.KEY_CONSUMER_TYPE, consumerType); + values.put(BatteryHistEntry.KEY_CONSUME_POWER, consumePower); + values.put(BatteryHistEntry.KEY_FOREGROUND_USAGE_TIME, foregroundUsageTimeInMs); + values.put(BatteryHistEntry.KEY_BACKGROUND_USAGE_TIME, backgroundUsageTimeInMs); + return new BatteryHistEntry(values); + } + private static void verifyExpectedBatteryLevelData( final BatteryLevelData resultData, final List expectedDailyTimestamps, @@ -451,4 +936,15 @@ public class DataProcessorTest { .isEqualTo(expectedEnd.getTimeInMillis()); } + private static void assertBatteryDiffEntry( + final BatteryDiffEntry entry, final long userId, final long uid, + final int consumerType, final double consumePercentage, + final long foregroundUsageTimeInMs, final long backgroundUsageTimeInMs) { + assertThat(entry.mBatteryHistEntry.mUserId).isEqualTo(userId); + assertThat(entry.mBatteryHistEntry.mUid).isEqualTo(uid); + assertThat(entry.mBatteryHistEntry.mConsumerType).isEqualTo(consumerType); + assertThat(entry.getPercentOfTotal()).isEqualTo(consumePercentage); + assertThat(entry.mForegroundUsageTimeInMs).isEqualTo(foregroundUsageTimeInMs); + assertThat(entry.mBackgroundUsageTimeInMs).isEqualTo(backgroundUsageTimeInMs); + } }