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