1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.fuelgauge.batteryusage; 18 19 import static com.android.settings.fuelgauge.batteryusage.ConvertUtils.getEffectivePackageName; 20 import static com.android.settings.fuelgauge.batteryusage.ConvertUtils.isSystemConsumer; 21 import static com.android.settings.fuelgauge.batteryusage.ConvertUtils.isUidConsumer; 22 import static com.android.settingslib.fuelgauge.BatteryStatus.BATTERY_LEVEL_UNKNOWN; 23 24 import android.app.usage.IUsageStatsManager; 25 import android.app.usage.UsageEvents; 26 import android.app.usage.UsageEvents.Event; 27 import android.content.ContentValues; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.pm.PackageManager; 31 import android.content.pm.UserInfo; 32 import android.os.BatteryConsumer; 33 import android.os.BatteryStatsManager; 34 import android.os.BatteryUsageStats; 35 import android.os.BatteryUsageStatsQuery; 36 import android.os.Process; 37 import android.os.RemoteException; 38 import android.os.ServiceManager; 39 import android.os.UidBatteryConsumer; 40 import android.os.UserBatteryConsumer; 41 import android.os.UserHandle; 42 import android.os.UserManager; 43 import android.text.TextUtils; 44 import android.text.format.DateUtils; 45 import android.util.ArrayMap; 46 import android.util.ArraySet; 47 import android.util.Log; 48 import android.util.SparseArray; 49 50 import androidx.annotation.NonNull; 51 import androidx.annotation.Nullable; 52 53 import com.android.internal.annotations.VisibleForTesting; 54 import com.android.internal.os.PowerProfile; 55 import com.android.settings.Utils; 56 import com.android.settings.fuelgauge.BatteryUtils; 57 import com.android.settings.overlay.FeatureFactory; 58 import com.android.settingslib.fuelgauge.BatteryStatus; 59 import com.android.settingslib.spaprivileged.model.app.AppListRepositoryUtil; 60 61 import com.google.common.base.Preconditions; 62 63 import java.time.Duration; 64 import java.util.ArrayList; 65 import java.util.Calendar; 66 import java.util.Collection; 67 import java.util.Collections; 68 import java.util.Comparator; 69 import java.util.List; 70 import java.util.Map; 71 import java.util.Set; 72 import java.util.stream.Collectors; 73 import java.util.stream.Stream; 74 75 /** 76 * A utility class to process data loaded from database and make the data easy to use for battery 77 * usage UI. 78 */ 79 public final class DataProcessor { 80 private static final String TAG = "DataProcessor"; 81 private static final int POWER_COMPONENT_SYSTEM_SERVICES = 7; 82 private static final int POWER_COMPONENT_WAKELOCK = 12; 83 private static final int MIN_AVERAGE_POWER_THRESHOLD_MILLI_AMP = 10; 84 private static final int MIN_DAILY_DATA_SIZE = 2; 85 private static final int MAX_DIFF_SECONDS_OF_UPPER_TIMESTAMP = 5; 86 private static final String MEDIASERVER_PACKAGE_NAME = "mediaserver"; 87 private static final String ANDROID_CORE_APPS_SHARED_USER_ID = "android.uid.shared"; 88 private static final Map<String, BatteryHistEntry> EMPTY_BATTERY_MAP = new ArrayMap<>(); 89 private static final BatteryHistEntry EMPTY_BATTERY_HIST_ENTRY = 90 new BatteryHistEntry(new ContentValues()); 91 92 @VisibleForTesting 93 static final long DEFAULT_USAGE_DURATION_FOR_INCOMPLETE_INTERVAL = 94 DateUtils.SECOND_IN_MILLIS * 30; 95 96 @VisibleForTesting 97 static final int SELECTED_INDEX_ALL = BatteryChartViewModel.SELECTED_INDEX_ALL; 98 99 @VisibleForTesting 100 static final Comparator<AppUsageEvent> APP_USAGE_EVENT_TIMESTAMP_COMPARATOR = 101 Comparator.comparing(AppUsageEvent::getTimestamp); 102 103 @VisibleForTesting 104 static final Comparator<BatteryEvent> BATTERY_EVENT_TIMESTAMP_COMPARATOR = 105 Comparator.comparing(BatteryEvent::getTimestamp); 106 107 @VisibleForTesting 108 static boolean sDebug = false; 109 110 @VisibleForTesting 111 static long sTestCurrentTimeMillis = 0; 112 113 @VisibleForTesting 114 static Set<String> sTestSystemAppsPackageNames; 115 116 @VisibleForTesting 117 static IUsageStatsManager sUsageStatsManager = 118 IUsageStatsManager.Stub.asInterface( 119 ServiceManager.getService(Context.USAGE_STATS_SERVICE)); 120 121 public static final String CURRENT_TIME_BATTERY_HISTORY_PLACEHOLDER = 122 "CURRENT_TIME_BATTERY_HISTORY_PLACEHOLDER"; 123 124 /** A callback listener when battery usage loading async task is executed. */ 125 public interface UsageMapAsyncResponse { 126 /** The callback function when batteryUsageMap is loaded. */ onBatteryCallbackDataLoaded( Map<Integer, Map<Integer, BatteryDiffData>> batteryCallbackData)127 void onBatteryCallbackDataLoaded( 128 Map<Integer, Map<Integer, BatteryDiffData>> batteryCallbackData); 129 } 130 DataProcessor()131 private DataProcessor() { 132 } 133 134 /** 135 * @return Returns battery usage data of different entries. 136 * Returns null if the input is invalid or there is no enough data. 137 */ 138 @Nullable getBatteryUsageData( Context context, @Nullable final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap)139 public static Map<Integer, Map<Integer, BatteryDiffData>> getBatteryUsageData( 140 Context context, 141 @Nullable final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) { 142 if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) { 143 Log.d(TAG, "getBatteryLevelData() returns null"); 144 return null; 145 } 146 // Process raw history map data into hourly timestamps. 147 final Map<Long, Map<String, BatteryHistEntry>> processedBatteryHistoryMap = 148 getHistoryMapWithExpectedTimestamps(context, batteryHistoryMap); 149 // Wrap and processed history map into easy-to-use format for UI rendering. 150 final BatteryLevelData batteryLevelData = 151 getLevelDataThroughProcessedHistoryMap(context, processedBatteryHistoryMap); 152 // Loads the current battery usage data from the battery stats service. 153 final Map<String, BatteryHistEntry> currentBatteryHistoryMap = 154 getCurrentBatteryHistoryMapFromStatsService(context); 155 // Replaces the placeholder in processedBatteryHistoryMap. 156 for (Map.Entry<Long, Map<String, BatteryHistEntry>> mapEntry 157 : processedBatteryHistoryMap.entrySet()) { 158 if (mapEntry.getValue().containsKey(CURRENT_TIME_BATTERY_HISTORY_PLACEHOLDER)) { 159 mapEntry.setValue(currentBatteryHistoryMap); 160 } 161 } 162 return batteryLevelData == null 163 ? null 164 : generateBatteryUsageMap(context, 165 getBatteryDiffDataMap(context, 166 batteryLevelData.getHourlyBatteryLevelsPerDay(), 167 processedBatteryHistoryMap, 168 /*appUsagePeriodMap=*/ null, 169 getSystemAppsPackageNames(context), 170 getSystemAppsUids(context)), 171 batteryLevelData); 172 } 173 174 /** 175 * Gets the {@link BatteryUsageStats} from system service. 176 */ 177 @Nullable getBatteryUsageStats(final Context context)178 public static BatteryUsageStats getBatteryUsageStats(final Context context) { 179 final BatteryUsageStatsQuery batteryUsageStatsQuery = 180 new BatteryUsageStatsQuery 181 .Builder() 182 .includeBatteryHistory() 183 .includeProcessStateData() 184 .build(); 185 return context.getSystemService(BatteryStatsManager.class) 186 .getBatteryUsageStats(batteryUsageStatsQuery); 187 } 188 189 /** 190 * Gets the {@link UsageEvents} from system service for all unlocked users. 191 */ 192 @Nullable getAppUsageEvents(Context context)193 public static Map<Long, UsageEvents> getAppUsageEvents(Context context) { 194 final long start = System.currentTimeMillis(); 195 context = DatabaseUtils.getParentContext(context); 196 if (context == null) { 197 return null; 198 } 199 final Map<Long, UsageEvents> resultMap = new ArrayMap(); 200 final UserManager userManager = context.getSystemService(UserManager.class); 201 if (userManager == null) { 202 return null; 203 } 204 final long sixDaysAgoTimestamp = 205 DatabaseUtils.getTimestampSixDaysAgo(Calendar.getInstance()); 206 for (final UserInfo user : userManager.getAliveUsers()) { 207 final UsageEvents events = getAppUsageEventsForUser( 208 context, userManager, user.id, sixDaysAgoTimestamp); 209 if (events != null) { 210 resultMap.put(Long.valueOf(user.id), events); 211 } 212 } 213 final long elapsedTime = System.currentTimeMillis() - start; 214 Log.d(TAG, String.format("getAppUsageEvents() for all unlocked users in %d/ms", 215 elapsedTime)); 216 return resultMap.isEmpty() ? null : resultMap; 217 } 218 219 /** 220 * Gets the {@link UsageEvents} from system service for the specific user. 221 */ 222 @Nullable getAppUsageEventsForUser( Context context, final int userID, final long startTimestampOfLevelData)223 public static UsageEvents getAppUsageEventsForUser( 224 Context context, final int userID, final long startTimestampOfLevelData) { 225 final long start = System.currentTimeMillis(); 226 context = DatabaseUtils.getParentContext(context); 227 if (context == null) { 228 return null; 229 } 230 final UserManager userManager = context.getSystemService(UserManager.class); 231 if (userManager == null) { 232 return null; 233 } 234 final long sixDaysAgoTimestamp = 235 DatabaseUtils.getTimestampSixDaysAgo(Calendar.getInstance()); 236 final long earliestTimestamp = Math.max(sixDaysAgoTimestamp, startTimestampOfLevelData); 237 final UsageEvents events = getAppUsageEventsForUser( 238 context, userManager, userID, earliestTimestamp); 239 final long elapsedTime = System.currentTimeMillis() - start; 240 Log.d(TAG, String.format("getAppUsageEventsForUser() for user %d in %d/ms", 241 userID, elapsedTime)); 242 return events; 243 } 244 245 /** 246 * Closes the {@link BatteryUsageStats} after using it. 247 */ closeBatteryUsageStats(BatteryUsageStats batteryUsageStats)248 public static void closeBatteryUsageStats(BatteryUsageStats batteryUsageStats) { 249 if (batteryUsageStats != null) { 250 try { 251 batteryUsageStats.close(); 252 } catch (Exception e) { 253 Log.e(TAG, "BatteryUsageStats.close() failed", e); 254 } 255 } 256 } 257 258 /** 259 * Generates the indexed {@link AppUsagePeriod} list data for each corresponding time slot. 260 * Attributes the list of {@link AppUsageEvent} into hourly time slots and reformat them into 261 * {@link AppUsagePeriod} for easier use in the following process. 262 * 263 * <p>There could be 2 cases of the returned value:</p> 264 * <ul> 265 * <li>null: empty or invalid data.</li> 266 * <li>non-null: must be a 2d map and composed by: 267 * <p> [0][0] ~ [maxDailyIndex][maxHourlyIndex]</p></li> 268 * </ul> 269 * 270 * <p>The structure is consistent with the battery usage map returned by 271 * {@code generateBatteryUsageMap}.</p> 272 * 273 * <p>{@code Long} stands for the userId.</p> 274 * <p>{@code String} stands for the packageName.</p> 275 */ 276 @Nullable 277 public static Map<Integer, Map<Integer, Map<Long, Map<String, List<AppUsagePeriod>>>>> generateAppUsagePeriodMap( Context context, final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay, final List<AppUsageEvent> appUsageEventList, final List<BatteryEvent> batteryEventList)278 generateAppUsagePeriodMap( 279 Context context, 280 final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay, 281 final List<AppUsageEvent> appUsageEventList, 282 final List<BatteryEvent> batteryEventList) { 283 if (appUsageEventList.isEmpty()) { 284 Log.w(TAG, "appUsageEventList is empty"); 285 return null; 286 } 287 // Sorts the appUsageEventList and batteryEventList in ascending order based on the 288 // timestamp before distribution. 289 Collections.sort(appUsageEventList, APP_USAGE_EVENT_TIMESTAMP_COMPARATOR); 290 Collections.sort(batteryEventList, BATTERY_EVENT_TIMESTAMP_COMPARATOR); 291 final Map<Integer, Map<Integer, Map<Long, Map<String, List<AppUsagePeriod>>>>> resultMap = 292 new ArrayMap<>(); 293 294 for (int dailyIndex = 0; dailyIndex < hourlyBatteryLevelsPerDay.size(); dailyIndex++) { 295 final Map<Integer, Map<Long, Map<String, List<AppUsagePeriod>>>> dailyMap = 296 new ArrayMap<>(); 297 resultMap.put(dailyIndex, dailyMap); 298 if (hourlyBatteryLevelsPerDay.get(dailyIndex) == null) { 299 continue; 300 } 301 final List<Long> timestamps = hourlyBatteryLevelsPerDay.get(dailyIndex).getTimestamps(); 302 for (int hourlyIndex = 0; hourlyIndex < timestamps.size() - 1; hourlyIndex++) { 303 final long startTimestamp = timestamps.get(hourlyIndex); 304 final long endTimestamp = timestamps.get(hourlyIndex + 1); 305 // Gets the app usage event list for this hourly slot first. 306 final List<AppUsageEvent> hourlyAppUsageEventList = 307 getAppUsageEventListWithinTimeRangeWithBuffer( 308 appUsageEventList, startTimestamp, endTimestamp); 309 310 // The value could be null when there is no data in the hourly slot. 311 dailyMap.put( 312 hourlyIndex, 313 buildAppUsagePeriodList(context, hourlyAppUsageEventList, batteryEventList, 314 startTimestamp, endTimestamp)); 315 } 316 } 317 return resultMap; 318 } 319 320 /** 321 * Generates the list of {@link AppUsageEvent} from the supplied {@link UsageEvents}. 322 */ generateAppUsageEventListFromUsageEvents( Context context, Map<Long, UsageEvents> usageEventsMap)323 public static List<AppUsageEvent> generateAppUsageEventListFromUsageEvents( 324 Context context, Map<Long, UsageEvents> usageEventsMap) { 325 final List<AppUsageEvent> appUsageEventList = new ArrayList<>(); 326 long numEventsFetched = 0; 327 long numAllEventsFetched = 0; 328 final Set<String> ignoreScreenOnTimeTaskRootSet = 329 FeatureFactory.getFactory(context) 330 .getPowerUsageFeatureProvider(context) 331 .getIgnoreScreenOnTimeTaskRootSet(); 332 for (final long userId : usageEventsMap.keySet()) { 333 final UsageEvents usageEvents = usageEventsMap.get(userId); 334 while (usageEvents.hasNextEvent()) { 335 final Event event = new Event(); 336 usageEvents.getNextEvent(event); 337 numAllEventsFetched++; 338 switch (event.getEventType()) { 339 case Event.ACTIVITY_RESUMED: 340 case Event.ACTIVITY_STOPPED: 341 case Event.DEVICE_SHUTDOWN: 342 final String taskRootClassName = event.getTaskRootClassName(); 343 if (!TextUtils.isEmpty(taskRootClassName) 344 && ignoreScreenOnTimeTaskRootSet.contains(taskRootClassName)) { 345 Log.w(TAG, String.format( 346 "Ignoring a usage event with task root class name %s, " 347 + "(timestamp=%d, type=%d)", 348 taskRootClassName, 349 event.getTimeStamp(), 350 event.getEventType())); 351 break; 352 } 353 final AppUsageEvent appUsageEvent = 354 ConvertUtils.convertToAppUsageEvent( 355 context, sUsageStatsManager, event, userId); 356 if (appUsageEvent != null) { 357 numEventsFetched++; 358 appUsageEventList.add(appUsageEvent); 359 } 360 break; 361 default: 362 break; 363 } 364 } 365 } 366 Log.w(TAG, String.format( 367 "Read %d relevant events (%d total) from UsageStatsManager", numEventsFetched, 368 numAllEventsFetched)); 369 return appUsageEventList; 370 } 371 372 /** 373 * Generates the list of {@link BatteryEntry} from the supplied {@link BatteryUsageStats}. 374 */ 375 @Nullable generateBatteryEntryListFromBatteryUsageStats( final Context context, @Nullable final BatteryUsageStats batteryUsageStats)376 public static List<BatteryEntry> generateBatteryEntryListFromBatteryUsageStats( 377 final Context context, 378 @Nullable final BatteryUsageStats batteryUsageStats) { 379 if (batteryUsageStats == null) { 380 Log.w(TAG, "batteryUsageStats is null content"); 381 return null; 382 } 383 if (!shouldShowBatteryAttributionList(context)) { 384 return null; 385 } 386 final BatteryUtils batteryUtils = BatteryUtils.getInstance(context); 387 final int dischargePercentage = Math.max(0, batteryUsageStats.getDischargePercentage()); 388 final List<BatteryEntry> usageList = getCoalescedUsageList( 389 context, batteryUtils, batteryUsageStats, /*loadDataInBackground=*/ false); 390 final double totalPower = batteryUsageStats.getConsumedPower(); 391 for (int i = 0; i < usageList.size(); i++) { 392 final BatteryEntry entry = usageList.get(i); 393 final double percentOfTotal = batteryUtils.calculateBatteryPercent( 394 entry.getConsumedPower(), totalPower, dischargePercentage); 395 entry.mPercent = percentOfTotal; 396 } 397 return usageList; 398 } 399 400 /** 401 * @return Returns the latest battery history map loaded from the battery stats service. 402 */ getCurrentBatteryHistoryMapFromStatsService( final Context context)403 public static Map<String, BatteryHistEntry> getCurrentBatteryHistoryMapFromStatsService( 404 final Context context) { 405 final List<BatteryHistEntry> batteryHistEntryList = 406 getBatteryHistListFromFromStatsService(context); 407 return batteryHistEntryList == null ? new ArrayMap<>() 408 : batteryHistEntryList.stream().collect(Collectors.toMap(e -> e.getKey(), e -> e)); 409 } 410 411 /** 412 * @return Returns the processed history map which has interpolated to every hour data. 413 * The start timestamp is the first timestamp in batteryHistoryMap. The end timestamp is current 414 * time. The keys of processed history map should contain every hour between the start and end 415 * timestamp. If there's no data in some key, the value will be the empty map. 416 */ getHistoryMapWithExpectedTimestamps( Context context, final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap)417 static Map<Long, Map<String, BatteryHistEntry>> getHistoryMapWithExpectedTimestamps( 418 Context context, 419 final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) { 420 final long startTime = System.currentTimeMillis(); 421 final List<Long> rawTimestampList = new ArrayList<>(batteryHistoryMap.keySet()); 422 final Map<Long, Map<String, BatteryHistEntry>> resultMap = new ArrayMap(); 423 if (rawTimestampList.isEmpty()) { 424 Log.d(TAG, "empty batteryHistoryMap in getHistoryMapWithExpectedTimestamps()"); 425 return resultMap; 426 } 427 Collections.sort(rawTimestampList); 428 final long currentTime = getCurrentTimeMillis(); 429 final List<Long> expectedTimestampList = getTimestampSlots(rawTimestampList, currentTime); 430 interpolateHistory( 431 context, rawTimestampList, expectedTimestampList, batteryHistoryMap, resultMap); 432 Log.d(TAG, String.format("getHistoryMapWithExpectedTimestamps() size=%d in %d/ms", 433 resultMap.size(), (System.currentTimeMillis() - startTime))); 434 return resultMap; 435 } 436 437 @Nullable getLevelDataThroughProcessedHistoryMap( Context context, final Map<Long, Map<String, BatteryHistEntry>> processedBatteryHistoryMap)438 static BatteryLevelData getLevelDataThroughProcessedHistoryMap( 439 Context context, 440 final Map<Long, Map<String, BatteryHistEntry>> processedBatteryHistoryMap) { 441 // There should be at least the start and end timestamps. Otherwise, return null to not show 442 // data in usage chart. 443 if (processedBatteryHistoryMap.size() < MIN_DAILY_DATA_SIZE) { 444 return null; 445 } 446 Map<Long, Integer> batteryLevelMap = new ArrayMap<>(); 447 for (Long timestamp : processedBatteryHistoryMap.keySet()) { 448 batteryLevelMap.put( 449 timestamp, getLevel(context, processedBatteryHistoryMap, timestamp)); 450 } 451 return new BatteryLevelData(batteryLevelMap); 452 } 453 454 /** 455 * Computes expected timestamp slots. The start timestamp is the first timestamp in 456 * rawTimestampList. The end timestamp is current time. The middle timestamps are the sharp hour 457 * timestamps between the start and end timestamps. 458 */ 459 @VisibleForTesting getTimestampSlots(final List<Long> rawTimestampList, final long currentTime)460 static List<Long> getTimestampSlots(final List<Long> rawTimestampList, final long currentTime) { 461 final List<Long> timestampSlots = new ArrayList<>(); 462 if (rawTimestampList.isEmpty()) { 463 return timestampSlots; 464 } 465 final long startTimestamp = rawTimestampList.get(0); 466 final long endTimestamp = currentTime; 467 // If the start timestamp is later or equal the end one, return the empty list. 468 if (startTimestamp >= endTimestamp) { 469 return timestampSlots; 470 } 471 timestampSlots.add(startTimestamp); 472 for (long timestamp = TimestampUtils.getNextHourTimestamp(startTimestamp); 473 timestamp < endTimestamp; timestamp += DateUtils.HOUR_IN_MILLIS) { 474 timestampSlots.add(timestamp); 475 } 476 timestampSlots.add(endTimestamp); 477 return timestampSlots; 478 } 479 480 @VisibleForTesting isFromFullCharge(@ullable final Map<String, BatteryHistEntry> entryList)481 static boolean isFromFullCharge(@Nullable final Map<String, BatteryHistEntry> entryList) { 482 if (entryList == null) { 483 Log.d(TAG, "entryList is null in isFromFullCharge()"); 484 return false; 485 } 486 final List<String> entryKeys = new ArrayList<>(entryList.keySet()); 487 if (entryKeys.isEmpty()) { 488 Log.d(TAG, "empty entryList in isFromFullCharge()"); 489 return false; 490 } 491 // The hist entries in the same timestamp should have same battery status and level. 492 // Checking the first one should be enough. 493 final BatteryHistEntry firstHistEntry = entryList.get(entryKeys.get(0)); 494 return BatteryStatus.isCharged(firstHistEntry.mBatteryStatus, firstHistEntry.mBatteryLevel); 495 } 496 497 @VisibleForTesting findNearestTimestamp(final List<Long> timestamps, final long target)498 static long[] findNearestTimestamp(final List<Long> timestamps, final long target) { 499 final long[] results = new long[]{Long.MIN_VALUE, Long.MAX_VALUE}; 500 // Searches the nearest lower and upper timestamp value. 501 timestamps.forEach(timestamp -> { 502 if (timestamp <= target && timestamp > results[0]) { 503 results[0] = timestamp; 504 } 505 if (timestamp >= target && timestamp < results[1]) { 506 results[1] = timestamp; 507 } 508 }); 509 // Uses zero value to represent invalid searching result. 510 results[0] = results[0] == Long.MIN_VALUE ? 0 : results[0]; 511 results[1] = results[1] == Long.MAX_VALUE ? 0 : results[1]; 512 return results; 513 } 514 getBatteryDiffDataMap( 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, final @NonNull Set<String> systemAppsPackageNames, final @NonNull Set<Integer> systemAppsUids)515 static Map<Long, BatteryDiffData> getBatteryDiffDataMap( 516 Context context, 517 final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay, 518 final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap, 519 final Map<Integer, Map<Integer, Map<Long, Map<String, List<AppUsagePeriod>>>>> 520 appUsagePeriodMap, 521 final @NonNull Set<String> systemAppsPackageNames, 522 final @NonNull Set<Integer> systemAppsUids) { 523 final Map<Long, BatteryDiffData> batteryDiffDataMap = new ArrayMap<>(); 524 final int currentUserId = context.getUserId(); 525 final UserHandle userHandle = 526 Utils.getManagedProfile(context.getSystemService(UserManager.class)); 527 final int workProfileUserId = 528 userHandle != null ? userHandle.getIdentifier() : Integer.MIN_VALUE; 529 // Each time slot usage diff data = 530 // sum(Math.abs(timestamp[i+1] data - timestamp[i] data)); 531 // since we want to aggregate every hour usage diff data into a single time slot. 532 for (int dailyIndex = 0; dailyIndex < hourlyBatteryLevelsPerDay.size(); dailyIndex++) { 533 if (hourlyBatteryLevelsPerDay.get(dailyIndex) == null) { 534 continue; 535 } 536 final List<Long> hourlyTimestamps = 537 hourlyBatteryLevelsPerDay.get(dailyIndex).getTimestamps(); 538 for (int hourlyIndex = 0; hourlyIndex < hourlyTimestamps.size() - 1; hourlyIndex++) { 539 final Long startTimestamp = hourlyTimestamps.get(hourlyIndex); 540 final Long endTimestamp = hourlyTimestamps.get(hourlyIndex + 1); 541 final int startBatteryLevel = 542 hourlyBatteryLevelsPerDay.get(dailyIndex).getLevels().get(hourlyIndex); 543 final int endBatteryLevel = 544 hourlyBatteryLevelsPerDay.get(dailyIndex).getLevels().get(hourlyIndex + 1); 545 final long slotDuration = endTimestamp - startTimestamp; 546 List<Map<String, BatteryHistEntry>> slotBatteryHistoryList = new ArrayList<>(); 547 slotBatteryHistoryList.add( 548 batteryHistoryMap.getOrDefault(startTimestamp, EMPTY_BATTERY_MAP)); 549 for (Long timestamp = TimestampUtils.getNextHourTimestamp(startTimestamp); 550 timestamp < endTimestamp; timestamp += DateUtils.HOUR_IN_MILLIS) { 551 slotBatteryHistoryList.add( 552 batteryHistoryMap.getOrDefault(timestamp, EMPTY_BATTERY_MAP)); 553 } 554 slotBatteryHistoryList.add( 555 batteryHistoryMap.getOrDefault(endTimestamp, EMPTY_BATTERY_MAP)); 556 557 final BatteryDiffData hourlyBatteryDiffData = 558 insertHourlyUsageDiffDataPerSlot( 559 context, 560 startTimestamp, 561 endTimestamp, 562 startBatteryLevel, 563 endBatteryLevel, 564 currentUserId, 565 workProfileUserId, 566 slotDuration, 567 systemAppsPackageNames, 568 systemAppsUids, 569 appUsagePeriodMap == null 570 || appUsagePeriodMap.get(dailyIndex) == null 571 ? null 572 : appUsagePeriodMap.get(dailyIndex).get(hourlyIndex), 573 slotBatteryHistoryList); 574 batteryDiffDataMap.put(startTimestamp, hourlyBatteryDiffData); 575 } 576 } 577 return batteryDiffDataMap; 578 } 579 580 /** 581 * @return Returns the indexed battery usage data for each corresponding time slot. 582 * 583 * <p>There could be 2 cases of the returned value:</p> 584 * <ul> 585 * <li> null: empty or invalid data.</li> 586 * <li> 1 part: if batteryLevelData is null.</li> 587 * <p> [SELECTED_INDEX_ALL][SELECTED_INDEX_ALL]</p> 588 * <li> 3 parts: if batteryLevelData is not null.</li> 589 * <p> 1 - [SELECTED_INDEX_ALL][SELECTED_INDEX_ALL]</p> 590 * <p> 2 - [0][SELECTED_INDEX_ALL] ~ [maxDailyIndex][SELECTED_INDEX_ALL]</p> 591 * <p> 3 - [0][0] ~ [maxDailyIndex][maxHourlyIndex]</p> 592 * </ul> 593 */ generateBatteryUsageMap( final Context context, final Map<Long, BatteryDiffData> batteryDiffDataMap, final @Nullable BatteryLevelData batteryLevelData)594 static Map<Integer, Map<Integer, BatteryDiffData>> generateBatteryUsageMap( 595 final Context context, 596 final Map<Long, BatteryDiffData> batteryDiffDataMap, 597 final @Nullable BatteryLevelData batteryLevelData) { 598 final Map<Integer, Map<Integer, BatteryDiffData>> resultMap = new ArrayMap<>(); 599 if (batteryLevelData == null) { 600 Preconditions.checkArgument(batteryDiffDataMap.size() == 1); 601 BatteryDiffData batteryDiffData = batteryDiffDataMap.values().stream().toList().get(0); 602 final Map<Integer, BatteryDiffData> allUsageMap = new ArrayMap<>(); 603 allUsageMap.put(SELECTED_INDEX_ALL, batteryDiffData); 604 resultMap.put(SELECTED_INDEX_ALL, allUsageMap); 605 return resultMap; 606 } 607 List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay = 608 batteryLevelData.getHourlyBatteryLevelsPerDay(); 609 // Insert diff data from [0][0] to [maxDailyIndex][maxHourlyIndex]. 610 insertHourlyUsageDiffData(hourlyBatteryLevelsPerDay, batteryDiffDataMap, resultMap); 611 // Insert diff data from [0][SELECTED_INDEX_ALL] to [maxDailyIndex][SELECTED_INDEX_ALL]. 612 insertDailyUsageDiffData(context, hourlyBatteryLevelsPerDay, resultMap); 613 // Insert diff data [SELECTED_INDEX_ALL][SELECTED_INDEX_ALL]. 614 insertAllUsageDiffData(context, resultMap); 615 if (!isUsageMapValid(resultMap, hourlyBatteryLevelsPerDay)) { 616 return null; 617 } 618 return resultMap; 619 } 620 621 @VisibleForTesting 622 @Nullable generateBatteryDiffData( final Context context, final long startTimestamp, final List<BatteryHistEntry> batteryHistEntryList, final @NonNull Set<String> systemAppsPackageNames, final @NonNull Set<Integer> systemAppsUids)623 static BatteryDiffData generateBatteryDiffData( 624 final Context context, 625 final long startTimestamp, 626 final List<BatteryHistEntry> batteryHistEntryList, 627 final @NonNull Set<String> systemAppsPackageNames, 628 final @NonNull Set<Integer> systemAppsUids) { 629 if (batteryHistEntryList == null || batteryHistEntryList.isEmpty()) { 630 Log.w(TAG, "batteryHistEntryList is null or empty in generateBatteryDiffData()"); 631 return null; 632 } 633 final int currentUserId = context.getUserId(); 634 final UserHandle userHandle = 635 Utils.getManagedProfile(context.getSystemService(UserManager.class)); 636 final int workProfileUserId = 637 userHandle != null ? userHandle.getIdentifier() : Integer.MIN_VALUE; 638 final List<BatteryDiffEntry> appEntries = new ArrayList<>(); 639 final List<BatteryDiffEntry> systemEntries = new ArrayList<>(); 640 641 for (BatteryHistEntry entry : batteryHistEntryList) { 642 final boolean isFromOtherUsers = isConsumedFromOtherUsers( 643 currentUserId, workProfileUserId, entry); 644 // Not show other users' battery usage data. 645 if (isFromOtherUsers) { 646 continue; 647 } else { 648 final BatteryDiffEntry currentBatteryDiffEntry = new BatteryDiffEntry( 649 context, 650 entry.mUid, 651 entry.mUserId, 652 entry.getKey(), 653 entry.mIsHidden, 654 entry.mDrainType, 655 entry.mPackageName, 656 entry.mAppLabel, 657 entry.mConsumerType, 658 entry.mForegroundUsageTimeInMs, 659 entry.mBackgroundUsageTimeInMs, 660 /*screenOnTimeInMs=*/ 0, 661 entry.mConsumePower, 662 entry.mForegroundUsageConsumePower, 663 entry.mForegroundServiceUsageConsumePower, 664 entry.mBackgroundUsageConsumePower, 665 entry.mCachedUsageConsumePower); 666 if (currentBatteryDiffEntry.isSystemEntry()) { 667 systemEntries.add(currentBatteryDiffEntry); 668 } else { 669 appEntries.add(currentBatteryDiffEntry); 670 } 671 } 672 } 673 674 // If there is no data, return null instead of empty item. 675 if (appEntries.isEmpty() && systemEntries.isEmpty()) { 676 return null; 677 } 678 return new BatteryDiffData(context, startTimestamp, getCurrentTimeMillis(), 679 /* startBatteryLevel =*/ 100, getCurrentLevel(context), /* screenOnTime= */ 0L, 680 appEntries, systemEntries, systemAppsPackageNames, systemAppsUids, 681 /* isAccumulated= */ false); 682 } 683 684 /** 685 * <p>{@code Long} stands for the userId.</p> 686 * <p>{@code String} stands for the packageName.</p> 687 */ 688 @VisibleForTesting 689 @Nullable buildAppUsagePeriodList( Context context, final List<AppUsageEvent> appUsageEvents, final List<BatteryEvent> batteryEventList, final long startTime, final long endTime)690 static Map<Long, Map<String, List<AppUsagePeriod>>> buildAppUsagePeriodList( 691 Context context, final List<AppUsageEvent> appUsageEvents, 692 final List<BatteryEvent> batteryEventList, final long startTime, final long endTime) { 693 if (appUsageEvents.isEmpty()) { 694 return null; 695 } 696 697 // Attributes the list of AppUsagePeriod into device events and instance events for further 698 // use. 699 final List<AppUsageEvent> deviceEvents = new ArrayList<>(); 700 final ArrayMap<Integer, List<AppUsageEvent>> usageEventsByInstanceId = new ArrayMap<>(); 701 for (final AppUsageEvent event : appUsageEvents) { 702 final AppUsageEventType eventType = event.getType(); 703 if (eventType == AppUsageEventType.ACTIVITY_RESUMED 704 || eventType == AppUsageEventType.ACTIVITY_STOPPED) { 705 final int instanceId = event.getInstanceId(); 706 if (usageEventsByInstanceId.get(instanceId) == null) { 707 usageEventsByInstanceId.put(instanceId, new ArrayList<>()); 708 } 709 usageEventsByInstanceId.get(instanceId).add(event); 710 } else if (eventType == AppUsageEventType.DEVICE_SHUTDOWN) { 711 // Track device-wide events in their own list as they affect any app. 712 deviceEvents.add(event); 713 } 714 } 715 if (usageEventsByInstanceId.isEmpty()) { 716 return null; 717 } 718 719 final Map<Long, Map<String, List<AppUsagePeriod>>> allUsagePeriods = new ArrayMap<>(); 720 721 for (int i = 0; i < usageEventsByInstanceId.size(); i++) { 722 // The usage periods for an instance are determined by the usage events with its 723 // instance id and any device-wide events such as device shutdown. 724 final List<AppUsageEvent> usageEvents = usageEventsByInstanceId.valueAt(i); 725 if (usageEvents == null || usageEvents.isEmpty()) { 726 continue; 727 } 728 // The same instance must have same userId and packageName. 729 final AppUsageEvent firstEvent = usageEvents.get(0); 730 final long eventUserId = firstEvent.getUserId(); 731 final String packageName = getEffectivePackageName( 732 context, 733 sUsageStatsManager, 734 firstEvent.getPackageName(), 735 firstEvent.getTaskRootPackageName()); 736 usageEvents.addAll(deviceEvents); 737 // Sorts the usageEvents in ascending order based on the timestamp before computing the 738 // period. 739 Collections.sort(usageEvents, APP_USAGE_EVENT_TIMESTAMP_COMPARATOR); 740 741 // A package might have multiple instances. Computes the usage period per instance id 742 // and then merges them into the same user-package map. 743 final List<AppUsagePeriod> usagePeriodList = 744 excludePowerConnectedTimeFromAppUsagePeriodList( 745 buildAppUsagePeriodListPerInstance(usageEvents, startTime, endTime), 746 batteryEventList); 747 if (!usagePeriodList.isEmpty()) { 748 addToUsagePeriodMap(allUsagePeriods, usagePeriodList, eventUserId, packageName); 749 } 750 } 751 752 // Sorts all usage periods by start time. 753 for (final long userId : allUsagePeriods.keySet()) { 754 if (allUsagePeriods.get(userId) == null) { 755 continue; 756 } 757 for (final String packageName: allUsagePeriods.get(userId).keySet()) { 758 Collections.sort( 759 allUsagePeriods.get(userId).get(packageName), 760 Comparator.comparing(AppUsagePeriod::getStartTime)); 761 } 762 } 763 return allUsagePeriods.isEmpty() ? null : allUsagePeriods; 764 } 765 766 @VisibleForTesting buildAppUsagePeriodListPerInstance( final List<AppUsageEvent> usageEvents, final long startTime, final long endTime)767 static List<AppUsagePeriod> buildAppUsagePeriodListPerInstance( 768 final List<AppUsageEvent> usageEvents, final long startTime, final long endTime) { 769 final List<AppUsagePeriod> usagePeriodList = new ArrayList<>(); 770 AppUsagePeriod.Builder pendingUsagePeriod = AppUsagePeriod.newBuilder(); 771 772 for (final AppUsageEvent event : usageEvents) { 773 final long eventTime = event.getTimestamp(); 774 775 if (event.getType() == AppUsageEventType.ACTIVITY_RESUMED) { 776 // If there is an existing start time, simply ignore this start event. 777 // If there was no start time, then start a new period. 778 if (!pendingUsagePeriod.hasStartTime()) { 779 pendingUsagePeriod.setStartTime(eventTime); 780 } 781 } else if (event.getType() == AppUsageEventType.ACTIVITY_STOPPED) { 782 pendingUsagePeriod.setEndTime(eventTime); 783 if (!pendingUsagePeriod.hasStartTime()) { 784 pendingUsagePeriod.setStartTime( 785 getStartTimeForIncompleteUsagePeriod(pendingUsagePeriod)); 786 } 787 // If we already have start time, add it directly. 788 validateAndAddToPeriodList( 789 usagePeriodList, pendingUsagePeriod.build(), startTime, endTime); 790 pendingUsagePeriod.clear(); 791 } else if (event.getType() == AppUsageEventType.DEVICE_SHUTDOWN) { 792 // The end event might be lost when device is shutdown. Use the estimated end 793 // time for the period. 794 if (pendingUsagePeriod.hasStartTime()) { 795 pendingUsagePeriod.setEndTime( 796 getEndTimeForIncompleteUsagePeriod(pendingUsagePeriod, eventTime)); 797 validateAndAddToPeriodList( 798 usagePeriodList, pendingUsagePeriod.build(), startTime, endTime); 799 pendingUsagePeriod.clear(); 800 } 801 } 802 } 803 // If there exists unclosed period, the stop event might happen in the next time 804 // slot. Use the endTime for the period. 805 if (pendingUsagePeriod.hasStartTime() && pendingUsagePeriod.getStartTime() < endTime) { 806 pendingUsagePeriod.setEndTime(endTime); 807 validateAndAddToPeriodList( 808 usagePeriodList, pendingUsagePeriod.build(), startTime, endTime); 809 pendingUsagePeriod.clear(); 810 } 811 return usagePeriodList; 812 } 813 814 @VisibleForTesting excludePowerConnectedTimeFromAppUsagePeriodList( final List<AppUsagePeriod> usagePeriodList, final List<BatteryEvent> batteryEventList)815 static List<AppUsagePeriod> excludePowerConnectedTimeFromAppUsagePeriodList( 816 final List<AppUsagePeriod> usagePeriodList, 817 final List<BatteryEvent> batteryEventList) { 818 final List<AppUsagePeriod> resultList = new ArrayList<>(); 819 int index = 0; 820 for (AppUsagePeriod inputPeriod : usagePeriodList) { 821 long lastStartTime = inputPeriod.getStartTime(); 822 while (index < batteryEventList.size()) { 823 BatteryEvent batteryEvent = batteryEventList.get(index); 824 if (batteryEvent.getTimestamp() < inputPeriod.getStartTime()) { 825 // Because the batteryEventList has been sorted, here is to mark the power 826 // connection state when the usage period starts. If power is connected when 827 // the usage period starts, the starting period will be ignored; otherwise it 828 // will be added. 829 if (batteryEvent.getType() == BatteryEventType.POWER_CONNECTED) { 830 lastStartTime = 0; 831 } else if (batteryEvent.getType() == BatteryEventType.POWER_DISCONNECTED) { 832 lastStartTime = inputPeriod.getStartTime(); 833 } 834 index++; 835 continue; 836 } 837 if (batteryEvent.getTimestamp() > inputPeriod.getEndTime()) { 838 // Because the batteryEventList has been sorted, if any event is already after 839 // the end time, all the following events should be able to drop directly. 840 break; 841 } 842 843 if (batteryEvent.getType() == BatteryEventType.POWER_CONNECTED 844 && lastStartTime != 0) { 845 resultList.add(AppUsagePeriod.newBuilder() 846 .setStartTime(lastStartTime) 847 .setEndTime(batteryEvent.getTimestamp()) 848 .build()); 849 lastStartTime = 0; 850 } else if (batteryEvent.getType() == BatteryEventType.POWER_DISCONNECTED) { 851 lastStartTime = batteryEvent.getTimestamp(); 852 } 853 index++; 854 } 855 if (lastStartTime != 0) { 856 resultList.add(AppUsagePeriod.newBuilder() 857 .setStartTime(lastStartTime) 858 .setEndTime(inputPeriod.getEndTime()) 859 .build()); 860 } 861 } 862 return resultList; 863 } 864 865 @VisibleForTesting getScreenOnTime( final Map<Long, Map<String, List<AppUsagePeriod>>> appUsageMap, final long userId, final String packageName)866 static long getScreenOnTime( 867 final Map<Long, Map<String, List<AppUsagePeriod>>> appUsageMap, 868 final long userId, 869 final String packageName) { 870 if (appUsageMap == null || appUsageMap.get(userId) == null) { 871 return 0; 872 } 873 874 return getScreenOnTime(appUsageMap.get(userId).get(packageName)); 875 } 876 getBatteryDiffDataMapFromStatsService( final Context context, final long startTimestamp, @NonNull final Set<String> systemAppsPackageNames, @NonNull final Set<Integer> systemAppsUids)877 static Map<Long, BatteryDiffData> getBatteryDiffDataMapFromStatsService( 878 final Context context, final long startTimestamp, 879 @NonNull final Set<String> systemAppsPackageNames, 880 @NonNull final Set<Integer> systemAppsUids) { 881 Map<Long, BatteryDiffData> batteryDiffDataMap = new ArrayMap<>(1); 882 batteryDiffDataMap.put(startTimestamp, generateBatteryDiffData( 883 context, startTimestamp, getBatteryHistListFromFromStatsService(context), 884 systemAppsPackageNames, systemAppsUids)); 885 return batteryDiffDataMap; 886 } 887 loadLabelAndIcon( @ullable final Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageMap)888 static void loadLabelAndIcon( 889 @Nullable final Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageMap) { 890 if (batteryUsageMap == null) { 891 return; 892 } 893 // Pre-loads each BatteryDiffEntry relative icon and label for all slots. 894 final BatteryDiffData batteryUsageMapForAll = 895 batteryUsageMap.get(SELECTED_INDEX_ALL).get(SELECTED_INDEX_ALL); 896 if (batteryUsageMapForAll != null) { 897 batteryUsageMapForAll.getAppDiffEntryList().forEach( 898 entry -> entry.loadLabelAndIcon()); 899 batteryUsageMapForAll.getSystemDiffEntryList().forEach( 900 entry -> entry.loadLabelAndIcon()); 901 } 902 } 903 getSystemAppsPackageNames(Context context)904 static Set<String> getSystemAppsPackageNames(Context context) { 905 return sTestSystemAppsPackageNames != null ? sTestSystemAppsPackageNames 906 : AppListRepositoryUtil.getSystemPackageNames(context, context.getUserId()); 907 } 908 getSystemAppsUids(Context context)909 static Set<Integer> getSystemAppsUids(Context context) { 910 Set<Integer> result = new ArraySet<>(1); 911 try { 912 result.add(context.getPackageManager().getUidForSharedUser( 913 ANDROID_CORE_APPS_SHARED_USER_ID)); 914 } catch (PackageManager.NameNotFoundException e) { 915 // No Android Core Apps 916 } 917 return result; 918 } 919 920 /** 921 * Generates the list of {@link AppUsageEvent} within the specific time range. 922 * The buffer is added to make sure the app usage calculation near the boundaries is correct. 923 * 924 * Note: The appUsageEventList should have been sorted when calling this function. 925 */ getAppUsageEventListWithinTimeRangeWithBuffer( final List<AppUsageEvent> appUsageEventList, final long startTime, final long endTime)926 private static List<AppUsageEvent> getAppUsageEventListWithinTimeRangeWithBuffer( 927 final List<AppUsageEvent> appUsageEventList, final long startTime, final long endTime) { 928 final long start = startTime - DatabaseUtils.USAGE_QUERY_BUFFER_HOURS; 929 final long end = endTime + DatabaseUtils.USAGE_QUERY_BUFFER_HOURS; 930 final List<AppUsageEvent> resultList = new ArrayList<>(); 931 for (final AppUsageEvent event : appUsageEventList) { 932 final long eventTime = event.getTimestamp(); 933 // Because the appUsageEventList has been sorted, if any event is already after the end 934 // time, all the following events should be able to drop directly. 935 if (eventTime > end) { 936 break; 937 } 938 // If the event timestamp is in [start, end], add it into the result list. 939 if (eventTime >= start) { 940 resultList.add(event); 941 } 942 } 943 return resultList; 944 } 945 validateAndAddToPeriodList( final List<AppUsagePeriod> appUsagePeriodList, final AppUsagePeriod appUsagePeriod, final long startTime, final long endTime)946 private static void validateAndAddToPeriodList( 947 final List<AppUsagePeriod> appUsagePeriodList, 948 final AppUsagePeriod appUsagePeriod, 949 final long startTime, 950 final long endTime) { 951 final long periodStartTime = 952 trimPeriodTime(appUsagePeriod.getStartTime(), startTime, endTime); 953 final long periodEndTime = trimPeriodTime(appUsagePeriod.getEndTime(), startTime, endTime); 954 // Only when the period is valid, add it into the list. 955 if (periodStartTime < periodEndTime) { 956 final AppUsagePeriod period = 957 AppUsagePeriod.newBuilder() 958 .setStartTime(periodStartTime) 959 .setEndTime(periodEndTime) 960 .build(); 961 appUsagePeriodList.add(period); 962 } 963 } 964 trimPeriodTime( final long originalTime, final long startTime, final long endTime)965 private static long trimPeriodTime( 966 final long originalTime, final long startTime, final long endTime) { 967 long finalTime = Math.max(originalTime, startTime); 968 finalTime = Math.min(finalTime, endTime); 969 return finalTime; 970 } 971 addToUsagePeriodMap( final Map<Long, Map<String, List<AppUsagePeriod>>> usagePeriodMap, final List<AppUsagePeriod> usagePeriodList, final long userId, final String packageName)972 private static void addToUsagePeriodMap( 973 final Map<Long, Map<String, List<AppUsagePeriod>>> usagePeriodMap, 974 final List<AppUsagePeriod> usagePeriodList, 975 final long userId, 976 final String packageName) { 977 usagePeriodMap.computeIfAbsent(userId, key -> new ArrayMap<>()); 978 final Map<String, List<AppUsagePeriod>> packageNameMap = usagePeriodMap.get(userId); 979 packageNameMap.computeIfAbsent(packageName, key -> new ArrayList<>()); 980 packageNameMap.get(packageName).addAll(usagePeriodList); 981 } 982 983 /** 984 * Returns the start time that gives {@code usagePeriod} the default usage duration. 985 */ getStartTimeForIncompleteUsagePeriod( final AppUsagePeriodOrBuilder usagePeriod)986 private static long getStartTimeForIncompleteUsagePeriod( 987 final AppUsagePeriodOrBuilder usagePeriod) { 988 return usagePeriod.getEndTime() - DEFAULT_USAGE_DURATION_FOR_INCOMPLETE_INTERVAL; 989 } 990 991 /** 992 * Returns the end time that gives {@code usagePeriod} the default usage duration. 993 */ getEndTimeForIncompleteUsagePeriod( final AppUsagePeriodOrBuilder usagePeriod, final long eventTime)994 private static long getEndTimeForIncompleteUsagePeriod( 995 final AppUsagePeriodOrBuilder usagePeriod, final long eventTime) { 996 return Math.min( 997 usagePeriod.getStartTime() + DEFAULT_USAGE_DURATION_FOR_INCOMPLETE_INTERVAL, 998 eventTime); 999 } 1000 1001 @Nullable getAppUsageEventsForUser( Context context, final UserManager userManager, final int userID, final long earliestTimestamp)1002 private static UsageEvents getAppUsageEventsForUser( 1003 Context context, final UserManager userManager, final int userID, 1004 final long earliestTimestamp) { 1005 final String callingPackage = context.getPackageName(); 1006 final long now = System.currentTimeMillis(); 1007 // When the user is not unlocked, UsageStatsManager will return null, so bypass the 1008 // following data loading logics directly. 1009 if (!userManager.isUserUnlocked(userID)) { 1010 Log.w(TAG, "fail to load app usage event for user :" + userID + " because locked"); 1011 return null; 1012 } 1013 final long startTime = DatabaseUtils.getAppUsageStartTimestampOfUser( 1014 context, userID, earliestTimestamp); 1015 return loadAppUsageEventsForUserFromService( 1016 sUsageStatsManager, startTime, now, userID, callingPackage); 1017 } 1018 1019 @Nullable loadAppUsageEventsForUserFromService( final IUsageStatsManager usageStatsManager, final long startTime, final long endTime, final int userId, final String callingPackage)1020 private static UsageEvents loadAppUsageEventsForUserFromService( 1021 final IUsageStatsManager usageStatsManager, final long startTime, final long endTime, 1022 final int userId, final String callingPackage) { 1023 final long start = System.currentTimeMillis(); 1024 UsageEvents events = null; 1025 try { 1026 events = usageStatsManager.queryEventsForUser( 1027 startTime, endTime, userId, callingPackage); 1028 } catch (RemoteException e) { 1029 Log.e(TAG, "Error fetching usage events: ", e); 1030 } 1031 final long elapsedTime = System.currentTimeMillis() - start; 1032 Log.d(TAG, String.format("getAppUsageEventsForUser(): %d from %d to %d in %d/ms", userId, 1033 startTime, endTime, elapsedTime)); 1034 return events; 1035 } 1036 1037 @Nullable getBatteryHistListFromFromStatsService( final Context context)1038 private static List<BatteryHistEntry> getBatteryHistListFromFromStatsService( 1039 final Context context) { 1040 List<BatteryHistEntry> batteryHistEntryList = null; 1041 try { 1042 final BatteryUsageStats batteryUsageStats = getBatteryUsageStats(context); 1043 final List<BatteryEntry> batteryEntryList = 1044 generateBatteryEntryListFromBatteryUsageStats(context, batteryUsageStats); 1045 batteryHistEntryList = convertToBatteryHistEntry(batteryEntryList, batteryUsageStats); 1046 closeBatteryUsageStats(batteryUsageStats); 1047 } catch (RuntimeException e) { 1048 Log.e(TAG, "load batteryUsageStats:", e); 1049 } 1050 1051 return batteryHistEntryList; 1052 } 1053 1054 @VisibleForTesting 1055 @Nullable convertToBatteryHistEntry( @ullable final List<BatteryEntry> batteryEntryList, final BatteryUsageStats batteryUsageStats)1056 static List<BatteryHistEntry> convertToBatteryHistEntry( 1057 @Nullable final List<BatteryEntry> batteryEntryList, 1058 final BatteryUsageStats batteryUsageStats) { 1059 if (batteryEntryList == null || batteryEntryList.isEmpty()) { 1060 Log.w(TAG, "batteryEntryList is null or empty in convertToBatteryHistEntry()"); 1061 return null; 1062 } 1063 return batteryEntryList.stream() 1064 .filter(entry -> { 1065 final long foregroundMs = entry.getTimeInForegroundMs(); 1066 final long backgroundMs = entry.getTimeInBackgroundMs(); 1067 return entry.getConsumedPower() > 0 1068 || (entry.getConsumedPower() == 0 1069 && (foregroundMs != 0 || backgroundMs != 0)); 1070 }) 1071 .map(entry -> ConvertUtils.convertToBatteryHistEntry(entry, batteryUsageStats)) 1072 .collect(Collectors.toList()); 1073 } 1074 1075 /** 1076 * Interpolates history map based on expected timestamp slots and processes the corner case when 1077 * the expected start timestamp is earlier than what we have. 1078 */ interpolateHistory( Context context, final List<Long> rawTimestampList, final List<Long> expectedTimestampSlots, final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap, final Map<Long, Map<String, BatteryHistEntry>> resultMap)1079 private static void interpolateHistory( 1080 Context context, 1081 final List<Long> rawTimestampList, 1082 final List<Long> expectedTimestampSlots, 1083 final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap, 1084 final Map<Long, Map<String, BatteryHistEntry>> resultMap) { 1085 if (rawTimestampList.isEmpty() || expectedTimestampSlots.isEmpty()) { 1086 return; 1087 } 1088 final int expectedTimestampSlotsSize = expectedTimestampSlots.size(); 1089 final long startTimestamp = expectedTimestampSlots.get(0); 1090 final long endTimestamp = expectedTimestampSlots.get(expectedTimestampSlotsSize - 1); 1091 1092 resultMap.put(startTimestamp, batteryHistoryMap.get(startTimestamp)); 1093 for (int index = 1; index < expectedTimestampSlotsSize - 1; index++) { 1094 interpolateHistoryForSlot(context, expectedTimestampSlots.get(index), rawTimestampList, 1095 batteryHistoryMap, resultMap); 1096 } 1097 resultMap.put(endTimestamp, 1098 Map.of(CURRENT_TIME_BATTERY_HISTORY_PLACEHOLDER, EMPTY_BATTERY_HIST_ENTRY)); 1099 } 1100 interpolateHistoryForSlot( Context context, final long currentSlot, final List<Long> rawTimestampList, final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap, final Map<Long, Map<String, BatteryHistEntry>> resultMap)1101 private static void interpolateHistoryForSlot( 1102 Context context, 1103 final long currentSlot, 1104 final List<Long> rawTimestampList, 1105 final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap, 1106 final Map<Long, Map<String, BatteryHistEntry>> resultMap) { 1107 final long[] nearestTimestamps = findNearestTimestamp(rawTimestampList, currentSlot); 1108 final long lowerTimestamp = nearestTimestamps[0]; 1109 final long upperTimestamp = nearestTimestamps[1]; 1110 // Case 1: upper timestamp is zero since scheduler is delayed! 1111 if (upperTimestamp == 0) { 1112 log(context, "job scheduler is delayed", currentSlot, null); 1113 resultMap.put(currentSlot, new ArrayMap<>()); 1114 return; 1115 } 1116 // Case 2: upper timestamp is closed to the current timestamp. 1117 if ((upperTimestamp - currentSlot) 1118 < MAX_DIFF_SECONDS_OF_UPPER_TIMESTAMP * DateUtils.SECOND_IN_MILLIS) { 1119 log(context, "force align into the nearest slot", currentSlot, null); 1120 resultMap.put(currentSlot, batteryHistoryMap.get(upperTimestamp)); 1121 return; 1122 } 1123 // Case 3: lower timestamp is zero before starting to collect data. 1124 if (lowerTimestamp == 0) { 1125 log(context, "no lower timestamp slot data", currentSlot, null); 1126 resultMap.put(currentSlot, new ArrayMap<>()); 1127 return; 1128 } 1129 interpolateHistoryForSlot( 1130 context, currentSlot, lowerTimestamp, upperTimestamp, batteryHistoryMap, resultMap); 1131 } 1132 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)1133 private static void interpolateHistoryForSlot( 1134 Context context, 1135 final long currentSlot, 1136 final long lowerTimestamp, 1137 final long upperTimestamp, 1138 final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap, 1139 final Map<Long, Map<String, BatteryHistEntry>> resultMap) { 1140 final Map<String, BatteryHistEntry> lowerEntryDataMap = 1141 batteryHistoryMap.get(lowerTimestamp); 1142 final Map<String, BatteryHistEntry> upperEntryDataMap = 1143 batteryHistoryMap.get(upperTimestamp); 1144 // Verifies whether the lower data is valid to use or not by checking boot time. 1145 final BatteryHistEntry upperEntryDataFirstEntry = 1146 upperEntryDataMap.values().stream().findFirst().get(); 1147 final long upperEntryDataBootTimestamp = 1148 upperEntryDataFirstEntry.mTimestamp - upperEntryDataFirstEntry.mBootTimestamp; 1149 // Lower data is captured before upper data corresponding device is booting. 1150 // Skips the booting-specific logics and always does interpolation for daily chart level 1151 // data. 1152 if (lowerTimestamp < upperEntryDataBootTimestamp 1153 && !TimestampUtils.isMidnight(currentSlot)) { 1154 // Provides an opportunity to force align the slot directly. 1155 if ((upperTimestamp - currentSlot) < 10 * DateUtils.MINUTE_IN_MILLIS) { 1156 log(context, "force align into the nearest slot", currentSlot, null); 1157 resultMap.put(currentSlot, upperEntryDataMap); 1158 } else { 1159 log(context, "in the different booting section", currentSlot, null); 1160 resultMap.put(currentSlot, new ArrayMap<>()); 1161 } 1162 return; 1163 } 1164 log(context, "apply interpolation arithmetic", currentSlot, null); 1165 final Map<String, BatteryHistEntry> newHistEntryMap = new ArrayMap<>(); 1166 final double timestampLength = upperTimestamp - lowerTimestamp; 1167 final double timestampDiff = currentSlot - lowerTimestamp; 1168 // Applies interpolation arithmetic for each BatteryHistEntry. 1169 for (String entryKey : upperEntryDataMap.keySet()) { 1170 final BatteryHistEntry lowerEntry = lowerEntryDataMap.get(entryKey); 1171 final BatteryHistEntry upperEntry = upperEntryDataMap.get(entryKey); 1172 // Checks whether there is any abnormal battery reset conditions. 1173 if (lowerEntry != null) { 1174 final boolean invalidForegroundUsageTime = 1175 lowerEntry.mForegroundUsageTimeInMs > upperEntry.mForegroundUsageTimeInMs; 1176 final boolean invalidBackgroundUsageTime = 1177 lowerEntry.mBackgroundUsageTimeInMs > upperEntry.mBackgroundUsageTimeInMs; 1178 if (invalidForegroundUsageTime || invalidBackgroundUsageTime) { 1179 newHistEntryMap.put(entryKey, upperEntry); 1180 log(context, "abnormal reset condition is found", currentSlot, upperEntry); 1181 continue; 1182 } 1183 } 1184 final BatteryHistEntry newEntry = 1185 BatteryHistEntry.interpolate( 1186 currentSlot, 1187 upperTimestamp, 1188 /*ratio=*/ timestampDiff / timestampLength, 1189 lowerEntry, 1190 upperEntry); 1191 newHistEntryMap.put(entryKey, newEntry); 1192 if (lowerEntry == null) { 1193 log(context, "cannot find lower entry data", currentSlot, upperEntry); 1194 continue; 1195 } 1196 } 1197 resultMap.put(currentSlot, newHistEntryMap); 1198 } 1199 getLevel( Context context, final Map<Long, Map<String, BatteryHistEntry>> processedBatteryHistoryMap, final long timestamp)1200 private static Integer getLevel( 1201 Context context, 1202 final Map<Long, Map<String, BatteryHistEntry>> processedBatteryHistoryMap, 1203 final long timestamp) { 1204 final Map<String, BatteryHistEntry> entryMap = processedBatteryHistoryMap.get(timestamp); 1205 if (entryMap == null || entryMap.isEmpty()) { 1206 Log.e(TAG, "abnormal entry list in the timestamp:" 1207 + ConvertUtils.utcToLocalTimeForLogging(timestamp)); 1208 return BATTERY_LEVEL_UNKNOWN; 1209 } 1210 // The current time battery history hasn't been loaded yet, returns the current battery 1211 // level. 1212 if (entryMap.containsKey(CURRENT_TIME_BATTERY_HISTORY_PLACEHOLDER)) { 1213 return getCurrentLevel(context); 1214 } 1215 // Averages the battery level in each time slot to avoid corner conditions. 1216 float batteryLevelCounter = 0; 1217 for (BatteryHistEntry entry : entryMap.values()) { 1218 batteryLevelCounter += entry.mBatteryLevel; 1219 } 1220 return Math.round(batteryLevelCounter / entryMap.size()); 1221 } 1222 getCurrentLevel(Context context)1223 private static int getCurrentLevel(Context context) { 1224 final Intent intent = BatteryUtils.getBatteryIntent(context); 1225 return BatteryStatus.getBatteryLevel(intent); 1226 } 1227 insertHourlyUsageDiffData( final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay, final Map<Long, BatteryDiffData> batteryDiffDataMap, final Map<Integer, Map<Integer, BatteryDiffData>> resultMap)1228 private static void insertHourlyUsageDiffData( 1229 final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay, 1230 final Map<Long, BatteryDiffData> batteryDiffDataMap, 1231 final Map<Integer, Map<Integer, BatteryDiffData>> resultMap) { 1232 // Each time slot usage diff data = 1233 // sum(Math.abs(timestamp[i+1] data - timestamp[i] data)); 1234 // since we want to aggregate every hour usage diff data into a single time slot. 1235 for (int dailyIndex = 0; dailyIndex < hourlyBatteryLevelsPerDay.size(); dailyIndex++) { 1236 final Map<Integer, BatteryDiffData> dailyDiffMap = new ArrayMap<>(); 1237 resultMap.put(dailyIndex, dailyDiffMap); 1238 if (hourlyBatteryLevelsPerDay.get(dailyIndex) == null) { 1239 continue; 1240 } 1241 final List<Long> hourlyTimestamps = 1242 hourlyBatteryLevelsPerDay.get(dailyIndex).getTimestamps(); 1243 for (int hourlyIndex = 0; hourlyIndex < hourlyTimestamps.size() - 1; hourlyIndex++) { 1244 final Long startTimestamp = hourlyTimestamps.get(hourlyIndex); 1245 dailyDiffMap.put(hourlyIndex, batteryDiffDataMap.get(startTimestamp)); 1246 } 1247 } 1248 } 1249 insertDailyUsageDiffData( final Context context, final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay, final Map<Integer, Map<Integer, BatteryDiffData>> resultMap)1250 private static void insertDailyUsageDiffData( 1251 final Context context, 1252 final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay, 1253 final Map<Integer, Map<Integer, BatteryDiffData>> resultMap) { 1254 for (int index = 0; index < hourlyBatteryLevelsPerDay.size(); index++) { 1255 Map<Integer, BatteryDiffData> dailyUsageMap = resultMap.get(index); 1256 if (dailyUsageMap == null) { 1257 dailyUsageMap = new ArrayMap<>(); 1258 resultMap.put(index, dailyUsageMap); 1259 } 1260 dailyUsageMap.put( 1261 SELECTED_INDEX_ALL, 1262 getAccumulatedUsageDiffData(context, dailyUsageMap.values())); 1263 } 1264 } 1265 insertAllUsageDiffData( final Context context, final Map<Integer, Map<Integer, BatteryDiffData>> resultMap)1266 private static void insertAllUsageDiffData( 1267 final Context context, 1268 final Map<Integer, Map<Integer, BatteryDiffData>> resultMap) { 1269 final List<BatteryDiffData> diffDataList = new ArrayList<>(); 1270 resultMap.keySet().forEach( 1271 key -> diffDataList.add(resultMap.get(key).get(SELECTED_INDEX_ALL))); 1272 final Map<Integer, BatteryDiffData> allUsageMap = new ArrayMap<>(); 1273 allUsageMap.put(SELECTED_INDEX_ALL, getAccumulatedUsageDiffData(context, diffDataList)); 1274 resultMap.put(SELECTED_INDEX_ALL, allUsageMap); 1275 } 1276 1277 @Nullable insertHourlyUsageDiffDataPerSlot( final Context context, final long startTimestamp, final long endTimestamp, final int startBatteryLevel, final int endBatteryLevel, final int currentUserId, final int workProfileUserId, final long slotDuration, final Set<String> systemAppsPackageNames, final Set<Integer> systemAppsUids, final Map<Long, Map<String, List<AppUsagePeriod>>> appUsageMap, final List<Map<String, BatteryHistEntry>> slotBatteryHistoryList)1278 private static BatteryDiffData insertHourlyUsageDiffDataPerSlot( 1279 final Context context, 1280 final long startTimestamp, 1281 final long endTimestamp, 1282 final int startBatteryLevel, 1283 final int endBatteryLevel, 1284 final int currentUserId, 1285 final int workProfileUserId, 1286 final long slotDuration, 1287 final Set<String> systemAppsPackageNames, 1288 final Set<Integer> systemAppsUids, 1289 final Map<Long, Map<String, List<AppUsagePeriod>>> appUsageMap, 1290 final List<Map<String, BatteryHistEntry>> slotBatteryHistoryList) { 1291 long slotScreenOnTime = 0L; 1292 if (appUsageMap != null) { 1293 final List<AppUsagePeriod> flatAppUsagePeriodList = new ArrayList<>(); 1294 for (final long userId : appUsageMap.keySet()) { 1295 if ((userId != currentUserId && userId != workProfileUserId) 1296 || appUsageMap.get(userId) == null) { 1297 continue; 1298 } 1299 for (final String packageName : appUsageMap.get(userId).keySet()) { 1300 final List<AppUsagePeriod> appUsagePeriodList = 1301 appUsageMap.get(userId).get(packageName); 1302 if (appUsagePeriodList != null) { 1303 flatAppUsagePeriodList.addAll(appUsagePeriodList); 1304 } 1305 } 1306 } 1307 slotScreenOnTime = 1308 Math.min(slotDuration, getScreenOnTime(flatAppUsagePeriodList)); 1309 } 1310 1311 final List<BatteryDiffEntry> appEntries = new ArrayList<>(); 1312 final List<BatteryDiffEntry> systemEntries = new ArrayList<>(); 1313 1314 // Collects all keys in these three time slot records as all populations. 1315 final Set<String> allBatteryHistEntryKeys = new ArraySet<>(); 1316 for (Map<String, BatteryHistEntry> slotBatteryHistMap : slotBatteryHistoryList) { 1317 if (slotBatteryHistMap.isEmpty()) { 1318 // We should not get the empty list since we have at least one fake data to record 1319 // the battery level and status in each time slot, the empty list is used to 1320 // represent there is no enough data to apply interpolation arithmetic. 1321 return null; 1322 } 1323 allBatteryHistEntryKeys.addAll(slotBatteryHistMap.keySet()); 1324 } 1325 1326 // Calculates all packages diff usage data in a specific time slot. 1327 for (String key : allBatteryHistEntryKeys) { 1328 if (key == null) { 1329 continue; 1330 } 1331 1332 BatteryHistEntry selectedBatteryEntry = null; 1333 final List<BatteryHistEntry> batteryHistEntries = new ArrayList<>(); 1334 for (Map<String, BatteryHistEntry> slotBatteryHistMap : slotBatteryHistoryList) { 1335 BatteryHistEntry entry = 1336 slotBatteryHistMap.getOrDefault(key, EMPTY_BATTERY_HIST_ENTRY); 1337 batteryHistEntries.add(entry); 1338 if (selectedBatteryEntry == null && entry != EMPTY_BATTERY_HIST_ENTRY) { 1339 selectedBatteryEntry = entry; 1340 } 1341 } 1342 if (selectedBatteryEntry == null) { 1343 continue; 1344 } 1345 1346 // Not show other users' battery usage data. 1347 final boolean isFromOtherUsers = isConsumedFromOtherUsers( 1348 currentUserId, workProfileUserId, selectedBatteryEntry); 1349 if (isFromOtherUsers) { 1350 continue; 1351 } 1352 1353 // Cumulative values is a specific time slot for a specific app. 1354 long foregroundUsageTimeInMs = 0; 1355 long backgroundUsageTimeInMs = 0; 1356 double consumePower = 0; 1357 double foregroundUsageConsumePower = 0; 1358 double foregroundServiceUsageConsumePower = 0; 1359 double backgroundUsageConsumePower = 0; 1360 double cachedUsageConsumePower = 0; 1361 for (int i = 0; i < batteryHistEntries.size() - 1; i++) { 1362 final BatteryHistEntry currentEntry = batteryHistEntries.get(i); 1363 final BatteryHistEntry nextEntry = batteryHistEntries.get(i + 1); 1364 foregroundUsageTimeInMs += 1365 getDiffValue( 1366 currentEntry.mForegroundUsageTimeInMs, 1367 nextEntry.mForegroundUsageTimeInMs); 1368 backgroundUsageTimeInMs += 1369 getDiffValue( 1370 currentEntry.mBackgroundUsageTimeInMs, 1371 nextEntry.mBackgroundUsageTimeInMs); 1372 consumePower += 1373 getDiffValue( 1374 currentEntry.mConsumePower, 1375 nextEntry.mConsumePower); 1376 foregroundUsageConsumePower += 1377 getDiffValue( 1378 currentEntry.mForegroundUsageConsumePower, 1379 nextEntry.mForegroundUsageConsumePower); 1380 foregroundServiceUsageConsumePower += 1381 getDiffValue( 1382 currentEntry.mForegroundServiceUsageConsumePower, 1383 nextEntry.mForegroundServiceUsageConsumePower); 1384 backgroundUsageConsumePower += 1385 getDiffValue( 1386 currentEntry.mBackgroundUsageConsumePower, 1387 nextEntry.mBackgroundUsageConsumePower); 1388 cachedUsageConsumePower += 1389 getDiffValue( 1390 currentEntry.mCachedUsageConsumePower, 1391 nextEntry.mCachedUsageConsumePower); 1392 } 1393 if (isSystemConsumer(selectedBatteryEntry.mConsumerType) 1394 && selectedBatteryEntry.mDrainType == BatteryConsumer.POWER_COMPONENT_SCREEN) { 1395 // Replace Screen system component time with screen on time. 1396 foregroundUsageTimeInMs = slotScreenOnTime; 1397 } 1398 // Excludes entry since we don't have enough data to calculate. 1399 if (foregroundUsageTimeInMs == 0 1400 && backgroundUsageTimeInMs == 0 1401 && consumePower == 0) { 1402 continue; 1403 } 1404 // Forces refine the cumulative value since it may introduce deviation error since we 1405 // will apply the interpolation arithmetic. 1406 final float totalUsageTimeInMs = 1407 foregroundUsageTimeInMs + backgroundUsageTimeInMs; 1408 if (totalUsageTimeInMs > slotDuration) { 1409 final float ratio = slotDuration / totalUsageTimeInMs; 1410 if (sDebug) { 1411 Log.w(TAG, String.format("abnormal usage time %d|%d for:\n%s", 1412 Duration.ofMillis(foregroundUsageTimeInMs).getSeconds(), 1413 Duration.ofMillis(backgroundUsageTimeInMs).getSeconds(), 1414 selectedBatteryEntry)); 1415 } 1416 foregroundUsageTimeInMs = 1417 Math.round(foregroundUsageTimeInMs * ratio); 1418 backgroundUsageTimeInMs = 1419 Math.round(backgroundUsageTimeInMs * ratio); 1420 consumePower = consumePower * ratio; 1421 foregroundUsageConsumePower = foregroundUsageConsumePower * ratio; 1422 foregroundServiceUsageConsumePower = foregroundServiceUsageConsumePower * ratio; 1423 backgroundUsageConsumePower = backgroundUsageConsumePower * ratio; 1424 cachedUsageConsumePower = cachedUsageConsumePower * ratio; 1425 } 1426 1427 // Compute the screen on time and make sure it won't exceed the threshold. 1428 final long screenOnTime = Math.min( 1429 (long) slotDuration, 1430 getScreenOnTime( 1431 appUsageMap, 1432 selectedBatteryEntry.mUserId, 1433 selectedBatteryEntry.mPackageName)); 1434 // Make sure the background + screen-on time will not exceed the threshold. 1435 backgroundUsageTimeInMs = Math.min( 1436 backgroundUsageTimeInMs, (long) slotDuration - screenOnTime); 1437 final BatteryDiffEntry currentBatteryDiffEntry = new BatteryDiffEntry( 1438 context, 1439 selectedBatteryEntry.mUid, 1440 selectedBatteryEntry.mUserId, 1441 selectedBatteryEntry.getKey(), 1442 selectedBatteryEntry.mIsHidden, 1443 selectedBatteryEntry.mDrainType, 1444 selectedBatteryEntry.mPackageName, 1445 selectedBatteryEntry.mAppLabel, 1446 selectedBatteryEntry.mConsumerType, 1447 foregroundUsageTimeInMs, 1448 backgroundUsageTimeInMs, 1449 screenOnTime, 1450 consumePower, 1451 foregroundUsageConsumePower, 1452 foregroundServiceUsageConsumePower, 1453 backgroundUsageConsumePower, 1454 cachedUsageConsumePower); 1455 if (currentBatteryDiffEntry.isSystemEntry()) { 1456 systemEntries.add(currentBatteryDiffEntry); 1457 } else { 1458 appEntries.add(currentBatteryDiffEntry); 1459 } 1460 } 1461 1462 // If there is no data, return null instead of empty item. 1463 if (appEntries.isEmpty() && systemEntries.isEmpty()) { 1464 return null; 1465 } 1466 1467 return new BatteryDiffData(context, startTimestamp, endTimestamp, startBatteryLevel, 1468 endBatteryLevel, slotScreenOnTime, appEntries, systemEntries, 1469 systemAppsPackageNames, systemAppsUids, /* isAccumulated= */ false); 1470 } 1471 getScreenOnTime(@ullable final List<AppUsagePeriod> appUsagePeriodList)1472 private static long getScreenOnTime(@Nullable final List<AppUsagePeriod> appUsagePeriodList) { 1473 if (appUsagePeriodList == null || appUsagePeriodList.isEmpty()) { 1474 return 0; 1475 } 1476 // Create a list of endpoints (the beginning or the end) of usage periods and order the list 1477 // chronologically. 1478 final List<AppUsageEndPoint> endPoints = 1479 appUsagePeriodList.stream() 1480 .flatMap( 1481 foregroundUsage -> 1482 Stream.of( 1483 AppUsageEndPoint.newBuilder() 1484 .setTimestamp( 1485 foregroundUsage.getStartTime()) 1486 .setType(AppUsageEndPointType.START) 1487 .build(), 1488 AppUsageEndPoint.newBuilder() 1489 .setTimestamp(foregroundUsage.getEndTime()) 1490 .setType(AppUsageEndPointType.END) 1491 .build())) 1492 .sorted((x, y) -> (int) (x.getTimestamp() - y.getTimestamp())) 1493 .collect(Collectors.toList()); 1494 1495 // Traverse the list of endpoints in order to determine the non-overlapping usage duration. 1496 int numberOfActiveAppUsagePeriods = 0; 1497 long startOfCurrentContiguousAppUsagePeriod = 0; 1498 long totalScreenOnTime = 0; 1499 for (final AppUsageEndPoint endPoint : endPoints) { 1500 if (endPoint.getType() == AppUsageEndPointType.START) { 1501 if (numberOfActiveAppUsagePeriods++ == 0) { 1502 startOfCurrentContiguousAppUsagePeriod = endPoint.getTimestamp(); 1503 } 1504 } else { 1505 if (--numberOfActiveAppUsagePeriods == 0) { 1506 totalScreenOnTime += 1507 (endPoint.getTimestamp() - startOfCurrentContiguousAppUsagePeriod); 1508 } 1509 } 1510 } 1511 1512 return totalScreenOnTime; 1513 } 1514 isConsumedFromOtherUsers( final int currentUserId, final int workProfileUserId, final BatteryHistEntry batteryHistEntry)1515 private static boolean isConsumedFromOtherUsers( 1516 final int currentUserId, 1517 final int workProfileUserId, 1518 final BatteryHistEntry batteryHistEntry) { 1519 return isUidConsumer(batteryHistEntry.mConsumerType) 1520 && batteryHistEntry.mUserId != currentUserId 1521 && batteryHistEntry.mUserId != workProfileUserId; 1522 } 1523 1524 @Nullable getAccumulatedUsageDiffData( final Context context, final Collection<BatteryDiffData> batteryDiffDataList)1525 private static BatteryDiffData getAccumulatedUsageDiffData( 1526 final Context context, final Collection<BatteryDiffData> batteryDiffDataList) { 1527 final Map<String, BatteryDiffEntry> diffEntryMap = new ArrayMap<>(); 1528 final List<BatteryDiffEntry> appEntries = new ArrayList<>(); 1529 final List<BatteryDiffEntry> systemEntries = new ArrayList<>(); 1530 1531 long startTimestamp = Long.MAX_VALUE; 1532 long endTimestamp = 0; 1533 int startBatteryLevel = BATTERY_LEVEL_UNKNOWN; 1534 int endBatteryLevel = BATTERY_LEVEL_UNKNOWN; 1535 long totalScreenOnTime = 0; 1536 for (BatteryDiffData batteryDiffData : batteryDiffDataList) { 1537 if (batteryDiffData == null) { 1538 continue; 1539 } 1540 if (startTimestamp > batteryDiffData.getStartTimestamp()) { 1541 startTimestamp = batteryDiffData.getStartTimestamp(); 1542 startBatteryLevel = batteryDiffData.getStartBatteryLevel(); 1543 } 1544 if (endTimestamp > batteryDiffData.getEndTimestamp()) { 1545 endTimestamp = batteryDiffData.getEndTimestamp(); 1546 endBatteryLevel = batteryDiffData.getEndBatteryLevel(); 1547 } 1548 totalScreenOnTime += batteryDiffData.getScreenOnTime(); 1549 for (BatteryDiffEntry entry : batteryDiffData.getAppDiffEntryList()) { 1550 computeUsageDiffDataPerEntry(entry, diffEntryMap); 1551 } 1552 for (BatteryDiffEntry entry : batteryDiffData.getSystemDiffEntryList()) { 1553 computeUsageDiffDataPerEntry(entry, diffEntryMap); 1554 } 1555 } 1556 1557 final Collection<BatteryDiffEntry> diffEntryList = diffEntryMap.values(); 1558 for (BatteryDiffEntry entry : diffEntryList) { 1559 if (entry.isSystemEntry()) { 1560 systemEntries.add(entry); 1561 } else { 1562 appEntries.add(entry); 1563 } 1564 } 1565 1566 return diffEntryList.isEmpty() ? null : new BatteryDiffData(context, startTimestamp, 1567 endTimestamp, startBatteryLevel, endBatteryLevel, totalScreenOnTime, appEntries, 1568 systemEntries, /* systemAppsPackageNames= */ new ArraySet<>(), 1569 /* systemAppsUids= */ new ArraySet<>(), /* isAccumulated= */ true); 1570 } 1571 computeUsageDiffDataPerEntry( final BatteryDiffEntry entry, final Map<String, BatteryDiffEntry> diffEntryMap)1572 private static void computeUsageDiffDataPerEntry( 1573 final BatteryDiffEntry entry, 1574 final Map<String, BatteryDiffEntry> diffEntryMap) { 1575 final String key = entry.getKey(); 1576 final BatteryDiffEntry oldBatteryDiffEntry = diffEntryMap.get(key); 1577 // Creates new BatteryDiffEntry if we don't have it. 1578 if (oldBatteryDiffEntry == null) { 1579 diffEntryMap.put(key, entry.clone()); 1580 } else { 1581 // Sums up some field data into the existing one. 1582 oldBatteryDiffEntry.mForegroundUsageTimeInMs += 1583 entry.mForegroundUsageTimeInMs; 1584 oldBatteryDiffEntry.mBackgroundUsageTimeInMs += 1585 entry.mBackgroundUsageTimeInMs; 1586 oldBatteryDiffEntry.mScreenOnTimeInMs += 1587 entry.mScreenOnTimeInMs; 1588 oldBatteryDiffEntry.mConsumePower += entry.mConsumePower; 1589 oldBatteryDiffEntry.mForegroundUsageConsumePower += entry.mForegroundUsageConsumePower; 1590 oldBatteryDiffEntry.mForegroundServiceUsageConsumePower 1591 += entry.mForegroundServiceUsageConsumePower; 1592 oldBatteryDiffEntry.mBackgroundUsageConsumePower += entry.mBackgroundUsageConsumePower; 1593 oldBatteryDiffEntry.mCachedUsageConsumePower += entry.mCachedUsageConsumePower; 1594 } 1595 } 1596 shouldShowBatteryAttributionList(final Context context)1597 private static boolean shouldShowBatteryAttributionList(final Context context) { 1598 final PowerProfile powerProfile = new PowerProfile(context); 1599 // Cheap hack to try to figure out if the power_profile.xml was populated. 1600 final double averagePowerForOrdinal = powerProfile.getAveragePowerForOrdinal( 1601 PowerProfile.POWER_GROUP_DISPLAY_SCREEN_FULL, 0); 1602 final boolean shouldShowBatteryAttributionList = 1603 averagePowerForOrdinal >= MIN_AVERAGE_POWER_THRESHOLD_MILLI_AMP; 1604 if (!shouldShowBatteryAttributionList) { 1605 Log.w(TAG, "shouldShowBatteryAttributionList(): " + averagePowerForOrdinal); 1606 } 1607 return shouldShowBatteryAttributionList; 1608 } 1609 1610 /** 1611 * We want to coalesce some UIDs. For example, dex2oat runs under a shared gid that 1612 * exists for all users of the same app. We detect this case and merge the power use 1613 * for dex2oat to the device OWNER's use of the app. 1614 * 1615 * @return A sorted list of apps using power. 1616 */ getCoalescedUsageList(final Context context, final BatteryUtils batteryUtils, final BatteryUsageStats batteryUsageStats, final boolean loadDataInBackground)1617 private static List<BatteryEntry> getCoalescedUsageList(final Context context, 1618 final BatteryUtils batteryUtils, 1619 final BatteryUsageStats batteryUsageStats, 1620 final boolean loadDataInBackground) { 1621 final PackageManager packageManager = context.getPackageManager(); 1622 final UserManager userManager = context.getSystemService(UserManager.class); 1623 final SparseArray<BatteryEntry> batteryEntryList = new SparseArray<>(); 1624 final ArrayList<BatteryEntry> results = new ArrayList<>(); 1625 final List<UidBatteryConsumer> uidBatteryConsumers = 1626 batteryUsageStats.getUidBatteryConsumers(); 1627 1628 // Sort to have all apps with "real" UIDs first, followed by apps that are supposed 1629 // to be combined with the real ones. 1630 uidBatteryConsumers.sort(Comparator.comparingInt( 1631 consumer -> consumer.getUid() == getRealUid(consumer) ? 0 : 1)); 1632 1633 for (int i = 0, size = uidBatteryConsumers.size(); i < size; i++) { 1634 final UidBatteryConsumer consumer = uidBatteryConsumers.get(i); 1635 final int uid = getRealUid(consumer); 1636 1637 final String[] packages = packageManager.getPackagesForUid(uid); 1638 if (batteryUtils.shouldHideUidBatteryConsumerUnconditionally(consumer, packages)) { 1639 continue; 1640 } 1641 1642 final boolean isHidden = batteryUtils.shouldHideUidBatteryConsumer(consumer, packages); 1643 final int index = batteryEntryList.indexOfKey(uid); 1644 if (index < 0) { 1645 // New entry. 1646 batteryEntryList.put(uid, new BatteryEntry(context, userManager, consumer, 1647 isHidden, uid, packages, null, loadDataInBackground)); 1648 } else { 1649 // Combine BatterySippers if we already have one with this UID. 1650 final BatteryEntry existingSipper = batteryEntryList.valueAt(index); 1651 existingSipper.add(consumer); 1652 } 1653 } 1654 1655 final BatteryConsumer deviceConsumer = batteryUsageStats.getAggregateBatteryConsumer( 1656 BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE); 1657 1658 for (int componentId = 0; componentId < BatteryConsumer.POWER_COMPONENT_COUNT; 1659 componentId++) { 1660 results.add(new BatteryEntry(context, componentId, 1661 deviceConsumer.getConsumedPower(componentId), 1662 deviceConsumer.getUsageDurationMillis(componentId), 1663 componentId == POWER_COMPONENT_SYSTEM_SERVICES 1664 || componentId == POWER_COMPONENT_WAKELOCK)); 1665 } 1666 1667 for (int componentId = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID; 1668 componentId < BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID 1669 + deviceConsumer.getCustomPowerComponentCount(); 1670 componentId++) { 1671 results.add(new BatteryEntry(context, componentId, 1672 deviceConsumer.getCustomPowerComponentName(componentId), 1673 deviceConsumer.getConsumedPowerForCustomComponent(componentId))); 1674 } 1675 1676 final List<UserBatteryConsumer> userBatteryConsumers = 1677 batteryUsageStats.getUserBatteryConsumers(); 1678 for (int i = 0, size = userBatteryConsumers.size(); i < size; i++) { 1679 final UserBatteryConsumer consumer = userBatteryConsumers.get(i); 1680 results.add(new BatteryEntry(context, userManager, consumer, /* isHidden */ true, 1681 Process.INVALID_UID, null, null, loadDataInBackground)); 1682 } 1683 1684 final int numUidSippers = batteryEntryList.size(); 1685 1686 for (int i = 0; i < numUidSippers; i++) { 1687 results.add(batteryEntryList.valueAt(i)); 1688 } 1689 1690 // The sort order must have changed, so re-sort based on total power use. 1691 results.sort(BatteryEntry.COMPARATOR); 1692 return results; 1693 } 1694 getRealUid(final UidBatteryConsumer consumer)1695 private static int getRealUid(final UidBatteryConsumer consumer) { 1696 int realUid = consumer.getUid(); 1697 1698 // Check if this UID is a shared GID. If so, we combine it with the OWNER's 1699 // actual app UID. 1700 if (isSharedGid(consumer.getUid())) { 1701 realUid = UserHandle.getUid(UserHandle.USER_SYSTEM, 1702 UserHandle.getAppIdFromSharedAppGid(consumer.getUid())); 1703 } 1704 1705 // Check if this UID is a system UID (mediaserver, logd, nfc, drm, etc). 1706 if (isSystemUid(realUid) 1707 && !MEDIASERVER_PACKAGE_NAME.equals(consumer.getPackageWithHighestDrain())) { 1708 // Use the system UID for all UIDs running in their own sandbox that 1709 // are not apps. We exclude mediaserver because we already are expected to 1710 // report that as a separate item. 1711 realUid = Process.SYSTEM_UID; 1712 } 1713 return realUid; 1714 } 1715 isSharedGid(final int uid)1716 private static boolean isSharedGid(final int uid) { 1717 return UserHandle.getAppIdFromSharedAppGid(uid) > 0; 1718 } 1719 isSystemUid(final int uid)1720 private static boolean isSystemUid(final int uid) { 1721 final int appUid = UserHandle.getAppId(uid); 1722 return appUid >= Process.SYSTEM_UID && appUid < Process.FIRST_APPLICATION_UID; 1723 } 1724 isUsageMapValid( final Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageMap, final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay)1725 private static boolean isUsageMapValid( 1726 final Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageMap, 1727 final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay) { 1728 if (batteryUsageMap.get(SELECTED_INDEX_ALL) == null 1729 || !batteryUsageMap.get(SELECTED_INDEX_ALL).containsKey(SELECTED_INDEX_ALL)) { 1730 Log.e(TAG, "no [SELECTED_INDEX_ALL][SELECTED_INDEX_ALL] in batteryUsageMap"); 1731 return false; 1732 } 1733 for (int dailyIndex = 0; dailyIndex < hourlyBatteryLevelsPerDay.size(); dailyIndex++) { 1734 if (batteryUsageMap.get(dailyIndex) == null 1735 || !batteryUsageMap.get(dailyIndex).containsKey(SELECTED_INDEX_ALL)) { 1736 Log.e(TAG, "no [" + dailyIndex + "][SELECTED_INDEX_ALL] in batteryUsageMap, " 1737 + "daily size is: " + hourlyBatteryLevelsPerDay.size()); 1738 return false; 1739 } 1740 if (hourlyBatteryLevelsPerDay.get(dailyIndex) == null) { 1741 continue; 1742 } 1743 final List<Long> timestamps = hourlyBatteryLevelsPerDay.get(dailyIndex).getTimestamps(); 1744 // Length of hourly usage map should be the length of hourly level data - 1. 1745 for (int hourlyIndex = 0; hourlyIndex < timestamps.size() - 1; hourlyIndex++) { 1746 if (!batteryUsageMap.get(dailyIndex).containsKey(hourlyIndex)) { 1747 Log.e(TAG, "no [" + dailyIndex + "][" + hourlyIndex + "] in batteryUsageMap, " 1748 + "hourly size is: " + (timestamps.size() - 1)); 1749 return false; 1750 } 1751 } 1752 } 1753 return true; 1754 } 1755 getDiffValue(long v1, long v2)1756 private static long getDiffValue(long v1, long v2) { 1757 return v2 > v1 ? v2 - v1 : 0; 1758 } 1759 getDiffValue(double v1, double v2)1760 private static double getDiffValue(double v1, double v2) { 1761 return v2 > v1 ? v2 - v1 : 0; 1762 } 1763 getCurrentTimeMillis()1764 private static long getCurrentTimeMillis() { 1765 return sTestCurrentTimeMillis > 0 ? sTestCurrentTimeMillis : System.currentTimeMillis(); 1766 } 1767 log(Context context, final String content, final long timestamp, final BatteryHistEntry entry)1768 private static void log(Context context, final String content, final long timestamp, 1769 final BatteryHistEntry entry) { 1770 if (sDebug) { 1771 Log.d(TAG, String.format(entry != null ? "%s %s:\n%s" : "%s %s:%s", 1772 ConvertUtils.utcToLocalTimeForLogging(timestamp), content, entry)); 1773 } 1774 } 1775 } 1776