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