| /* |
| * 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.getEffectivePackageName; |
| import static com.android.settings.fuelgauge.batteryusage.ConvertUtils.utcToLocalTime; |
| |
| import android.app.usage.IUsageStatsManager; |
| import android.app.usage.UsageEvents; |
| import android.app.usage.UsageEvents.Event; |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.PackageManager; |
| import android.content.pm.UserInfo; |
| import android.os.BatteryConsumer; |
| import android.os.BatteryStatsManager; |
| import android.os.BatteryUsageStats; |
| import android.os.BatteryUsageStatsQuery; |
| import android.os.Process; |
| import android.os.RemoteException; |
| import android.os.ServiceManager; |
| import android.os.UidBatteryConsumer; |
| import android.os.UserBatteryConsumer; |
| import android.os.UserHandle; |
| import android.os.UserManager; |
| import android.text.TextUtils; |
| import android.text.format.DateUtils; |
| import android.util.ArrayMap; |
| import android.util.ArraySet; |
| import android.util.Log; |
| import android.util.SparseArray; |
| |
| import androidx.annotation.Nullable; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.os.PowerProfile; |
| import com.android.settings.Utils; |
| import com.android.settings.fuelgauge.BatteryUtils; |
| import com.android.settings.overlay.FeatureFactory; |
| import com.android.settingslib.fuelgauge.BatteryStatus; |
| import com.android.settingslib.spaprivileged.model.app.AppListRepositoryUtil; |
| |
| import java.time.Duration; |
| import java.util.ArrayList; |
| import java.util.Calendar; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.stream.Collectors; |
| import java.util.stream.Stream; |
| |
| /** |
| * 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 String TAG = "DataProcessor"; |
| private static final int POWER_COMPONENT_SYSTEM_SERVICES = 7; |
| private static final int POWER_COMPONENT_WAKELOCK = 12; |
| private static final int MIN_AVERAGE_POWER_THRESHOLD_MILLI_AMP = 10; |
| 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 long MIN_TIME_SLOT = DateUtils.HOUR_IN_MILLIS * 2; |
| private static final String MEDIASERVER_PACKAGE_NAME = "mediaserver"; |
| 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 long DEFAULT_USAGE_DURATION_FOR_INCOMPLETE_INTERVAL = |
| DateUtils.SECOND_IN_MILLIS * 30; |
| |
| @VisibleForTesting |
| static final int SELECTED_INDEX_ALL = BatteryChartViewModel.SELECTED_INDEX_ALL; |
| |
| @VisibleForTesting |
| static boolean sDebug = false; |
| |
| @VisibleForTesting |
| static long sTestCurrentTimeMillis = 0; |
| |
| @VisibleForTesting |
| static Set<String> sTestSystemAppsSet; |
| |
| @VisibleForTesting |
| static IUsageStatsManager sUsageStatsManager = |
| IUsageStatsManager.Stub.asInterface( |
| ServiceManager.getService(Context.USAGE_STATS_SERVICE)); |
| |
| public static final String CURRENT_TIME_BATTERY_HISTORY_PLACEHOLDER = |
| "CURRENT_TIME_BATTERY_HISTORY_PLACEHOLDER"; |
| public static final Comparator<AppUsageEvent> TIMESTAMP_COMPARATOR = |
| Comparator.comparing(AppUsageEvent::getTimestamp); |
| |
| /** A callback listener when battery usage loading async task is executed. */ |
| public interface UsageMapAsyncResponse { |
| /** The callback function when batteryUsageMap is loaded. */ |
| void onBatteryCallbackDataLoaded(BatteryCallbackData batteryCallbackData); |
| } |
| |
| private DataProcessor() { |
| } |
| |
| /** |
| * @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); |
| // Loads the current battery usage data from the battery stats service. |
| final Map<String, BatteryHistEntry> currentBatteryHistoryMap = |
| getCurrentBatteryHistoryMapFromStatsService(context); |
| // Replaces the placeholder in processedBatteryHistoryMap. |
| for (Map.Entry<Long, Map<String, BatteryHistEntry>> mapEntry |
| : processedBatteryHistoryMap.entrySet()) { |
| if (mapEntry.getValue().containsKey(CURRENT_TIME_BATTERY_HISTORY_PLACEHOLDER)) { |
| mapEntry.setValue(currentBatteryHistoryMap); |
| } |
| } |
| return batteryLevelData == null |
| ? null |
| : getBatteryUsageMap( |
| context, |
| batteryLevelData.getHourlyBatteryLevelsPerDay(), |
| processedBatteryHistoryMap, |
| /*appUsagePeriodMap=*/ null); |
| } |
| |
| /** |
| * Gets the {@link BatteryUsageStats} from system service. |
| */ |
| @Nullable |
| public static BatteryUsageStats getBatteryUsageStats(final Context context) { |
| final BatteryUsageStatsQuery batteryUsageStatsQuery = |
| new BatteryUsageStatsQuery |
| .Builder() |
| .includeBatteryHistory() |
| .includeProcessStateData() |
| .build(); |
| return context.getSystemService(BatteryStatsManager.class) |
| .getBatteryUsageStats(batteryUsageStatsQuery); |
| } |
| |
| /** |
| * Gets the {@link UsageEvents} from system service for all unlocked users. |
| */ |
| @Nullable |
| public static Map<Long, UsageEvents> getAppUsageEvents(Context context) { |
| final long start = System.currentTimeMillis(); |
| context = DatabaseUtils.getOwnerContext(context); |
| if (context == null) { |
| return null; |
| } |
| final Map<Long, UsageEvents> resultMap = new HashMap(); |
| final UserManager userManager = context.getSystemService(UserManager.class); |
| if (userManager == null) { |
| return null; |
| } |
| final long sixDaysAgoTimestamp = |
| DatabaseUtils.getTimestampSixDaysAgo(Calendar.getInstance()); |
| for (final UserInfo user : userManager.getAliveUsers()) { |
| final UsageEvents events = getAppUsageEventsForUser( |
| context, userManager, user.id, sixDaysAgoTimestamp); |
| if (events != null) { |
| resultMap.put(Long.valueOf(user.id), events); |
| } |
| } |
| final long elapsedTime = System.currentTimeMillis() - start; |
| Log.d(TAG, String.format("getAppUsageEvents() for all unlocked users in %d/ms", |
| elapsedTime)); |
| return resultMap.isEmpty() ? null : resultMap; |
| } |
| |
| /** |
| * Gets the {@link UsageEvents} from system service for the specific user. |
| */ |
| @Nullable |
| public static UsageEvents getAppUsageEventsForUser( |
| Context context, final int userID, final long startTimestampOfLevelData) { |
| final long start = System.currentTimeMillis(); |
| context = DatabaseUtils.getOwnerContext(context); |
| if (context == null) { |
| return null; |
| } |
| final UserManager userManager = context.getSystemService(UserManager.class); |
| if (userManager == null) { |
| return null; |
| } |
| final long sixDaysAgoTimestamp = |
| DatabaseUtils.getTimestampSixDaysAgo(Calendar.getInstance()); |
| final long earliestTimestamp = Math.max(sixDaysAgoTimestamp, startTimestampOfLevelData); |
| final UsageEvents events = getAppUsageEventsForUser( |
| context, userManager, userID, earliestTimestamp); |
| final long elapsedTime = System.currentTimeMillis() - start; |
| Log.d(TAG, String.format("getAppUsageEventsForUser() for user %d in %d/ms", |
| userID, elapsedTime)); |
| return events; |
| } |
| |
| /** |
| * Closes the {@link BatteryUsageStats} after using it. |
| */ |
| public static void closeBatteryUsageStats(BatteryUsageStats batteryUsageStats) { |
| if (batteryUsageStats != null) { |
| try { |
| batteryUsageStats.close(); |
| } catch (Exception e) { |
| Log.e(TAG, "BatteryUsageStats.close() failed", e); |
| } |
| } |
| } |
| |
| /** |
| * Generates the indexed {@link AppUsagePeriod} list data for each corresponding time slot. |
| * Attributes the list of {@link AppUsageEvent} into hourly time slots and reformat them into |
| * {@link AppUsagePeriod} for easier use in the following process. |
| * |
| * <p>There could be 2 cases of the returned value:</p> |
| * <ul> |
| * <li>null: empty or invalid data.</li> |
| * <li>non-null: must be a 2d map and composed by: |
| * <p> [0][0] ~ [maxDailyIndex][maxHourlyIndex]</p></li> |
| * </ul> |
| * |
| * <p>The structure is consistent with the battery usage map returned by |
| * {@code getBatteryUsageMap}.</p> |
| * |
| * <p>{@code Long} stands for the userId.</p> |
| * <p>{@code String} stands for the packageName.</p> |
| */ |
| @Nullable |
| public static Map<Integer, Map<Integer, Map<Long, Map<String, List<AppUsagePeriod>>>>> |
| generateAppUsagePeriodMap( |
| final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay, |
| final List<AppUsageEvent> appUsageEventList) { |
| if (appUsageEventList.isEmpty()) { |
| Log.w(TAG, "appUsageEventList is empty"); |
| return null; |
| } |
| // Sorts the appUsageEventList in ascending order based on the timestamp before |
| // distribution. |
| Collections.sort(appUsageEventList, TIMESTAMP_COMPARATOR); |
| final Map<Integer, Map<Integer, Map<Long, Map<String, List<AppUsagePeriod>>>>> resultMap = |
| new HashMap<>(); |
| |
| final long dailySize = hourlyBatteryLevelsPerDay.size(); |
| for (int dailyIndex = 0; dailyIndex < hourlyBatteryLevelsPerDay.size(); dailyIndex++) { |
| final Map<Integer, Map<Long, Map<String, List<AppUsagePeriod>>>> dailyMap = |
| new HashMap<>(); |
| resultMap.put(dailyIndex, dailyMap); |
| if (hourlyBatteryLevelsPerDay.get(dailyIndex) == null) { |
| continue; |
| } |
| final List<Long> timestamps = hourlyBatteryLevelsPerDay.get(dailyIndex).getTimestamps(); |
| final long hourlySize = timestamps.size() - 1; |
| for (int hourlyIndex = 0; hourlyIndex < timestamps.size() - 1; hourlyIndex++) { |
| // The start and end timestamps of this slot should be the adjacent timestamps. |
| final long startTimestamp = timestamps.get(hourlyIndex); |
| // The final slot is to show the data from last even hour until now but the |
| // timestamp in hourlyBatteryLevelsPerDay is not the real value. So use current |
| // timestamp instead of reading the timestamp from hourlyBatteryLevelsPerDay here. |
| final long endTimestamp = |
| dailyIndex == dailySize - 1 && hourlyIndex == hourlySize - 1 && !sDebug |
| ? System.currentTimeMillis() : timestamps.get(hourlyIndex + 1); |
| |
| // Gets the app usage event list for this hourly slot first. |
| final List<AppUsageEvent> hourlyAppUsageEventList = |
| getAppUsageEventListWithinTimeRangeWithBuffer( |
| appUsageEventList, startTimestamp, endTimestamp); |
| |
| // The value could be null when there is no data in the hourly slot. |
| dailyMap.put( |
| hourlyIndex, |
| buildAppUsagePeriodList( |
| hourlyAppUsageEventList, startTimestamp, endTimestamp)); |
| } |
| } |
| return resultMap; |
| } |
| |
| /** |
| * Generates the list of {@link AppUsageEvent} from the supplied {@link UsageEvents}. |
| */ |
| public static List<AppUsageEvent> generateAppUsageEventListFromUsageEvents( |
| Context context, Map<Long, UsageEvents> usageEventsMap) { |
| final List<AppUsageEvent> appUsageEventList = new ArrayList<>(); |
| long numEventsFetched = 0; |
| long numAllEventsFetched = 0; |
| final Set<String> ignoreScreenOnTimeTaskRootSet = |
| FeatureFactory.getFactory(context) |
| .getPowerUsageFeatureProvider(context) |
| .getIgnoreScreenOnTimeTaskRootSet(); |
| for (final long userId : usageEventsMap.keySet()) { |
| final UsageEvents usageEvents = usageEventsMap.get(userId); |
| while (usageEvents.hasNextEvent()) { |
| final Event event = new Event(); |
| usageEvents.getNextEvent(event); |
| numAllEventsFetched++; |
| switch (event.getEventType()) { |
| case Event.ACTIVITY_RESUMED: |
| case Event.ACTIVITY_STOPPED: |
| case Event.DEVICE_SHUTDOWN: |
| final String taskRootClassName = event.getTaskRootClassName(); |
| if (!TextUtils.isEmpty(taskRootClassName) |
| && ignoreScreenOnTimeTaskRootSet.contains(taskRootClassName)) { |
| Log.w(TAG, String.format( |
| "Ignoring a usage event with task root class name %s, " |
| + "(timestamp=%d, type=%d)", |
| taskRootClassName, |
| event.getTimeStamp(), |
| event.getEventType())); |
| break; |
| } |
| final AppUsageEvent appUsageEvent = |
| ConvertUtils.convertToAppUsageEvent( |
| context, sUsageStatsManager, event, userId); |
| if (appUsageEvent != null) { |
| numEventsFetched++; |
| appUsageEventList.add(appUsageEvent); |
| } |
| break; |
| default: |
| break; |
| } |
| } |
| } |
| Log.w(TAG, String.format( |
| "Read %d relevant events (%d total) from UsageStatsManager", numEventsFetched, |
| numAllEventsFetched)); |
| return appUsageEventList; |
| } |
| |
| /** |
| * @return Returns the device screen-on time data. |
| * |
| * <p>There could be 2 cases of the returned value:</p> |
| * <ul> |
| * <li>null: empty or invalid data.</li> |
| * <li>non-null: must be a 2d map and composed by 3 parts:</li> |
| * <p> 1 - [SELECTED_INDEX_ALL][SELECTED_INDEX_ALL]</p> |
| * <p> 2 - [0][SELECTED_INDEX_ALL] ~ [maxDailyIndex][SELECTED_INDEX_ALL]</p> |
| * <p> 3 - [0][0] ~ [maxDailyIndex][maxHourlyIndex]</p> |
| * </ul> |
| * |
| * <p>The structure is consistent with the battery usage map returned by |
| * {@code getBatteryUsageMap}.</p> |
| */ |
| @Nullable |
| public static Map<Integer, Map<Integer, Long>> getDeviceScreenOnTime( |
| final Map<Integer, Map<Integer, Map<Long, Map<String, List<AppUsagePeriod>>>>> |
| appUsagePeriodMap) { |
| if (appUsagePeriodMap == null) { |
| return null; |
| } |
| final Map<Integer, Map<Integer, Long>> deviceScreenOnTime = new HashMap<>(); |
| insertHourlyDeviceScreenOnTime(appUsagePeriodMap, deviceScreenOnTime); |
| insertDailyDeviceScreenOnTime(appUsagePeriodMap, deviceScreenOnTime); |
| insertAllDeviceScreenOnTime(deviceScreenOnTime); |
| return deviceScreenOnTime; |
| } |
| |
| /** |
| * Generates the list of {@link BatteryEntry} from the supplied {@link BatteryUsageStats}. |
| */ |
| @Nullable |
| public static List<BatteryEntry> generateBatteryEntryListFromBatteryUsageStats( |
| final Context context, |
| @Nullable final BatteryUsageStats batteryUsageStats) { |
| if (batteryUsageStats == null) { |
| Log.w(TAG, "batteryUsageStats is null content"); |
| return null; |
| } |
| if (!shouldShowBatteryAttributionList(context)) { |
| return null; |
| } |
| final BatteryUtils batteryUtils = BatteryUtils.getInstance(context); |
| final int dischargePercentage = Math.max(0, batteryUsageStats.getDischargePercentage()); |
| final List<BatteryEntry> usageList = getCoalescedUsageList( |
| context, batteryUtils, batteryUsageStats, /*loadDataInBackground=*/ false); |
| final double totalPower = batteryUsageStats.getConsumedPower(); |
| for (int i = 0; i < usageList.size(); i++) { |
| final BatteryEntry entry = usageList.get(i); |
| final double percentOfTotal = batteryUtils.calculateBatteryPercent( |
| entry.getConsumedPower(), totalPower, dischargePercentage); |
| entry.mPercent = percentOfTotal; |
| } |
| return usageList; |
| } |
| |
| /** |
| * @return Returns the latest battery history map loaded from the battery stats service. |
| */ |
| public static Map<String, BatteryHistEntry> getCurrentBatteryHistoryMapFromStatsService( |
| final Context context) { |
| final List<BatteryHistEntry> batteryHistEntryList = |
| getBatteryHistListFromFromStatsService(context); |
| return batteryHistEntryList == null ? new HashMap<>() |
| : batteryHistEntryList.stream().collect(Collectors.toMap(e -> e.getKey(), e -> e)); |
| } |
| |
| /** |
| * @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. |
| */ |
| 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 long currentTime = getCurrentTimeMillis(); |
| final List<Long> expectedTimestampList = getTimestampSlots(rawTimestampList, currentTime); |
| final boolean isFromFullCharge = |
| isFromFullCharge(batteryHistoryMap.get(rawTimestampList.get(0))); |
| interpolateHistory( |
| context, rawTimestampList, expectedTimestampList, currentTime, isFromFullCharge, |
| batteryHistoryMap, resultMap); |
| Log.d(TAG, String.format("getHistoryMapWithExpectedTimestamps() size=%d in %d/ms", |
| resultMap.size(), (System.currentTimeMillis() - startTime))); |
| return resultMap; |
| } |
| |
| @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 long currentTime) { |
| final List<Long> timestampSlots = new ArrayList<>(); |
| if (rawTimestampList.isEmpty()) { |
| return timestampSlots; |
| } |
| final long rawStartTimestamp = rawTimestampList.get(0); |
| // No matter the start is from last full charge or 6 days ago, use the nearest even hour. |
| final long startTimestamp = getNearestEvenHourTimestamp(rawStartTimestamp); |
| // Use the first even hour after the current time as the end. |
| final long endTimestamp = getFirstEvenHourAfterTimestamp(currentTime); |
| // 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); |
| // If the timestamp diff is smaller than MIN_TIME_SLOT, returns the empty list directly. |
| if (endTime - startTime < MIN_TIME_SLOT) { |
| return dailyTimestampList; |
| } |
| long nextDay = getTimestampOfNextDay(startTime); |
| // Only if the timestamp diff in the first day is bigger than MIN_TIME_SLOT, start from the |
| // first day. Otherwise, start from the second day. |
| if (nextDay - startTime >= MIN_TIME_SLOT) { |
| dailyTimestampList.add(startTime); |
| } |
| while (nextDay < endTime) { |
| dailyTimestampList.add(nextDay); |
| nextDay = getTimestampOfNextDay(nextDay); |
| } |
| final long lastDailyTimestamp = dailyTimestampList.get(dailyTimestampList.size() - 1); |
| // Only if the timestamp diff in the last day is bigger than MIN_TIME_SLOT, add the |
| // last day. |
| if (endTime - lastDailyTimestamp >= MIN_TIME_SLOT) { |
| dailyTimestampList.add(endTime); |
| } |
| // The dailyTimestampList must have the start and end timestamp, otherwise, return an empty |
| // list. |
| if (dailyTimestampList.size() < MIN_TIMESTAMP_DATA_SIZE) { |
| return new ArrayList<>(); |
| } |
| 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. |
| * |
| * <p>There could be 2 cases of the returned value:</p> |
| * <ul> |
| * <li>null: empty or invalid data.</li> |
| * <li>non-null: must be a 2d map and composed by 3 parts:</li> |
| * <p> 1 - [SELECTED_INDEX_ALL][SELECTED_INDEX_ALL]</p> |
| * <p> 2 - [0][SELECTED_INDEX_ALL] ~ [maxDailyIndex][SELECTED_INDEX_ALL]</p> |
| * <p> 3 - [0][0] ~ [maxDailyIndex][maxHourlyIndex]</p> |
| * </ul> |
| */ |
| @Nullable |
| static Map<Integer, Map<Integer, BatteryDiffData>> getBatteryUsageMap( |
| final Context context, |
| final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay, |
| final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap, |
| final Map<Integer, Map<Integer, Map<Long, Map<String, List<AppUsagePeriod>>>>> |
| appUsagePeriodMap) { |
| if (batteryHistoryMap.isEmpty()) { |
| return null; |
| } |
| final Map<Integer, Map<Integer, BatteryDiffData>> resultMap = new HashMap<>(); |
| final Set<String> systemAppsSet = getSystemAppsSet(context); |
| // Insert diff data from [0][0] to [maxDailyIndex][maxHourlyIndex]. |
| insertHourlyUsageDiffData(context, systemAppsSet, hourlyBatteryLevelsPerDay, |
| batteryHistoryMap, appUsagePeriodMap, resultMap); |
| // Insert diff data from [0][SELECTED_INDEX_ALL] to [maxDailyIndex][SELECTED_INDEX_ALL]. |
| insertDailyUsageDiffData(context, hourlyBatteryLevelsPerDay, resultMap); |
| // Insert diff data [SELECTED_INDEX_ALL][SELECTED_INDEX_ALL]. |
| insertAllUsageDiffData(context, resultMap); |
| if (!isUsageMapValid(resultMap, hourlyBatteryLevelsPerDay)) { |
| return null; |
| } |
| return resultMap; |
| } |
| |
| @VisibleForTesting |
| @Nullable |
| static BatteryDiffData generateBatteryDiffData( |
| final Context context, |
| final List<BatteryHistEntry> batteryHistEntryList) { |
| if (batteryHistEntryList == null || batteryHistEntryList.isEmpty()) { |
| Log.w(TAG, "batteryHistEntryList is null or empty in generateBatteryDiffData()"); |
| return null; |
| } |
| final int currentUserId = context.getUserId(); |
| final UserHandle userHandle = |
| Utils.getManagedProfile(context.getSystemService(UserManager.class)); |
| final int workProfileUserId = |
| userHandle != null ? userHandle.getIdentifier() : Integer.MIN_VALUE; |
| final List<BatteryDiffEntry> appEntries = new ArrayList<>(); |
| final List<BatteryDiffEntry> systemEntries = new ArrayList<>(); |
| |
| for (BatteryHistEntry entry : batteryHistEntryList) { |
| final boolean isFromOtherUsers = isConsumedFromOtherUsers( |
| currentUserId, workProfileUserId, entry); |
| // Not show other users' battery usage data. |
| if (isFromOtherUsers) { |
| continue; |
| } else { |
| final BatteryDiffEntry currentBatteryDiffEntry = new BatteryDiffEntry( |
| context, |
| entry.mForegroundUsageTimeInMs, |
| entry.mBackgroundUsageTimeInMs, |
| /*screenOnTimeInMs=*/ 0, |
| entry.mConsumePower, |
| entry.mForegroundUsageConsumePower, |
| entry.mForegroundServiceUsageConsumePower, |
| entry.mBackgroundUsageConsumePower, |
| entry.mCachedUsageConsumePower, |
| entry); |
| if (currentBatteryDiffEntry.isSystemEntry()) { |
| systemEntries.add(currentBatteryDiffEntry); |
| } else { |
| appEntries.add(currentBatteryDiffEntry); |
| } |
| } |
| } |
| |
| // If there is no data, return null instead of empty item. |
| if (appEntries.isEmpty() && systemEntries.isEmpty()) { |
| return null; |
| } |
| |
| final Set<String> systemAppsSet = getSystemAppsSet(context); |
| return new BatteryDiffData( |
| context, appEntries, systemEntries, systemAppsSet, /* isAccumulated= */ false); |
| } |
| |
| /** |
| * <p>{@code Long} stands for the userId.</p> |
| * <p>{@code String} stands for the packageName.</p> |
| */ |
| @VisibleForTesting |
| @Nullable |
| static Map<Long, Map<String, List<AppUsagePeriod>>> buildAppUsagePeriodList( |
| final List<AppUsageEvent> allAppUsageEvents, final long startTime, final long endTime) { |
| if (allAppUsageEvents.isEmpty()) { |
| return null; |
| } |
| |
| // Attributes the list of AppUsagePeriod into device events and instance events for further |
| // use. |
| final List<AppUsageEvent> deviceEvents = new ArrayList<>(); |
| final ArrayMap<Integer, List<AppUsageEvent>> usageEventsByInstanceId = new ArrayMap<>(); |
| for (final AppUsageEvent event : allAppUsageEvents) { |
| final AppUsageEventType eventType = event.getType(); |
| if (eventType == AppUsageEventType.ACTIVITY_RESUMED |
| || eventType == AppUsageEventType.ACTIVITY_STOPPED) { |
| final int instanceId = event.getInstanceId(); |
| if (usageEventsByInstanceId.get(instanceId) == null) { |
| usageEventsByInstanceId.put(instanceId, new ArrayList<>()); |
| } |
| usageEventsByInstanceId.get(instanceId).add(event); |
| } else if (eventType == AppUsageEventType.DEVICE_SHUTDOWN) { |
| // Track device-wide events in their own list as they affect any app. |
| deviceEvents.add(event); |
| } |
| } |
| if (usageEventsByInstanceId.isEmpty()) { |
| return null; |
| } |
| |
| final Map<Long, Map<String, List<AppUsagePeriod>>> allUsagePeriods = new HashMap<>(); |
| |
| for (int i = 0; i < usageEventsByInstanceId.size(); i++) { |
| // The usage periods for an instance are determined by the usage events with its |
| // instance id and any device-wide events such as device shutdown. |
| final List<AppUsageEvent> usageEvents = usageEventsByInstanceId.valueAt(i); |
| if (usageEvents == null || usageEvents.isEmpty()) { |
| continue; |
| } |
| // The same instance must have same userId and packageName. |
| final AppUsageEvent firstEvent = usageEvents.get(0); |
| final long eventUserId = firstEvent.getUserId(); |
| final String packageName = getEffectivePackageName( |
| sUsageStatsManager, |
| firstEvent.getPackageName(), |
| firstEvent.getTaskRootPackageName()); |
| usageEvents.addAll(deviceEvents); |
| // Sorts the usageEvents in ascending order based on the timestamp before computing the |
| // period. |
| Collections.sort(usageEvents, TIMESTAMP_COMPARATOR); |
| |
| // A package might have multiple instances. Computes the usage period per instance id |
| // and then merges them into the same user-package map. |
| final List<AppUsagePeriod> usagePeriodList = |
| buildAppUsagePeriodListPerInstance(usageEvents, startTime, endTime); |
| if (!usagePeriodList.isEmpty()) { |
| addToUsagePeriodMap(allUsagePeriods, usagePeriodList, eventUserId, packageName); |
| } |
| } |
| |
| // Sorts all usage periods by start time. |
| for (final long userId : allUsagePeriods.keySet()) { |
| if (allUsagePeriods.get(userId) == null) { |
| continue; |
| } |
| for (final String packageName: allUsagePeriods.get(userId).keySet()) { |
| Collections.sort( |
| allUsagePeriods.get(userId).get(packageName), |
| Comparator.comparing(AppUsagePeriod::getStartTime)); |
| } |
| } |
| return allUsagePeriods.isEmpty() ? null : allUsagePeriods; |
| } |
| |
| @VisibleForTesting |
| static List<AppUsagePeriod> buildAppUsagePeriodListPerInstance( |
| final List<AppUsageEvent> usageEvents, final long startTime, final long endTime) { |
| final List<AppUsagePeriod> usagePeriodList = new ArrayList<>(); |
| AppUsagePeriod.Builder pendingUsagePeriod = AppUsagePeriod.newBuilder(); |
| |
| for (final AppUsageEvent event : usageEvents) { |
| final long eventTime = event.getTimestamp(); |
| |
| if (event.getType() == AppUsageEventType.ACTIVITY_RESUMED) { |
| // If there is an existing start time, simply ignore this start event. |
| // If there was no start time, then start a new period. |
| if (!pendingUsagePeriod.hasStartTime()) { |
| pendingUsagePeriod.setStartTime(eventTime); |
| } |
| } else if (event.getType() == AppUsageEventType.ACTIVITY_STOPPED) { |
| pendingUsagePeriod.setEndTime(eventTime); |
| if (!pendingUsagePeriod.hasStartTime()) { |
| pendingUsagePeriod.setStartTime( |
| getStartTimeForIncompleteUsagePeriod(pendingUsagePeriod)); |
| } |
| // If we already have start time, add it directly. |
| validateAndAddToPeriodList( |
| usagePeriodList, pendingUsagePeriod.build(), startTime, endTime); |
| pendingUsagePeriod.clear(); |
| } else if (event.getType() == AppUsageEventType.DEVICE_SHUTDOWN) { |
| // The end event might be lost when device is shutdown. Use the estimated end |
| // time for the period. |
| if (pendingUsagePeriod.hasStartTime()) { |
| pendingUsagePeriod.setEndTime( |
| getEndTimeForIncompleteUsagePeriod(pendingUsagePeriod, eventTime)); |
| validateAndAddToPeriodList( |
| usagePeriodList, pendingUsagePeriod.build(), startTime, endTime); |
| pendingUsagePeriod.clear(); |
| } |
| } |
| } |
| // If there exists unclosed period, the stop event might happen in the next time |
| // slot. Use the endTime for the period. |
| if (pendingUsagePeriod.hasStartTime() && pendingUsagePeriod.getStartTime() < endTime) { |
| pendingUsagePeriod.setEndTime(endTime); |
| validateAndAddToPeriodList( |
| usagePeriodList, pendingUsagePeriod.build(), startTime, endTime); |
| pendingUsagePeriod.clear(); |
| } |
| return usagePeriodList; |
| } |
| |
| @VisibleForTesting |
| static long getScreenOnTime( |
| final Map<Long, Map<String, List<AppUsagePeriod>>> appUsageMap, |
| final long userId, |
| final String packageName) { |
| if (appUsageMap == null || appUsageMap.get(userId) == null) { |
| return 0; |
| } |
| |
| return getScreenOnTime(appUsageMap.get(userId).get(packageName)); |
| } |
| |
| /** |
| * @return Returns the overall battery usage data from battery stats service directly. |
| * |
| * The returned value should be always a 2d map and composed by only 1 part: |
| * - [SELECTED_INDEX_ALL][SELECTED_INDEX_ALL] |
| */ |
| static Map<Integer, Map<Integer, BatteryDiffData>> getBatteryUsageMapFromStatsService( |
| final Context context) { |
| final Map<Integer, Map<Integer, BatteryDiffData>> resultMap = new HashMap<>(); |
| final Map<Integer, BatteryDiffData> allUsageMap = new HashMap<>(); |
| // Always construct the map whether the value is null or not. |
| allUsageMap.put(SELECTED_INDEX_ALL, |
| generateBatteryDiffData(context, getBatteryHistListFromFromStatsService(context))); |
| resultMap.put(SELECTED_INDEX_ALL, allUsageMap); |
| return resultMap; |
| } |
| |
| static void loadLabelAndIcon( |
| @Nullable final Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageMap) { |
| if (batteryUsageMap == null) { |
| return; |
| } |
| // 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()); |
| } |
| } |
| |
| /** |
| * Generates the list of {@link AppUsageEvent} within the specific time range. |
| * The buffer is added to make sure the app usage calculation near the boundaries is correct. |
| * |
| * Note: The appUsageEventList should have been sorted when calling this function. |
| */ |
| private static List<AppUsageEvent> getAppUsageEventListWithinTimeRangeWithBuffer( |
| final List<AppUsageEvent> appUsageEventList, final long startTime, final long endTime) { |
| final long start = startTime - DatabaseUtils.USAGE_QUERY_BUFFER_HOURS; |
| final long end = endTime + DatabaseUtils.USAGE_QUERY_BUFFER_HOURS; |
| final List<AppUsageEvent> resultList = new ArrayList<>(); |
| for (final AppUsageEvent event : appUsageEventList) { |
| final long eventTime = event.getTimestamp(); |
| // Because the appUsageEventList has been sorted, if any event is already after the end |
| // time, all the following events should be able to drop directly. |
| if (eventTime > end) { |
| break; |
| } |
| // If the event timestamp is in [start, end], add it into the result list. |
| if (eventTime >= start) { |
| resultList.add(event); |
| } |
| } |
| return resultList; |
| } |
| |
| private static void validateAndAddToPeriodList( |
| final List<AppUsagePeriod> appUsagePeriodList, |
| final AppUsagePeriod appUsagePeriod, |
| final long startTime, |
| final long endTime) { |
| final long periodStartTime = |
| trimPeriodTime(appUsagePeriod.getStartTime(), startTime, endTime); |
| final long periodEndTime = trimPeriodTime(appUsagePeriod.getEndTime(), startTime, endTime); |
| // Only when the period is valid, add it into the list. |
| if (periodStartTime < periodEndTime) { |
| final AppUsagePeriod period = |
| AppUsagePeriod.newBuilder() |
| .setStartTime(periodStartTime) |
| .setEndTime(periodEndTime) |
| .build(); |
| appUsagePeriodList.add(period); |
| } |
| } |
| |
| private static long trimPeriodTime( |
| final long originalTime, final long startTime, final long endTime) { |
| long finalTime = Math.max(originalTime, startTime); |
| finalTime = Math.min(finalTime, endTime); |
| return finalTime; |
| } |
| |
| private static void addToUsagePeriodMap( |
| final Map<Long, Map<String, List<AppUsagePeriod>>> usagePeriodMap, |
| final List<AppUsagePeriod> usagePeriodList, |
| final long userId, |
| final String packageName) { |
| usagePeriodMap.computeIfAbsent(userId, key -> new HashMap<>()); |
| final Map<String, List<AppUsagePeriod>> packageNameMap = usagePeriodMap.get(userId); |
| packageNameMap.computeIfAbsent(packageName, key -> new ArrayList<>()); |
| packageNameMap.get(packageName).addAll(usagePeriodList); |
| } |
| |
| /** |
| * Returns the start time that gives {@code usagePeriod} the default usage duration. |
| */ |
| private static long getStartTimeForIncompleteUsagePeriod( |
| final AppUsagePeriodOrBuilder usagePeriod) { |
| return usagePeriod.getEndTime() - DEFAULT_USAGE_DURATION_FOR_INCOMPLETE_INTERVAL; |
| } |
| |
| /** |
| * Returns the end time that gives {@code usagePeriod} the default usage duration. |
| */ |
| private static long getEndTimeForIncompleteUsagePeriod( |
| final AppUsagePeriodOrBuilder usagePeriod, final long eventTime) { |
| return Math.min( |
| usagePeriod.getStartTime() + DEFAULT_USAGE_DURATION_FOR_INCOMPLETE_INTERVAL, |
| eventTime); |
| } |
| |
| private static void insertHourlyDeviceScreenOnTime( |
| final Map<Integer, Map<Integer, Map<Long, Map<String, List<AppUsagePeriod>>>>> |
| appUsagePeriodMap, |
| final Map<Integer, Map<Integer, Long>> resultMap) { |
| for (final int dailyIndex : appUsagePeriodMap.keySet()) { |
| final Map<Integer, Map<Long, Map<String, List<AppUsagePeriod>>>> dailyAppUsageMap = |
| appUsagePeriodMap.get(dailyIndex); |
| final Map<Integer, Long> dailyScreenOnTime = new HashMap<>(); |
| resultMap.put(dailyIndex, dailyScreenOnTime); |
| if (dailyAppUsageMap == null) { |
| continue; |
| } |
| |
| for (final int hourlyIndex : dailyAppUsageMap.keySet()) { |
| final Map<Long, Map<String, List<AppUsagePeriod>>> appUsageMap = |
| dailyAppUsageMap.get(hourlyIndex); |
| if (appUsageMap == null || appUsageMap.isEmpty()) { |
| dailyScreenOnTime.put(hourlyIndex, 0L); |
| } else { |
| final List<AppUsagePeriod> flatUsageList = new ArrayList<>(); |
| for (final long userId: appUsageMap.keySet()) { |
| if (appUsageMap.get(userId) == null) { |
| continue; |
| } |
| for (final String packageName: appUsageMap.get(userId).keySet()) { |
| final List<AppUsagePeriod> appUsagePeriodList = |
| appUsageMap.get(userId).get(packageName); |
| if (appUsagePeriodList != null && !appUsagePeriodList.isEmpty()) { |
| flatUsageList.addAll(appUsagePeriodList); |
| } |
| } |
| } |
| // Compute the screen on time and make sure it won't exceed the threshold. |
| final long screenOnTime = Math.min( |
| (long) TOTAL_HOURLY_TIME_THRESHOLD, getScreenOnTime(flatUsageList)); |
| dailyScreenOnTime.put(hourlyIndex, screenOnTime); |
| } |
| } |
| } |
| } |
| |
| private static void insertDailyDeviceScreenOnTime( |
| final Map<Integer, Map<Integer, Map<Long, Map<String, List<AppUsagePeriod>>>>> |
| appUsagePeriodMap, |
| final Map<Integer, Map<Integer, Long>> resultMap) { |
| for (final int dailyIndex : appUsagePeriodMap.keySet()) { |
| Map<Integer, Long> dailyResultMap = resultMap.get(dailyIndex); |
| if (dailyResultMap == null) { |
| dailyResultMap = new HashMap<>(); |
| resultMap.put(dailyIndex, dailyResultMap); |
| } |
| dailyResultMap.put( |
| SELECTED_INDEX_ALL, |
| getAccumulatedScreenOnTime(dailyResultMap)); |
| } |
| } |
| |
| private static void insertAllDeviceScreenOnTime( |
| final Map<Integer, Map<Integer, Long>> resultMap) { |
| final Map<Integer, Long> dailyAllMap = new HashMap<>(); |
| resultMap.keySet().forEach( |
| key -> dailyAllMap.put(key, resultMap.get(key).get(SELECTED_INDEX_ALL))); |
| final Map<Integer, Long> allUsageMap = new HashMap<>(); |
| allUsageMap.put(SELECTED_INDEX_ALL, getAccumulatedScreenOnTime(dailyAllMap)); |
| resultMap.put(SELECTED_INDEX_ALL, allUsageMap); |
| } |
| |
| private static long getAccumulatedScreenOnTime(final Map<Integer, Long> screenOnTimeMap) { |
| if (screenOnTimeMap == null || screenOnTimeMap.isEmpty()) { |
| return 0; |
| } |
| long sum = 0; |
| for (final int index : screenOnTimeMap.keySet()) { |
| sum += screenOnTimeMap.get(index) == null ? 0 : screenOnTimeMap.get(index); |
| } |
| return sum; |
| } |
| |
| @Nullable |
| private static UsageEvents getAppUsageEventsForUser( |
| Context context, final UserManager userManager, final int userID, |
| final long earliestTimestamp) { |
| final String callingPackage = context.getPackageName(); |
| final long now = System.currentTimeMillis(); |
| // When the user is not unlocked, UsageStatsManager will return null, so bypass the |
| // following data loading logics directly. |
| if (!userManager.isUserUnlocked(userID)) { |
| Log.w(TAG, "fail to load app usage event for user :" + userID + " because locked"); |
| return null; |
| } |
| final long startTime = DatabaseUtils.getAppUsageStartTimestampOfUser( |
| context, userID, earliestTimestamp); |
| return loadAppUsageEventsForUserFromService( |
| sUsageStatsManager, startTime, now, userID, callingPackage); |
| } |
| |
| @Nullable |
| private static UsageEvents loadAppUsageEventsForUserFromService( |
| final IUsageStatsManager usageStatsManager, final long startTime, final long endTime, |
| final int userId, final String callingPackage) { |
| final long start = System.currentTimeMillis(); |
| UsageEvents events = null; |
| try { |
| events = usageStatsManager.queryEventsForUser( |
| startTime, endTime, userId, callingPackage); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Error fetching usage events: ", e); |
| } |
| final long elapsedTime = System.currentTimeMillis() - start; |
| Log.d(TAG, String.format("getAppUsageEventsForUser(): %d from %d to %d in %d/ms", userId, |
| startTime, endTime, elapsedTime)); |
| return events; |
| } |
| |
| @Nullable |
| private static List<BatteryHistEntry> getBatteryHistListFromFromStatsService( |
| final Context context) { |
| List<BatteryHistEntry> batteryHistEntryList = null; |
| try { |
| final BatteryUsageStats batteryUsageStats = getBatteryUsageStats(context); |
| final List<BatteryEntry> batteryEntryList = |
| generateBatteryEntryListFromBatteryUsageStats(context, batteryUsageStats); |
| batteryHistEntryList = convertToBatteryHistEntry(batteryEntryList, batteryUsageStats); |
| closeBatteryUsageStats(batteryUsageStats); |
| } catch (RuntimeException e) { |
| Log.e(TAG, "load batteryUsageStats:" + e); |
| } |
| |
| return batteryHistEntryList; |
| } |
| |
| @VisibleForTesting |
| @Nullable |
| static List<BatteryHistEntry> convertToBatteryHistEntry( |
| @Nullable final List<BatteryEntry> batteryEntryList, |
| final BatteryUsageStats batteryUsageStats) { |
| if (batteryEntryList == null || batteryEntryList.isEmpty()) { |
| Log.w(TAG, "batteryEntryList is null or empty in convertToBatteryHistEntry()"); |
| return null; |
| } |
| return batteryEntryList.stream() |
| .filter(entry -> { |
| final long foregroundMs = entry.getTimeInForegroundMs(); |
| final long backgroundMs = entry.getTimeInBackgroundMs(); |
| return entry.getConsumedPower() > 0 |
| || (entry.getConsumedPower() == 0 |
| && (foregroundMs != 0 || backgroundMs != 0)); |
| }) |
| .map(entry -> ConvertUtils.convertToBatteryHistEntry(entry, batteryUsageStats)) |
| .collect(Collectors.toList()); |
| } |
| |
| /** |
| * 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 long currentTime, |
| final boolean isFromFullCharge, |
| final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap, |
| final Map<Long, Map<String, BatteryHistEntry>> resultMap) { |
| if (rawTimestampList.isEmpty() || expectedTimestampSlots.isEmpty()) { |
| return; |
| } |
| final long expectedStartTimestamp = expectedTimestampSlots.get(0); |
| final long rawStartTimestamp = rawTimestampList.get(0); |
| int startIndex = 0; |
| // If the expected start timestamp is full charge or earlier than what we have, use the |
| // first data of what we have directly. This should be OK because the expected start |
| // timestamp is the nearest even hour of the raw start timestamp, their time diff is no |
| // more than 1 hour. |
| if (isFromFullCharge || expectedStartTimestamp < rawStartTimestamp) { |
| startIndex = 1; |
| resultMap.put(expectedStartTimestamp, batteryHistoryMap.get(rawStartTimestamp)); |
| } |
| final int expectedTimestampSlotsSize = expectedTimestampSlots.size(); |
| for (int index = startIndex; index < expectedTimestampSlotsSize; index++) { |
| final long currentSlot = expectedTimestampSlots.get(index); |
| if (currentSlot > currentTime) { |
| // The slot timestamp is greater than the current time. Puts a placeholder first, |
| // then in the async task, loads the real time battery usage data from the battery |
| // stats service. |
| // If current time is odd hour, one placeholder is added. If the current hour is |
| // even hour, two placeholders are added. This is because the method |
| // insertHourlyUsageDiffDataPerSlot() requires continuing three hours data. |
| resultMap.put(currentSlot, |
| Map.of(CURRENT_TIME_BATTERY_HISTORY_PLACEHOLDER, EMPTY_BATTERY_HIST_ENTRY)); |
| continue; |
| } |
| final boolean isStartOrEnd = index == 0 || index == expectedTimestampSlotsSize - 1; |
| interpolateHistoryForSlot( |
| context, currentSlot, rawTimestampList, batteryHistoryMap, resultMap, |
| isStartOrEnd); |
| } |
| } |
| |
| 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 fist even hour timestamp after the given timestamp. |
| */ |
| private static long getFirstEvenHourAfterTimestamp(long rawTimestamp) { |
| return getLastEvenHourBeforeTimestamp(rawTimestamp + DateUtils.HOUR_IN_MILLIS * 2); |
| } |
| |
| /** |
| * @return Returns the last even hour timestamp before the given timestamp. |
| */ |
| private static long getLastEvenHourBeforeTimestamp(long rawTimestamp) { |
| // If raw hour is even, the hour doesn't need to change as well. |
| // Otherwise, the even hour before raw end should be raw hour - 1. |
| // For example, the even hour before 14:30:50 should be 14:00:00. While the even |
| // hour before 15:30:50 should be 14:00:00. |
| return getEvenHourTimestamp(rawTimestamp, /*addHourOfDay*/ -1); |
| } |
| |
| private static long getEvenHourTimestamp(long rawTimestamp, int addHourOfDay) { |
| final Calendar evenHourCalendar = Calendar.getInstance(); |
| evenHourCalendar.setTimeInMillis(rawTimestamp); |
| // Before computing the evenHourCalendar, record raw hour based on local timezone. |
| final int rawHour = evenHourCalendar.get(Calendar.HOUR_OF_DAY); |
| if (rawHour % 2 != 0) { |
| evenHourCalendar.add(Calendar.HOUR_OF_DAY, addHourOfDay); |
| } |
| evenHourCalendar.set(Calendar.MINUTE, 0); |
| evenHourCalendar.set(Calendar.SECOND, 0); |
| evenHourCalendar.set(Calendar.MILLISECOND, 0); |
| return evenHourCalendar.getTimeInMillis(); |
| } |
| |
| private static List<List<Long>> getHourlyTimestamps(final List<Long> dailyTimestamps) { |
| final List<List<Long>> hourlyTimestamps = new ArrayList<>(); |
| if (dailyTimestamps.size() < MIN_DAILY_DATA_SIZE) { |
| return hourlyTimestamps; |
| } |
| |
| for (int dailyStartIndex = 0; dailyStartIndex < dailyTimestamps.size() - 1; |
| dailyStartIndex++) { |
| long currentTimestamp = dailyTimestamps.get(dailyStartIndex); |
| final long dailyEndTimestamp = dailyTimestamps.get(dailyStartIndex + 1); |
| final List<Long> hourlyTimestampsPerDay = new ArrayList<>(); |
| while (currentTimestamp <= dailyEndTimestamp) { |
| hourlyTimestampsPerDay.add(currentTimestamp); |
| currentTimestamp += MIN_TIME_SLOT; |
| } |
| hourlyTimestamps.add(hourlyTimestampsPerDay); |
| } |
| return hourlyTimestamps; |
| } |
| |
| private static List<BatteryLevelData.PeriodBatteryLevelData> getHourlyPeriodBatteryLevelData( |
| Context context, |
| final Map<Long, Map<String, BatteryHistEntry>> processedBatteryHistoryMap, |
| 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; |
| } |
| // The current time battery history hasn't been loaded yet, returns the current battery |
| // level. |
| if (entryMap.containsKey(CURRENT_TIME_BATTERY_HISTORY_PLACEHOLDER)) { |
| final Intent intent = BatteryUtils.getBatteryIntent(context); |
| return BatteryStatus.getBatteryLevel(intent); |
| } |
| // 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 Set<String> systemAppsSet, |
| final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay, |
| final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap, |
| final Map<Integer, Map<Integer, Map<Long, Map<String, List<AppUsagePeriod>>>>> |
| appUsagePeriodMap, |
| 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, |
| systemAppsSet, |
| appUsagePeriodMap == null |
| || appUsagePeriodMap.get(dailyIndex) == null |
| ? null |
| : appUsagePeriodMap.get(dailyIndex).get(hourlyIndex), |
| batteryHistoryMap); |
| dailyDiffMap.put(hourlyIndex, hourlyBatteryDiffData); |
| } |
| } |
| } |
| |
| private static void insertDailyUsageDiffData( |
| final Context context, |
| 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(context, dailyUsageMap.values())); |
| } |
| } |
| |
| private static void insertAllUsageDiffData( |
| final Context context, |
| 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(context, diffDataList)); |
| resultMap.put(SELECTED_INDEX_ALL, allUsageMap); |
| } |
| |
| @Nullable |
| private static BatteryDiffData insertHourlyUsageDiffDataPerSlot( |
| final Context context, |
| final int currentUserId, |
| final int workProfileUserId, |
| final int currentIndex, |
| final List<Long> timestamps, |
| final Set<String> systemAppsSet, |
| final Map<Long, Map<String, List<AppUsagePeriod>>> appUsageMap, |
| 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()); |
| |
| // Calculates all packages diff usage data in a specific time slot. |
| for (String key : allBatteryHistEntryKeys) { |
| if (key == null) { |
| continue; |
| } |
| 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); |
| |
| final BatteryHistEntry selectedBatteryEntry = |
| selectBatteryHistEntry(currentEntry, nextEntry, nextTwoEntry); |
| if (selectedBatteryEntry == null) { |
| continue; |
| } |
| |
| // Not show other users' battery usage data. |
| final boolean isFromOtherUsers = isConsumedFromOtherUsers( |
| currentUserId, workProfileUserId, selectedBatteryEntry); |
| if (isFromOtherUsers) { |
| continue; |
| } |
| |
| // 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); |
| double foregroundUsageConsumePower = |
| getDiffValue( |
| currentEntry.mForegroundUsageConsumePower, |
| nextEntry.mForegroundUsageConsumePower, |
| nextTwoEntry.mForegroundUsageConsumePower); |
| double foregroundServiceUsageConsumePower = |
| getDiffValue( |
| currentEntry.mForegroundServiceUsageConsumePower, |
| nextEntry.mForegroundServiceUsageConsumePower, |
| nextTwoEntry.mForegroundServiceUsageConsumePower); |
| double backgroundUsageConsumePower = |
| getDiffValue( |
| currentEntry.mBackgroundUsageConsumePower, |
| nextEntry.mBackgroundUsageConsumePower, |
| nextTwoEntry.mBackgroundUsageConsumePower); |
| double cachedUsageConsumePower = |
| getDiffValue( |
| currentEntry.mCachedUsageConsumePower, |
| nextEntry.mCachedUsageConsumePower, |
| nextTwoEntry.mCachedUsageConsumePower); |
| // Excludes entry since we don't have enough data to calculate. |
| if (foregroundUsageTimeInMs == 0 |
| && backgroundUsageTimeInMs == 0 |
| && consumePower == 0) { |
| 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 (sDebug) { |
| 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; |
| foregroundUsageConsumePower = foregroundUsageConsumePower * ratio; |
| foregroundServiceUsageConsumePower = foregroundServiceUsageConsumePower * ratio; |
| backgroundUsageConsumePower = backgroundUsageConsumePower * ratio; |
| cachedUsageConsumePower = cachedUsageConsumePower * ratio; |
| } |
| |
| // Compute the screen on time and make sure it won't exceed the threshold. |
| final long screenOnTime = Math.min( |
| (long) TOTAL_HOURLY_TIME_THRESHOLD, |
| getScreenOnTime( |
| appUsageMap, |
| selectedBatteryEntry.mUserId, |
| selectedBatteryEntry.mPackageName)); |
| // Make sure the background + screen-on time will not exceed the threshold. |
| backgroundUsageTimeInMs = Math.min( |
| backgroundUsageTimeInMs, (long) TOTAL_HOURLY_TIME_THRESHOLD - screenOnTime); |
| final BatteryDiffEntry currentBatteryDiffEntry = new BatteryDiffEntry( |
| context, |
| foregroundUsageTimeInMs, |
| backgroundUsageTimeInMs, |
| screenOnTime, |
| consumePower, |
| foregroundUsageConsumePower, |
| foregroundServiceUsageConsumePower, |
| backgroundUsageConsumePower, |
| cachedUsageConsumePower, |
| selectedBatteryEntry); |
| if (currentBatteryDiffEntry.isSystemEntry()) { |
| systemEntries.add(currentBatteryDiffEntry); |
| } else { |
| appEntries.add(currentBatteryDiffEntry); |
| } |
| } |
| |
| // If there is no data, return null instead of empty item. |
| if (appEntries.isEmpty() && systemEntries.isEmpty()) { |
| return null; |
| } |
| |
| return new BatteryDiffData( |
| context, appEntries, systemEntries, systemAppsSet, /* isAccumulated= */ false); |
| } |
| |
| private static long getScreenOnTime(@Nullable final List<AppUsagePeriod> appUsagePeriodList) { |
| if (appUsagePeriodList == null || appUsagePeriodList.isEmpty()) { |
| return 0; |
| } |
| // Create a list of endpoints (the beginning or the end) of usage periods and order the list |
| // chronologically. |
| final List<AppUsageEndPoint> endPoints = |
| appUsagePeriodList.stream() |
| .flatMap( |
| foregroundUsage -> |
| Stream.of( |
| AppUsageEndPoint.newBuilder() |
| .setTimestamp( |
| foregroundUsage.getStartTime()) |
| .setType(AppUsageEndPointType.START) |
| .build(), |
| AppUsageEndPoint.newBuilder() |
| .setTimestamp(foregroundUsage.getEndTime()) |
| .setType(AppUsageEndPointType.END) |
| .build())) |
| .sorted((x, y) -> (int) (x.getTimestamp() - y.getTimestamp())) |
| .collect(Collectors.toList()); |
| |
| // Traverse the list of endpoints in order to determine the non-overlapping usage duration. |
| int numberOfActiveAppUsagePeriods = 0; |
| long startOfCurrentContiguousAppUsagePeriod = 0; |
| long totalScreenOnTime = 0; |
| for (final AppUsageEndPoint endPoint : endPoints) { |
| if (endPoint.getType() == AppUsageEndPointType.START) { |
| if (numberOfActiveAppUsagePeriods++ == 0) { |
| startOfCurrentContiguousAppUsagePeriod = endPoint.getTimestamp(); |
| } |
| } else { |
| if (--numberOfActiveAppUsagePeriods == 0) { |
| totalScreenOnTime += |
| (endPoint.getTimestamp() - startOfCurrentContiguousAppUsagePeriod); |
| } |
| } |
| } |
| |
| return totalScreenOnTime; |
| } |
| |
| 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 Context context, final Collection<BatteryDiffData> diffEntryListData) { |
| 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); |
| } |
| for (BatteryDiffEntry entry : diffEntryList.getSystemDiffEntryList()) { |
| computeUsageDiffDataPerEntry(entry, diffEntryMap); |
| } |
| } |
| |
| final Collection<BatteryDiffEntry> diffEntryList = diffEntryMap.values(); |
| for (BatteryDiffEntry entry : diffEntryList) { |
| if (entry.isSystemEntry()) { |
| systemEntries.add(entry); |
| } else { |
| appEntries.add(entry); |
| } |
| } |
| |
| return diffEntryList.isEmpty() ? null : new BatteryDiffData(context, appEntries, |
| systemEntries, /* systemAppsSet= */ null, /* isAccumulated= */ true); |
| } |
| |
| private static void computeUsageDiffDataPerEntry( |
| final BatteryDiffEntry entry, |
| final Map<String, BatteryDiffEntry> diffEntryMap) { |
| final String key = entry.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.mScreenOnTimeInMs += |
| entry.mScreenOnTimeInMs; |
| oldBatteryDiffEntry.mConsumePower += entry.mConsumePower; |
| oldBatteryDiffEntry.mForegroundUsageConsumePower += entry.mForegroundUsageConsumePower; |
| oldBatteryDiffEntry.mForegroundServiceUsageConsumePower |
| += entry.mForegroundServiceUsageConsumePower; |
| oldBatteryDiffEntry.mBackgroundUsageConsumePower += entry.mBackgroundUsageConsumePower; |
| oldBatteryDiffEntry.mCachedUsageConsumePower += entry.mCachedUsageConsumePower; |
| } |
| } |
| |
| private static boolean shouldShowBatteryAttributionList(final Context context) { |
| final PowerProfile powerProfile = new PowerProfile(context); |
| // Cheap hack to try to figure out if the power_profile.xml was populated. |
| final double averagePowerForOrdinal = powerProfile.getAveragePowerForOrdinal( |
| PowerProfile.POWER_GROUP_DISPLAY_SCREEN_FULL, 0); |
| final boolean shouldShowBatteryAttributionList = |
| averagePowerForOrdinal >= MIN_AVERAGE_POWER_THRESHOLD_MILLI_AMP; |
| if (!shouldShowBatteryAttributionList) { |
| Log.w(TAG, "shouldShowBatteryAttributionList(): " + averagePowerForOrdinal); |
| } |
| return shouldShowBatteryAttributionList; |
| } |
| |
| /** |
| * We want to coalesce some UIDs. For example, dex2oat runs under a shared gid that |
| * exists for all users of the same app. We detect this case and merge the power use |
| * for dex2oat to the device OWNER's use of the app. |
| * |
| * @return A sorted list of apps using power. |
| */ |
| private static List<BatteryEntry> getCoalescedUsageList(final Context context, |
| final BatteryUtils batteryUtils, |
| final BatteryUsageStats batteryUsageStats, |
| final boolean loadDataInBackground) { |
| final PackageManager packageManager = context.getPackageManager(); |
| final UserManager userManager = context.getSystemService(UserManager.class); |
| final SparseArray<BatteryEntry> batteryEntryList = new SparseArray<>(); |
| final ArrayList<BatteryEntry> results = new ArrayList<>(); |
| final List<UidBatteryConsumer> uidBatteryConsumers = |
| batteryUsageStats.getUidBatteryConsumers(); |
| |
| // Sort to have all apps with "real" UIDs first, followed by apps that are supposed |
| // to be combined with the real ones. |
| uidBatteryConsumers.sort(Comparator.comparingInt( |
| consumer -> consumer.getUid() == getRealUid(consumer) ? 0 : 1)); |
| |
| for (int i = 0, size = uidBatteryConsumers.size(); i < size; i++) { |
| final UidBatteryConsumer consumer = uidBatteryConsumers.get(i); |
| final int uid = getRealUid(consumer); |
| |
| final String[] packages = packageManager.getPackagesForUid(uid); |
| if (batteryUtils.shouldHideUidBatteryConsumerUnconditionally(consumer, packages)) { |
| continue; |
| } |
| |
| final boolean isHidden = batteryUtils.shouldHideUidBatteryConsumer(consumer, packages); |
| final int index = batteryEntryList.indexOfKey(uid); |
| if (index < 0) { |
| // New entry. |
| batteryEntryList.put(uid, new BatteryEntry(context, userManager, consumer, |
| isHidden, uid, packages, null, loadDataInBackground)); |
| } else { |
| // Combine BatterySippers if we already have one with this UID. |
| final BatteryEntry existingSipper = batteryEntryList.valueAt(index); |
| existingSipper.add(consumer); |
| } |
| } |
| |
| final BatteryConsumer deviceConsumer = batteryUsageStats.getAggregateBatteryConsumer( |
| BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE); |
| |
| for (int componentId = 0; componentId < BatteryConsumer.POWER_COMPONENT_COUNT; |
| componentId++) { |
| results.add(new BatteryEntry(context, componentId, |
| deviceConsumer.getConsumedPower(componentId), |
| deviceConsumer.getUsageDurationMillis(componentId), |
| componentId == POWER_COMPONENT_SYSTEM_SERVICES |
| || componentId == POWER_COMPONENT_WAKELOCK)); |
| } |
| |
| for (int componentId = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID; |
| componentId < BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID |
| + deviceConsumer.getCustomPowerComponentCount(); |
| componentId++) { |
| results.add(new BatteryEntry(context, componentId, |
| deviceConsumer.getCustomPowerComponentName(componentId), |
| deviceConsumer.getConsumedPowerForCustomComponent(componentId))); |
| } |
| |
| final List<UserBatteryConsumer> userBatteryConsumers = |
| batteryUsageStats.getUserBatteryConsumers(); |
| for (int i = 0, size = userBatteryConsumers.size(); i < size; i++) { |
| final UserBatteryConsumer consumer = userBatteryConsumers.get(i); |
| results.add(new BatteryEntry(context, userManager, consumer, /* isHidden */ true, |
| Process.INVALID_UID, null, null, loadDataInBackground)); |
| } |
| |
| final int numUidSippers = batteryEntryList.size(); |
| |
| for (int i = 0; i < numUidSippers; i++) { |
| results.add(batteryEntryList.valueAt(i)); |
| } |
| |
| // The sort order must have changed, so re-sort based on total power use. |
| results.sort(BatteryEntry.COMPARATOR); |
| return results; |
| } |
| |
| private static int getRealUid(final UidBatteryConsumer consumer) { |
| int realUid = consumer.getUid(); |
| |
| // Check if this UID is a shared GID. If so, we combine it with the OWNER's |
| // actual app UID. |
| if (isSharedGid(consumer.getUid())) { |
| realUid = UserHandle.getUid(UserHandle.USER_SYSTEM, |
| UserHandle.getAppIdFromSharedAppGid(consumer.getUid())); |
| } |
| |
| // Check if this UID is a system UID (mediaserver, logd, nfc, drm, etc). |
| if (isSystemUid(realUid) |
| && !MEDIASERVER_PACKAGE_NAME.equals(consumer.getPackageWithHighestDrain())) { |
| // Use the system UID for all UIDs running in their own sandbox that |
| // are not apps. We exclude mediaserver because we already are expected to |
| // report that as a separate item. |
| realUid = Process.SYSTEM_UID; |
| } |
| return realUid; |
| } |
| |
| private static boolean isSharedGid(final int uid) { |
| return UserHandle.getAppIdFromSharedAppGid(uid) > 0; |
| } |
| |
| private static boolean isSystemUid(final int uid) { |
| final int appUid = UserHandle.getAppId(uid); |
| return appUid >= Process.SYSTEM_UID && appUid < Process.FIRST_APPLICATION_UID; |
| } |
| |
| 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 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 Set<String> getSystemAppsSet(Context context) { |
| return sTestSystemAppsSet != null ? sTestSystemAppsSet |
| : AppListRepositoryUtil.getSystemPackageNames(context, context.getUserId(), false); |
| } |
| |
| private static long getCurrentTimeMillis() { |
| return sTestCurrentTimeMillis > 0 ? sTestCurrentTimeMillis : System.currentTimeMillis(); |
| } |
| |
| private static void log(Context context, final String content, final long timestamp, |
| final BatteryHistEntry entry) { |
| if (sDebug) { |
| Log.d(TAG, String.format(entry != null ? "%s %s:\n%s" : "%s %s:%s", |
| utcToLocalTime(context, timestamp), content, entry)); |
| } |
| } |
| } |