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.utcToLocalTime; 20 21 import android.app.settings.SettingsEnums; 22 import android.content.ContentValues; 23 import android.content.Context; 24 import android.os.AsyncTask; 25 import android.os.BatteryStatsManager; 26 import android.os.BatteryUsageStats; 27 import android.os.BatteryUsageStatsQuery; 28 import android.os.Handler; 29 import android.os.Looper; 30 import android.os.UserHandle; 31 import android.os.UserManager; 32 import android.text.TextUtils; 33 import android.text.format.DateUtils; 34 import android.util.ArraySet; 35 import android.util.Log; 36 37 import androidx.annotation.Nullable; 38 39 import com.android.internal.annotations.VisibleForTesting; 40 import com.android.settings.Utils; 41 import com.android.settings.fuelgauge.BatteryUtils; 42 import com.android.settings.overlay.FeatureFactory; 43 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; 44 import com.android.settingslib.fuelgauge.BatteryStatus; 45 46 import java.time.Duration; 47 import java.util.ArrayList; 48 import java.util.Calendar; 49 import java.util.Collection; 50 import java.util.Collections; 51 import java.util.HashMap; 52 import java.util.Iterator; 53 import java.util.List; 54 import java.util.Map; 55 import java.util.Set; 56 import java.util.stream.Collectors; 57 58 /** 59 * A utility class to process data loaded from database and make the data easy to use for battery 60 * usage UI. 61 */ 62 public final class DataProcessor { 63 private static final boolean DEBUG = false; 64 private static final String TAG = "DataProcessor"; 65 private static final int MIN_DAILY_DATA_SIZE = 2; 66 private static final int MIN_TIMESTAMP_DATA_SIZE = 2; 67 private static final int MAX_DIFF_SECONDS_OF_UPPER_TIMESTAMP = 5; 68 // Maximum total time value for each hourly slot cumulative data at most 2 hours. 69 private static final float TOTAL_HOURLY_TIME_THRESHOLD = DateUtils.HOUR_IN_MILLIS * 2; 70 private static final long MIN_TIME_SLOT = DateUtils.HOUR_IN_MILLIS * 2; 71 private static final Map<String, BatteryHistEntry> EMPTY_BATTERY_MAP = new HashMap<>(); 72 private static final BatteryHistEntry EMPTY_BATTERY_HIST_ENTRY = 73 new BatteryHistEntry(new ContentValues()); 74 75 @VisibleForTesting 76 static final double PERCENTAGE_OF_TOTAL_THRESHOLD = 1f; 77 @VisibleForTesting 78 static final int SELECTED_INDEX_ALL = BatteryChartViewModel.SELECTED_INDEX_ALL; 79 80 /** A fake package name to represent no BatteryEntry data. */ 81 public static final String FAKE_PACKAGE_NAME = "fake_package"; 82 83 /** A callback listener when battery usage loading async task is executed. */ 84 public interface UsageMapAsyncResponse { 85 /** The callback function when batteryUsageMap is loaded. */ onBatteryUsageMapLoaded( Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageMap)86 void onBatteryUsageMapLoaded( 87 Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageMap); 88 } 89 DataProcessor()90 private DataProcessor() { 91 } 92 93 /** 94 * @return Returns battery level data and start async task to compute battery diff usage data 95 * and load app labels + icons. 96 * Returns null if the input is invalid or not having at least 2 hours data. 97 */ 98 @Nullable getBatteryLevelData( Context context, @Nullable Handler handler, @Nullable final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap, final UsageMapAsyncResponse asyncResponseDelegate)99 public static BatteryLevelData getBatteryLevelData( 100 Context context, 101 @Nullable Handler handler, 102 @Nullable final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap, 103 final UsageMapAsyncResponse asyncResponseDelegate) { 104 if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) { 105 Log.d(TAG, "batteryHistoryMap is null in getBatteryLevelData()"); 106 loadBatteryUsageDataFromBatteryStatsService( 107 context, handler, asyncResponseDelegate); 108 return null; 109 } 110 handler = handler != null ? handler : new Handler(Looper.getMainLooper()); 111 // Process raw history map data into hourly timestamps. 112 final Map<Long, Map<String, BatteryHistEntry>> processedBatteryHistoryMap = 113 getHistoryMapWithExpectedTimestamps(context, batteryHistoryMap); 114 // Wrap and processed history map into easy-to-use format for UI rendering. 115 final BatteryLevelData batteryLevelData = 116 getLevelDataThroughProcessedHistoryMap(context, processedBatteryHistoryMap); 117 if (batteryLevelData == null) { 118 loadBatteryUsageDataFromBatteryStatsService( 119 context, handler, asyncResponseDelegate); 120 Log.d(TAG, "getBatteryLevelData() returns null"); 121 return null; 122 } 123 124 // Start the async task to compute diff usage data and load labels and icons. 125 new ComputeUsageMapAndLoadItemsTask( 126 context, 127 handler, 128 asyncResponseDelegate, 129 batteryLevelData.getHourlyBatteryLevelsPerDay(), 130 processedBatteryHistoryMap).execute(); 131 132 return batteryLevelData; 133 } 134 135 /** 136 * @return Returns battery usage data of different entries. 137 * Returns null if the input is invalid or there is no enough data. 138 */ 139 @Nullable getBatteryUsageData( Context context, @Nullable final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap)140 public static Map<Integer, Map<Integer, BatteryDiffData>> getBatteryUsageData( 141 Context context, 142 @Nullable final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) { 143 if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) { 144 Log.d(TAG, "getBatteryLevelData() returns null"); 145 return null; 146 } 147 // Process raw history map data into hourly timestamps. 148 final Map<Long, Map<String, BatteryHistEntry>> processedBatteryHistoryMap = 149 getHistoryMapWithExpectedTimestamps(context, batteryHistoryMap); 150 // Wrap and processed history map into easy-to-use format for UI rendering. 151 final BatteryLevelData batteryLevelData = 152 getLevelDataThroughProcessedHistoryMap(context, processedBatteryHistoryMap); 153 return batteryLevelData == null 154 ? null 155 : getBatteryUsageMap( 156 context, 157 batteryLevelData.getHourlyBatteryLevelsPerDay(), 158 processedBatteryHistoryMap); 159 } 160 161 /** 162 * @return Returns whether the target is in the CharSequence array. 163 */ contains(String target, CharSequence[] packageNames)164 public static boolean contains(String target, CharSequence[] packageNames) { 165 if (target != null && packageNames != null) { 166 for (CharSequence packageName : packageNames) { 167 if (TextUtils.equals(target, packageName)) { 168 return true; 169 } 170 } 171 } 172 return false; 173 } 174 175 /** 176 * @return Returns the processed history map which has interpolated to every hour data. 177 * The start and end timestamp must be the even hours. 178 * The keys of processed history map should contain every hour between the start and end 179 * timestamp. If there's no data in some key, the value will be the empty hashmap. 180 */ 181 @VisibleForTesting getHistoryMapWithExpectedTimestamps( Context context, final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap)182 static Map<Long, Map<String, BatteryHistEntry>> getHistoryMapWithExpectedTimestamps( 183 Context context, 184 final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) { 185 final long startTime = System.currentTimeMillis(); 186 final List<Long> rawTimestampList = new ArrayList<>(batteryHistoryMap.keySet()); 187 final Map<Long, Map<String, BatteryHistEntry>> resultMap = new HashMap(); 188 if (rawTimestampList.isEmpty()) { 189 Log.d(TAG, "empty batteryHistoryMap in getHistoryMapWithExpectedTimestamps()"); 190 return resultMap; 191 } 192 Collections.sort(rawTimestampList); 193 final List<Long> expectedTimestampList = getTimestampSlots(rawTimestampList); 194 final boolean isFromFullCharge = 195 isFromFullCharge(batteryHistoryMap.get(rawTimestampList.get(0))); 196 interpolateHistory( 197 context, rawTimestampList, expectedTimestampList, isFromFullCharge, 198 batteryHistoryMap, resultMap); 199 Log.d(TAG, String.format("getHistoryMapWithExpectedTimestamps() size=%d in %d/ms", 200 resultMap.size(), (System.currentTimeMillis() - startTime))); 201 return resultMap; 202 } 203 204 @VisibleForTesting 205 @Nullable getLevelDataThroughProcessedHistoryMap( Context context, final Map<Long, Map<String, BatteryHistEntry>> processedBatteryHistoryMap)206 static BatteryLevelData getLevelDataThroughProcessedHistoryMap( 207 Context context, 208 final Map<Long, Map<String, BatteryHistEntry>> processedBatteryHistoryMap) { 209 final List<Long> timestampList = new ArrayList<>(processedBatteryHistoryMap.keySet()); 210 Collections.sort(timestampList); 211 final List<Long> dailyTimestamps = getDailyTimestamps(timestampList); 212 // There should be at least the start and end timestamps. Otherwise, return null to not show 213 // data in usage chart. 214 if (dailyTimestamps.size() < MIN_DAILY_DATA_SIZE) { 215 return null; 216 } 217 218 final List<List<Long>> hourlyTimestamps = getHourlyTimestamps(dailyTimestamps); 219 final BatteryLevelData.PeriodBatteryLevelData dailyLevelData = 220 getPeriodBatteryLevelData(context, processedBatteryHistoryMap, dailyTimestamps); 221 final List<BatteryLevelData.PeriodBatteryLevelData> hourlyLevelData = 222 getHourlyPeriodBatteryLevelData( 223 context, processedBatteryHistoryMap, hourlyTimestamps); 224 return new BatteryLevelData(dailyLevelData, hourlyLevelData); 225 } 226 227 /** 228 * Computes expected timestamp slots for last full charge, which will return hourly timestamps 229 * between start and end two even hour values. 230 */ 231 @VisibleForTesting getTimestampSlots(final List<Long> rawTimestampList)232 static List<Long> getTimestampSlots(final List<Long> rawTimestampList) { 233 final List<Long> timestampSlots = new ArrayList<>(); 234 final int rawTimestampListSize = rawTimestampList.size(); 235 // If timestamp number is smaller than 2, the following computation is not necessary. 236 if (rawTimestampListSize < MIN_TIMESTAMP_DATA_SIZE) { 237 return timestampSlots; 238 } 239 final long rawStartTimestamp = rawTimestampList.get(0); 240 final long rawEndTimestamp = rawTimestampList.get(rawTimestampListSize - 1); 241 // No matter the start is from last full charge or 6 days ago, use the nearest even hour. 242 final long startTimestamp = getNearestEvenHourTimestamp(rawStartTimestamp); 243 // Use the even hour before the raw end timestamp as the end. 244 final long endTimestamp = getLastEvenHourBeforeTimestamp(rawEndTimestamp); 245 // If the start timestamp is later or equal the end one, return the empty list. 246 if (startTimestamp >= endTimestamp) { 247 return timestampSlots; 248 } 249 for (long timestamp = startTimestamp; timestamp <= endTimestamp; 250 timestamp += DateUtils.HOUR_IN_MILLIS) { 251 timestampSlots.add(timestamp); 252 } 253 return timestampSlots; 254 } 255 256 /** 257 * Computes expected daily timestamp slots. 258 * 259 * The valid result should be composed of 3 parts: 260 * 1) start timestamp 261 * 2) every 00:00 timestamp (default timezone) between the start and end 262 * 3) end timestamp 263 * Otherwise, returns an empty list. 264 */ 265 @VisibleForTesting getDailyTimestamps(final List<Long> timestampList)266 static List<Long> getDailyTimestamps(final List<Long> timestampList) { 267 final List<Long> dailyTimestampList = new ArrayList<>(); 268 // If timestamp number is smaller than 2, the following computation is not necessary. 269 if (timestampList.size() < MIN_TIMESTAMP_DATA_SIZE) { 270 return dailyTimestampList; 271 } 272 final long startTime = timestampList.get(0); 273 final long endTime = timestampList.get(timestampList.size() - 1); 274 // If the timestamp diff is smaller than MIN_TIME_SLOT, returns the empty list directly. 275 if (endTime - startTime < MIN_TIME_SLOT) { 276 return dailyTimestampList; 277 } 278 long nextDay = getTimestampOfNextDay(startTime); 279 // Only if the timestamp diff in the first day is bigger than MIN_TIME_SLOT, start from the 280 // first day. Otherwise, start from the second day. 281 if (nextDay - startTime >= MIN_TIME_SLOT) { 282 dailyTimestampList.add(startTime); 283 } 284 while (nextDay < endTime) { 285 dailyTimestampList.add(nextDay); 286 nextDay += DateUtils.DAY_IN_MILLIS; 287 } 288 final long lastDailyTimestamp = dailyTimestampList.get(dailyTimestampList.size() - 1); 289 // Only if the timestamp diff in the last day is bigger than MIN_TIME_SLOT, add the 290 // last day. 291 if (endTime - lastDailyTimestamp >= MIN_TIME_SLOT) { 292 dailyTimestampList.add(endTime); 293 } 294 // The dailyTimestampList must have the start and end timestamp, otherwise, return an empty 295 // list. 296 if (dailyTimestampList.size() < MIN_TIMESTAMP_DATA_SIZE) { 297 return new ArrayList<>(); 298 } 299 return dailyTimestampList; 300 } 301 302 @VisibleForTesting isFromFullCharge(@ullable final Map<String, BatteryHistEntry> entryList)303 static boolean isFromFullCharge(@Nullable final Map<String, BatteryHistEntry> entryList) { 304 if (entryList == null) { 305 Log.d(TAG, "entryList is null in isFromFullCharge()"); 306 return false; 307 } 308 final List<String> entryKeys = new ArrayList<>(entryList.keySet()); 309 if (entryKeys.isEmpty()) { 310 Log.d(TAG, "empty entryList in isFromFullCharge()"); 311 return false; 312 } 313 // The hist entries in the same timestamp should have same battery status and level. 314 // Checking the first one should be enough. 315 final BatteryHistEntry firstHistEntry = entryList.get(entryKeys.get(0)); 316 return BatteryStatus.isCharged(firstHistEntry.mBatteryStatus, firstHistEntry.mBatteryLevel); 317 } 318 319 @VisibleForTesting findNearestTimestamp(final List<Long> timestamps, final long target)320 static long[] findNearestTimestamp(final List<Long> timestamps, final long target) { 321 final long[] results = new long[] {Long.MIN_VALUE, Long.MAX_VALUE}; 322 // Searches the nearest lower and upper timestamp value. 323 timestamps.forEach(timestamp -> { 324 if (timestamp <= target && timestamp > results[0]) { 325 results[0] = timestamp; 326 } 327 if (timestamp >= target && timestamp < results[1]) { 328 results[1] = timestamp; 329 } 330 }); 331 // Uses zero value to represent invalid searching result. 332 results[0] = results[0] == Long.MIN_VALUE ? 0 : results[0]; 333 results[1] = results[1] == Long.MAX_VALUE ? 0 : results[1]; 334 return results; 335 } 336 337 /** 338 * @return Returns the timestamp for 00:00 1 day after the given timestamp based on local 339 * timezone. 340 */ 341 @VisibleForTesting getTimestampOfNextDay(long timestamp)342 static long getTimestampOfNextDay(long timestamp) { 343 return getTimestampWithDayDiff(timestamp, /*dayDiff=*/ 1); 344 } 345 346 /** 347 * Returns whether currentSlot will be used in daily chart. 348 */ 349 @VisibleForTesting isForDailyChart(final boolean isStartOrEnd, final long currentSlot)350 static boolean isForDailyChart(final boolean isStartOrEnd, final long currentSlot) { 351 // The start and end timestamps will always be used in daily chart. 352 if (isStartOrEnd) { 353 return true; 354 } 355 356 // The timestamps for 00:00 will be used in daily chart. 357 final long startOfTheDay = getTimestampWithDayDiff(currentSlot, /*dayDiff=*/ 0); 358 return currentSlot == startOfTheDay; 359 } 360 361 /** 362 * @return Returns the indexed battery usage data for each corresponding time slot. 363 * 364 * There could be 2 cases of the returned value: 365 * 1) null: empty or invalid data. 366 * 2) non-null: must be a 2d map and composed by 3 parts: 367 * 1 - [SELECTED_INDEX_ALL][SELECTED_INDEX_ALL] 368 * 2 - [0][SELECTED_INDEX_ALL] ~ [maxDailyIndex][SELECTED_INDEX_ALL] 369 * 3 - [0][0] ~ [maxDailyIndex][maxHourlyIndex] 370 */ 371 @VisibleForTesting 372 @Nullable getBatteryUsageMap( final Context context, final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay, final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap)373 static Map<Integer, Map<Integer, BatteryDiffData>> getBatteryUsageMap( 374 final Context context, 375 final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay, 376 final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) { 377 if (batteryHistoryMap.isEmpty()) { 378 return null; 379 } 380 final Map<Integer, Map<Integer, BatteryDiffData>> resultMap = new HashMap<>(); 381 // Insert diff data from [0][0] to [maxDailyIndex][maxHourlyIndex]. 382 insertHourlyUsageDiffData( 383 context, hourlyBatteryLevelsPerDay, batteryHistoryMap, resultMap); 384 // Insert diff data from [0][SELECTED_INDEX_ALL] to [maxDailyIndex][SELECTED_INDEX_ALL]. 385 insertDailyUsageDiffData(hourlyBatteryLevelsPerDay, resultMap); 386 // Insert diff data [SELECTED_INDEX_ALL][SELECTED_INDEX_ALL]. 387 insertAllUsageDiffData(resultMap); 388 // Compute the apps number before purge. Must put before purgeLowPercentageAndFakeData. 389 final int countOfAppBeforePurge = getCountOfApps(resultMap); 390 purgeLowPercentageAndFakeData(context, resultMap); 391 // Compute the apps number after purge. Must put after purgeLowPercentageAndFakeData. 392 final int countOfAppAfterPurge = getCountOfApps(resultMap); 393 if (!isUsageMapValid(resultMap, hourlyBatteryLevelsPerDay)) { 394 return null; 395 } 396 397 logAppCountMetrics(context, countOfAppBeforePurge, countOfAppAfterPurge); 398 return resultMap; 399 } 400 401 @VisibleForTesting 402 @Nullable generateBatteryDiffData( final Context context, @Nullable final List<BatteryEntry> batteryEntryList, final BatteryUsageStats batteryUsageStats)403 static BatteryDiffData generateBatteryDiffData( 404 final Context context, 405 @Nullable final List<BatteryEntry> batteryEntryList, 406 final BatteryUsageStats batteryUsageStats) { 407 final List<BatteryHistEntry> batteryHistEntryList = 408 convertToBatteryHistEntry(batteryEntryList, batteryUsageStats); 409 if (batteryHistEntryList == null || batteryHistEntryList.isEmpty()) { 410 Log.w(TAG, "batteryHistEntryList is null or empty in generateBatteryDiffData()"); 411 return null; 412 } 413 final int currentUserId = context.getUserId(); 414 final UserHandle userHandle = 415 Utils.getManagedProfile(context.getSystemService(UserManager.class)); 416 final int workProfileUserId = 417 userHandle != null ? userHandle.getIdentifier() : Integer.MIN_VALUE; 418 final List<BatteryDiffEntry> appEntries = new ArrayList<>(); 419 final List<BatteryDiffEntry> systemEntries = new ArrayList<>(); 420 double totalConsumePower = 0f; 421 double consumePowerFromOtherUsers = 0f; 422 423 for (BatteryHistEntry entry : batteryHistEntryList) { 424 final boolean isFromOtherUsers = isConsumedFromOtherUsers( 425 currentUserId, workProfileUserId, entry); 426 totalConsumePower += entry.mConsumePower; 427 if (isFromOtherUsers) { 428 consumePowerFromOtherUsers += entry.mConsumePower; 429 } else { 430 final BatteryDiffEntry currentBatteryDiffEntry = new BatteryDiffEntry( 431 context, 432 entry.mForegroundUsageTimeInMs, 433 entry.mBackgroundUsageTimeInMs, 434 entry.mConsumePower, 435 entry); 436 if (currentBatteryDiffEntry.isSystemEntry()) { 437 systemEntries.add(currentBatteryDiffEntry); 438 } else { 439 appEntries.add(currentBatteryDiffEntry); 440 } 441 } 442 } 443 if (consumePowerFromOtherUsers != 0) { 444 systemEntries.add(createOtherUsersEntry(context, consumePowerFromOtherUsers)); 445 } 446 447 // If there is no data, return null instead of empty item. 448 if (appEntries.isEmpty() && systemEntries.isEmpty()) { 449 return null; 450 } 451 452 return new BatteryDiffData(appEntries, systemEntries, totalConsumePower); 453 } 454 455 /** 456 * Starts the async task to load battery diff usage data and load app labels + icons. 457 */ loadBatteryUsageDataFromBatteryStatsService( Context context, @Nullable Handler handler, final UsageMapAsyncResponse asyncResponseDelegate)458 private static void loadBatteryUsageDataFromBatteryStatsService( 459 Context context, 460 @Nullable Handler handler, 461 final UsageMapAsyncResponse asyncResponseDelegate) { 462 new LoadUsageMapFromBatteryStatsServiceTask( 463 context, 464 handler, 465 asyncResponseDelegate).execute(); 466 } 467 468 /** 469 * @return Returns the overall battery usage data from battery stats service directly. 470 * 471 * The returned value should be always a 2d map and composed by only 1 part: 472 * - [SELECTED_INDEX_ALL][SELECTED_INDEX_ALL] 473 */ 474 @Nullable getBatteryUsageMapFromStatsService( final Context context)475 private static Map<Integer, Map<Integer, BatteryDiffData>> getBatteryUsageMapFromStatsService( 476 final Context context) { 477 final Map<Integer, Map<Integer, BatteryDiffData>> resultMap = new HashMap<>(); 478 final Map<Integer, BatteryDiffData> allUsageMap = new HashMap<>(); 479 // Always construct the map whether the value is null or not. 480 allUsageMap.put(SELECTED_INDEX_ALL, 481 getBatteryDiffDataFromBatteryStatsService(context)); 482 resultMap.put(SELECTED_INDEX_ALL, allUsageMap); 483 484 // Compute the apps number before purge. Must put before purgeLowPercentageAndFakeData. 485 final int countOfAppBeforePurge = getCountOfApps(resultMap); 486 purgeLowPercentageAndFakeData(context, resultMap); 487 // Compute the apps number after purge. Must put after purgeLowPercentageAndFakeData. 488 final int countOfAppAfterPurge = getCountOfApps(resultMap); 489 490 logAppCountMetrics(context, countOfAppBeforePurge, countOfAppAfterPurge); 491 return resultMap; 492 } 493 494 @Nullable getBatteryDiffDataFromBatteryStatsService( final Context context)495 private static BatteryDiffData getBatteryDiffDataFromBatteryStatsService( 496 final Context context) { 497 BatteryDiffData batteryDiffData = null; 498 try { 499 final BatteryUsageStatsQuery batteryUsageStatsQuery = 500 new BatteryUsageStatsQuery.Builder().includeBatteryHistory().build(); 501 final BatteryUsageStats batteryUsageStats = 502 context.getSystemService(BatteryStatsManager.class) 503 .getBatteryUsageStats(batteryUsageStatsQuery); 504 505 if (batteryUsageStats == null) { 506 Log.w(TAG, "batteryUsageStats is null content"); 507 return null; 508 } 509 510 final List<BatteryEntry> batteryEntryList = 511 generateBatteryEntryListFromBatteryUsageStats(context, batteryUsageStats); 512 batteryDiffData = generateBatteryDiffData(context, batteryEntryList, batteryUsageStats); 513 } catch (RuntimeException e) { 514 Log.e(TAG, "load batteryUsageStats:" + e); 515 } 516 517 return batteryDiffData; 518 } 519 520 @Nullable generateBatteryEntryListFromBatteryUsageStats( final Context context, final BatteryUsageStats batteryUsageStats)521 private static List<BatteryEntry> generateBatteryEntryListFromBatteryUsageStats( 522 final Context context, final BatteryUsageStats batteryUsageStats) { 523 // Loads the battery consuming data. 524 final BatteryAppListPreferenceController controller = 525 new BatteryAppListPreferenceController( 526 context, 527 /*preferenceKey=*/ null, 528 /*lifecycle=*/ null, 529 /*activity*=*/ null, 530 /*fragment=*/ null); 531 return controller.getBatteryEntryList(batteryUsageStats, /*showAllApps=*/ true); 532 } 533 534 @Nullable convertToBatteryHistEntry( @ullable final List<BatteryEntry> batteryEntryList, final BatteryUsageStats batteryUsageStats)535 private static List<BatteryHistEntry> convertToBatteryHistEntry( 536 @Nullable final List<BatteryEntry> batteryEntryList, 537 final BatteryUsageStats batteryUsageStats) { 538 if (batteryEntryList == null || batteryEntryList.isEmpty()) { 539 Log.w(TAG, "batteryEntryList is null or empty in convertToBatteryHistEntry()"); 540 return null; 541 } 542 return batteryEntryList.stream() 543 .filter(entry -> { 544 final long foregroundMs = entry.getTimeInForegroundMs(); 545 final long backgroundMs = entry.getTimeInBackgroundMs(); 546 return entry.getConsumedPower() > 0 547 || (entry.getConsumedPower() == 0 548 && (foregroundMs != 0 || backgroundMs != 0)); 549 }) 550 .map(entry -> ConvertUtils.convertToBatteryHistEntry( 551 entry, 552 batteryUsageStats)) 553 .collect(Collectors.toList()); 554 } 555 556 /** 557 * Interpolates history map based on expected timestamp slots and processes the corner case when 558 * the expected start timestamp is earlier than what we have. 559 */ interpolateHistory( Context context, final List<Long> rawTimestampList, final List<Long> expectedTimestampSlots, final boolean isFromFullCharge, final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap, final Map<Long, Map<String, BatteryHistEntry>> resultMap)560 private static void interpolateHistory( 561 Context context, 562 final List<Long> rawTimestampList, 563 final List<Long> expectedTimestampSlots, 564 final boolean isFromFullCharge, 565 final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap, 566 final Map<Long, Map<String, BatteryHistEntry>> resultMap) { 567 if (rawTimestampList.isEmpty() || expectedTimestampSlots.isEmpty()) { 568 return; 569 } 570 final long expectedStartTimestamp = expectedTimestampSlots.get(0); 571 final long rawStartTimestamp = rawTimestampList.get(0); 572 int startIndex = 0; 573 // If the expected start timestamp is full charge or earlier than what we have, use the 574 // first data of what we have directly. This should be OK because the expected start 575 // timestamp is the nearest even hour of the raw start timestamp, their time diff is no 576 // more than 1 hour. 577 if (isFromFullCharge || expectedStartTimestamp < rawStartTimestamp) { 578 startIndex = 1; 579 resultMap.put(expectedStartTimestamp, batteryHistoryMap.get(rawStartTimestamp)); 580 } 581 final int expectedTimestampSlotsSize = expectedTimestampSlots.size(); 582 for (int index = startIndex; index < expectedTimestampSlotsSize; index++) { 583 final long currentSlot = expectedTimestampSlots.get(index); 584 final boolean isStartOrEnd = index == 0 || index == expectedTimestampSlotsSize - 1; 585 interpolateHistoryForSlot( 586 context, currentSlot, rawTimestampList, batteryHistoryMap, resultMap, 587 isStartOrEnd); 588 } 589 } 590 interpolateHistoryForSlot( Context context, final long currentSlot, final List<Long> rawTimestampList, final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap, final Map<Long, Map<String, BatteryHistEntry>> resultMap, final boolean isStartOrEnd)591 private static void interpolateHistoryForSlot( 592 Context context, 593 final long currentSlot, 594 final List<Long> rawTimestampList, 595 final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap, 596 final Map<Long, Map<String, BatteryHistEntry>> resultMap, 597 final boolean isStartOrEnd) { 598 final long[] nearestTimestamps = findNearestTimestamp(rawTimestampList, currentSlot); 599 final long lowerTimestamp = nearestTimestamps[0]; 600 final long upperTimestamp = nearestTimestamps[1]; 601 // Case 1: upper timestamp is zero since scheduler is delayed! 602 if (upperTimestamp == 0) { 603 log(context, "job scheduler is delayed", currentSlot, null); 604 resultMap.put(currentSlot, new HashMap<>()); 605 return; 606 } 607 // Case 2: upper timestamp is closed to the current timestamp. 608 if ((upperTimestamp - currentSlot) 609 < MAX_DIFF_SECONDS_OF_UPPER_TIMESTAMP * DateUtils.SECOND_IN_MILLIS) { 610 log(context, "force align into the nearest slot", currentSlot, null); 611 resultMap.put(currentSlot, batteryHistoryMap.get(upperTimestamp)); 612 return; 613 } 614 // Case 3: lower timestamp is zero before starting to collect data. 615 if (lowerTimestamp == 0) { 616 log(context, "no lower timestamp slot data", currentSlot, null); 617 resultMap.put(currentSlot, new HashMap<>()); 618 return; 619 } 620 interpolateHistoryForSlot(context, 621 currentSlot, lowerTimestamp, upperTimestamp, batteryHistoryMap, resultMap, 622 isStartOrEnd); 623 } 624 interpolateHistoryForSlot( Context context, final long currentSlot, final long lowerTimestamp, final long upperTimestamp, final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap, final Map<Long, Map<String, BatteryHistEntry>> resultMap, final boolean isStartOrEnd)625 private static void interpolateHistoryForSlot( 626 Context context, 627 final long currentSlot, 628 final long lowerTimestamp, 629 final long upperTimestamp, 630 final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap, 631 final Map<Long, Map<String, BatteryHistEntry>> resultMap, 632 final boolean isStartOrEnd) { 633 final Map<String, BatteryHistEntry> lowerEntryDataMap = 634 batteryHistoryMap.get(lowerTimestamp); 635 final Map<String, BatteryHistEntry> upperEntryDataMap = 636 batteryHistoryMap.get(upperTimestamp); 637 // Verifies whether the lower data is valid to use or not by checking boot time. 638 final BatteryHistEntry upperEntryDataFirstEntry = 639 upperEntryDataMap.values().stream().findFirst().get(); 640 final long upperEntryDataBootTimestamp = 641 upperEntryDataFirstEntry.mTimestamp - upperEntryDataFirstEntry.mBootTimestamp; 642 // Lower data is captured before upper data corresponding device is booting. 643 // Skips the booting-specific logics and always does interpolation for daily chart level 644 // data. 645 if (lowerTimestamp < upperEntryDataBootTimestamp 646 && !isForDailyChart(isStartOrEnd, currentSlot)) { 647 // Provides an opportunity to force align the slot directly. 648 if ((upperTimestamp - currentSlot) < 10 * DateUtils.MINUTE_IN_MILLIS) { 649 log(context, "force align into the nearest slot", currentSlot, null); 650 resultMap.put(currentSlot, upperEntryDataMap); 651 } else { 652 log(context, "in the different booting section", currentSlot, null); 653 resultMap.put(currentSlot, new HashMap<>()); 654 } 655 return; 656 } 657 log(context, "apply interpolation arithmetic", currentSlot, null); 658 final Map<String, BatteryHistEntry> newHistEntryMap = new HashMap<>(); 659 final double timestampLength = upperTimestamp - lowerTimestamp; 660 final double timestampDiff = currentSlot - lowerTimestamp; 661 // Applies interpolation arithmetic for each BatteryHistEntry. 662 for (String entryKey : upperEntryDataMap.keySet()) { 663 final BatteryHistEntry lowerEntry = lowerEntryDataMap.get(entryKey); 664 final BatteryHistEntry upperEntry = upperEntryDataMap.get(entryKey); 665 // Checks whether there is any abnormal battery reset conditions. 666 if (lowerEntry != null) { 667 final boolean invalidForegroundUsageTime = 668 lowerEntry.mForegroundUsageTimeInMs > upperEntry.mForegroundUsageTimeInMs; 669 final boolean invalidBackgroundUsageTime = 670 lowerEntry.mBackgroundUsageTimeInMs > upperEntry.mBackgroundUsageTimeInMs; 671 if (invalidForegroundUsageTime || invalidBackgroundUsageTime) { 672 newHistEntryMap.put(entryKey, upperEntry); 673 log(context, "abnormal reset condition is found", currentSlot, upperEntry); 674 continue; 675 } 676 } 677 final BatteryHistEntry newEntry = 678 BatteryHistEntry.interpolate( 679 currentSlot, 680 upperTimestamp, 681 /*ratio=*/ timestampDiff / timestampLength, 682 lowerEntry, 683 upperEntry); 684 newHistEntryMap.put(entryKey, newEntry); 685 if (lowerEntry == null) { 686 log(context, "cannot find lower entry data", currentSlot, upperEntry); 687 continue; 688 } 689 } 690 resultMap.put(currentSlot, newHistEntryMap); 691 } 692 693 /** 694 * @return Returns the nearest even hour timestamp of the given timestamp. 695 */ getNearestEvenHourTimestamp(long rawTimestamp)696 private static long getNearestEvenHourTimestamp(long rawTimestamp) { 697 // If raw hour is even, the nearest even hour should be the even hour before raw 698 // start. The hour doesn't need to change and just set the minutes and seconds to 0. 699 // Otherwise, the nearest even hour should be raw hour + 1. 700 // For example, the nearest hour of 14:30:50 should be 14:00:00. While the nearest 701 // hour of 15:30:50 should be 16:00:00. 702 return getEvenHourTimestamp(rawTimestamp, /*addHourOfDay*/ 1); 703 } 704 705 /** 706 * @return Returns the last even hour timestamp before the given timestamp. 707 */ getLastEvenHourBeforeTimestamp(long rawTimestamp)708 private static long getLastEvenHourBeforeTimestamp(long rawTimestamp) { 709 // If raw hour is even, the hour doesn't need to change as well. 710 // Otherwise, the even hour before raw end should be raw hour - 1. 711 // For example, the even hour before 14:30:50 should be 14:00:00. While the even 712 // hour before 15:30:50 should be 14:00:00. 713 return getEvenHourTimestamp(rawTimestamp, /*addHourOfDay*/ -1); 714 } 715 getEvenHourTimestamp(long rawTimestamp, int addHourOfDay)716 private static long getEvenHourTimestamp(long rawTimestamp, int addHourOfDay) { 717 final Calendar evenHourCalendar = Calendar.getInstance(); 718 evenHourCalendar.setTimeInMillis(rawTimestamp); 719 // Before computing the evenHourCalendar, record raw hour based on local timezone. 720 final int rawHour = evenHourCalendar.get(Calendar.HOUR_OF_DAY); 721 if (rawHour % 2 != 0) { 722 evenHourCalendar.add(Calendar.HOUR_OF_DAY, addHourOfDay); 723 } 724 evenHourCalendar.set(Calendar.MINUTE, 0); 725 evenHourCalendar.set(Calendar.SECOND, 0); 726 evenHourCalendar.set(Calendar.MILLISECOND, 0); 727 return evenHourCalendar.getTimeInMillis(); 728 } 729 getHourlyTimestamps(final List<Long> dailyTimestamps)730 private static List<List<Long>> getHourlyTimestamps(final List<Long> dailyTimestamps) { 731 final List<List<Long>> hourlyTimestamps = new ArrayList<>(); 732 if (dailyTimestamps.size() < MIN_DAILY_DATA_SIZE) { 733 return hourlyTimestamps; 734 } 735 736 for (int dailyStartIndex = 0; dailyStartIndex < dailyTimestamps.size() - 1; 737 dailyStartIndex++) { 738 long currentTimestamp = dailyTimestamps.get(dailyStartIndex); 739 final long dailyEndTimestamp = dailyTimestamps.get(dailyStartIndex + 1); 740 final List<Long> hourlyTimestampsPerDay = new ArrayList<>(); 741 while (currentTimestamp <= dailyEndTimestamp) { 742 hourlyTimestampsPerDay.add(currentTimestamp); 743 currentTimestamp += MIN_TIME_SLOT; 744 } 745 hourlyTimestamps.add(hourlyTimestampsPerDay); 746 } 747 return hourlyTimestamps; 748 } 749 getHourlyPeriodBatteryLevelData( Context context, final Map<Long, Map<String, BatteryHistEntry>> processedBatteryHistoryMap, final List<List<Long>> timestamps)750 private static List<BatteryLevelData.PeriodBatteryLevelData> getHourlyPeriodBatteryLevelData( 751 Context context, 752 final Map<Long, Map<String, BatteryHistEntry>> processedBatteryHistoryMap, 753 final List<List<Long>> timestamps) { 754 final List<BatteryLevelData.PeriodBatteryLevelData> levelData = new ArrayList<>(); 755 timestamps.forEach( 756 timestampList -> levelData.add( 757 getPeriodBatteryLevelData( 758 context, processedBatteryHistoryMap, timestampList))); 759 return levelData; 760 } 761 getPeriodBatteryLevelData( Context context, final Map<Long, Map<String, BatteryHistEntry>> processedBatteryHistoryMap, final List<Long> timestamps)762 private static BatteryLevelData.PeriodBatteryLevelData getPeriodBatteryLevelData( 763 Context context, 764 final Map<Long, Map<String, BatteryHistEntry>> processedBatteryHistoryMap, 765 final List<Long> timestamps) { 766 final List<Integer> levels = new ArrayList<>(); 767 timestamps.forEach( 768 timestamp -> levels.add(getLevel(context, processedBatteryHistoryMap, timestamp))); 769 return new BatteryLevelData.PeriodBatteryLevelData(timestamps, levels); 770 } 771 getLevel( Context context, final Map<Long, Map<String, BatteryHistEntry>> processedBatteryHistoryMap, final long timestamp)772 private static Integer getLevel( 773 Context context, 774 final Map<Long, Map<String, BatteryHistEntry>> processedBatteryHistoryMap, 775 final long timestamp) { 776 final Map<String, BatteryHistEntry> entryMap = processedBatteryHistoryMap.get(timestamp); 777 if (entryMap == null || entryMap.isEmpty()) { 778 Log.e(TAG, "abnormal entry list in the timestamp:" 779 + utcToLocalTime(context, timestamp)); 780 return null; 781 } 782 // Averages the battery level in each time slot to avoid corner conditions. 783 float batteryLevelCounter = 0; 784 for (BatteryHistEntry entry : entryMap.values()) { 785 batteryLevelCounter += entry.mBatteryLevel; 786 } 787 return Math.round(batteryLevelCounter / entryMap.size()); 788 } 789 insertHourlyUsageDiffData( Context context, final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay, final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap, final Map<Integer, Map<Integer, BatteryDiffData>> resultMap)790 private static void insertHourlyUsageDiffData( 791 Context context, 792 final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay, 793 final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap, 794 final Map<Integer, Map<Integer, BatteryDiffData>> resultMap) { 795 final int currentUserId = context.getUserId(); 796 final UserHandle userHandle = 797 Utils.getManagedProfile(context.getSystemService(UserManager.class)); 798 final int workProfileUserId = 799 userHandle != null ? userHandle.getIdentifier() : Integer.MIN_VALUE; 800 // Each time slot usage diff data = 801 // Math.abs(timestamp[i+2] data - timestamp[i+1] data) + 802 // Math.abs(timestamp[i+1] data - timestamp[i] data); 803 // since we want to aggregate every two hours data into a single time slot. 804 for (int dailyIndex = 0; dailyIndex < hourlyBatteryLevelsPerDay.size(); dailyIndex++) { 805 final Map<Integer, BatteryDiffData> dailyDiffMap = new HashMap<>(); 806 resultMap.put(dailyIndex, dailyDiffMap); 807 if (hourlyBatteryLevelsPerDay.get(dailyIndex) == null) { 808 continue; 809 } 810 final List<Long> timestamps = hourlyBatteryLevelsPerDay.get(dailyIndex).getTimestamps(); 811 for (int hourlyIndex = 0; hourlyIndex < timestamps.size() - 1; hourlyIndex++) { 812 final BatteryDiffData hourlyBatteryDiffData = 813 insertHourlyUsageDiffDataPerSlot( 814 context, 815 currentUserId, 816 workProfileUserId, 817 hourlyIndex, 818 timestamps, 819 batteryHistoryMap); 820 dailyDiffMap.put(hourlyIndex, hourlyBatteryDiffData); 821 } 822 } 823 } 824 insertDailyUsageDiffData( final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay, final Map<Integer, Map<Integer, BatteryDiffData>> resultMap)825 private static void insertDailyUsageDiffData( 826 final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay, 827 final Map<Integer, Map<Integer, BatteryDiffData>> resultMap) { 828 for (int index = 0; index < hourlyBatteryLevelsPerDay.size(); index++) { 829 Map<Integer, BatteryDiffData> dailyUsageMap = resultMap.get(index); 830 if (dailyUsageMap == null) { 831 dailyUsageMap = new HashMap<>(); 832 resultMap.put(index, dailyUsageMap); 833 } 834 dailyUsageMap.put( 835 SELECTED_INDEX_ALL, 836 getAccumulatedUsageDiffData(dailyUsageMap.values())); 837 } 838 } 839 insertAllUsageDiffData( final Map<Integer, Map<Integer, BatteryDiffData>> resultMap)840 private static void insertAllUsageDiffData( 841 final Map<Integer, Map<Integer, BatteryDiffData>> resultMap) { 842 final List<BatteryDiffData> diffDataList = new ArrayList<>(); 843 resultMap.keySet().forEach( 844 key -> diffDataList.add(resultMap.get(key).get(SELECTED_INDEX_ALL))); 845 final Map<Integer, BatteryDiffData> allUsageMap = new HashMap<>(); 846 allUsageMap.put(SELECTED_INDEX_ALL, getAccumulatedUsageDiffData(diffDataList)); 847 resultMap.put(SELECTED_INDEX_ALL, allUsageMap); 848 } 849 850 @Nullable insertHourlyUsageDiffDataPerSlot( Context context, final int currentUserId, final int workProfileUserId, final int currentIndex, final List<Long> timestamps, final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap)851 private static BatteryDiffData insertHourlyUsageDiffDataPerSlot( 852 Context context, 853 final int currentUserId, 854 final int workProfileUserId, 855 final int currentIndex, 856 final List<Long> timestamps, 857 final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) { 858 final List<BatteryDiffEntry> appEntries = new ArrayList<>(); 859 final List<BatteryDiffEntry> systemEntries = new ArrayList<>(); 860 861 final Long currentTimestamp = timestamps.get(currentIndex); 862 final Long nextTimestamp = currentTimestamp + DateUtils.HOUR_IN_MILLIS; 863 final Long nextTwoTimestamp = nextTimestamp + DateUtils.HOUR_IN_MILLIS; 864 // Fetches BatteryHistEntry data from corresponding time slot. 865 final Map<String, BatteryHistEntry> currentBatteryHistMap = 866 batteryHistoryMap.getOrDefault(currentTimestamp, EMPTY_BATTERY_MAP); 867 final Map<String, BatteryHistEntry> nextBatteryHistMap = 868 batteryHistoryMap.getOrDefault(nextTimestamp, EMPTY_BATTERY_MAP); 869 final Map<String, BatteryHistEntry> nextTwoBatteryHistMap = 870 batteryHistoryMap.getOrDefault(nextTwoTimestamp, EMPTY_BATTERY_MAP); 871 // We should not get the empty list since we have at least one fake data to record 872 // the battery level and status in each time slot, the empty list is used to 873 // represent there is no enough data to apply interpolation arithmetic. 874 if (currentBatteryHistMap.isEmpty() 875 || nextBatteryHistMap.isEmpty() 876 || nextTwoBatteryHistMap.isEmpty()) { 877 return null; 878 } 879 880 // Collects all keys in these three time slot records as all populations. 881 final Set<String> allBatteryHistEntryKeys = new ArraySet<>(); 882 allBatteryHistEntryKeys.addAll(currentBatteryHistMap.keySet()); 883 allBatteryHistEntryKeys.addAll(nextBatteryHistMap.keySet()); 884 allBatteryHistEntryKeys.addAll(nextTwoBatteryHistMap.keySet()); 885 886 double totalConsumePower = 0.0; 887 double consumePowerFromOtherUsers = 0f; 888 // Calculates all packages diff usage data in a specific time slot. 889 for (String key : allBatteryHistEntryKeys) { 890 final BatteryHistEntry currentEntry = 891 currentBatteryHistMap.getOrDefault(key, EMPTY_BATTERY_HIST_ENTRY); 892 final BatteryHistEntry nextEntry = 893 nextBatteryHistMap.getOrDefault(key, EMPTY_BATTERY_HIST_ENTRY); 894 final BatteryHistEntry nextTwoEntry = 895 nextTwoBatteryHistMap.getOrDefault(key, EMPTY_BATTERY_HIST_ENTRY); 896 // Cumulative values is a specific time slot for a specific app. 897 long foregroundUsageTimeInMs = 898 getDiffValue( 899 currentEntry.mForegroundUsageTimeInMs, 900 nextEntry.mForegroundUsageTimeInMs, 901 nextTwoEntry.mForegroundUsageTimeInMs); 902 long backgroundUsageTimeInMs = 903 getDiffValue( 904 currentEntry.mBackgroundUsageTimeInMs, 905 nextEntry.mBackgroundUsageTimeInMs, 906 nextTwoEntry.mBackgroundUsageTimeInMs); 907 double consumePower = 908 getDiffValue( 909 currentEntry.mConsumePower, 910 nextEntry.mConsumePower, 911 nextTwoEntry.mConsumePower); 912 // Excludes entry since we don't have enough data to calculate. 913 if (foregroundUsageTimeInMs == 0 914 && backgroundUsageTimeInMs == 0 915 && consumePower == 0) { 916 continue; 917 } 918 final BatteryHistEntry selectedBatteryEntry = 919 selectBatteryHistEntry(currentEntry, nextEntry, nextTwoEntry); 920 if (selectedBatteryEntry == null) { 921 continue; 922 } 923 // Forces refine the cumulative value since it may introduce deviation error since we 924 // will apply the interpolation arithmetic. 925 final float totalUsageTimeInMs = 926 foregroundUsageTimeInMs + backgroundUsageTimeInMs; 927 if (totalUsageTimeInMs > TOTAL_HOURLY_TIME_THRESHOLD) { 928 final float ratio = TOTAL_HOURLY_TIME_THRESHOLD / totalUsageTimeInMs; 929 if (DEBUG) { 930 Log.w(TAG, String.format("abnormal usage time %d|%d for:\n%s", 931 Duration.ofMillis(foregroundUsageTimeInMs).getSeconds(), 932 Duration.ofMillis(backgroundUsageTimeInMs).getSeconds(), 933 currentEntry)); 934 } 935 foregroundUsageTimeInMs = 936 Math.round(foregroundUsageTimeInMs * ratio); 937 backgroundUsageTimeInMs = 938 Math.round(backgroundUsageTimeInMs * ratio); 939 consumePower = consumePower * ratio; 940 } 941 totalConsumePower += consumePower; 942 943 final boolean isFromOtherUsers = isConsumedFromOtherUsers( 944 currentUserId, workProfileUserId, selectedBatteryEntry); 945 if (isFromOtherUsers) { 946 consumePowerFromOtherUsers += consumePower; 947 } else { 948 final BatteryDiffEntry currentBatteryDiffEntry = new BatteryDiffEntry( 949 context, 950 foregroundUsageTimeInMs, 951 backgroundUsageTimeInMs, 952 consumePower, 953 selectedBatteryEntry); 954 if (currentBatteryDiffEntry.isSystemEntry()) { 955 systemEntries.add(currentBatteryDiffEntry); 956 } else { 957 appEntries.add(currentBatteryDiffEntry); 958 } 959 } 960 } 961 if (consumePowerFromOtherUsers != 0) { 962 systemEntries.add(createOtherUsersEntry(context, consumePowerFromOtherUsers)); 963 } 964 965 // If there is no data, return null instead of empty item. 966 if (appEntries.isEmpty() && systemEntries.isEmpty()) { 967 return null; 968 } 969 970 final BatteryDiffData resultDiffData = 971 new BatteryDiffData(appEntries, systemEntries, totalConsumePower); 972 return resultDiffData; 973 } 974 isConsumedFromOtherUsers( final int currentUserId, final int workProfileUserId, final BatteryHistEntry batteryHistEntry)975 private static boolean isConsumedFromOtherUsers( 976 final int currentUserId, 977 final int workProfileUserId, 978 final BatteryHistEntry batteryHistEntry) { 979 return batteryHistEntry.mConsumerType == ConvertUtils.CONSUMER_TYPE_UID_BATTERY 980 && batteryHistEntry.mUserId != currentUserId 981 && batteryHistEntry.mUserId != workProfileUserId; 982 } 983 984 @Nullable getAccumulatedUsageDiffData( final Collection<BatteryDiffData> diffEntryListData)985 private static BatteryDiffData getAccumulatedUsageDiffData( 986 final Collection<BatteryDiffData> diffEntryListData) { 987 double totalConsumePower = 0f; 988 final Map<String, BatteryDiffEntry> diffEntryMap = new HashMap<>(); 989 final List<BatteryDiffEntry> appEntries = new ArrayList<>(); 990 final List<BatteryDiffEntry> systemEntries = new ArrayList<>(); 991 992 for (BatteryDiffData diffEntryList : diffEntryListData) { 993 if (diffEntryList == null) { 994 continue; 995 } 996 for (BatteryDiffEntry entry : diffEntryList.getAppDiffEntryList()) { 997 computeUsageDiffDataPerEntry(entry, diffEntryMap); 998 totalConsumePower += entry.mConsumePower; 999 } 1000 for (BatteryDiffEntry entry : diffEntryList.getSystemDiffEntryList()) { 1001 computeUsageDiffDataPerEntry(entry, diffEntryMap); 1002 totalConsumePower += entry.mConsumePower; 1003 } 1004 } 1005 1006 final Collection<BatteryDiffEntry> diffEntryList = diffEntryMap.values(); 1007 for (BatteryDiffEntry entry : diffEntryList) { 1008 // Sets total daily consume power data into all BatteryDiffEntry. 1009 entry.setTotalConsumePower(totalConsumePower); 1010 if (entry.isSystemEntry()) { 1011 systemEntries.add(entry); 1012 } else { 1013 appEntries.add(entry); 1014 } 1015 } 1016 1017 return diffEntryList.isEmpty() ? null : new BatteryDiffData(appEntries, systemEntries); 1018 } 1019 computeUsageDiffDataPerEntry( final BatteryDiffEntry entry, final Map<String, BatteryDiffEntry> diffEntryMap)1020 private static void computeUsageDiffDataPerEntry( 1021 final BatteryDiffEntry entry, 1022 final Map<String, BatteryDiffEntry> diffEntryMap) { 1023 final String key = entry.mBatteryHistEntry.getKey(); 1024 final BatteryDiffEntry oldBatteryDiffEntry = diffEntryMap.get(key); 1025 // Creates new BatteryDiffEntry if we don't have it. 1026 if (oldBatteryDiffEntry == null) { 1027 diffEntryMap.put(key, entry.clone()); 1028 } else { 1029 // Sums up some field data into the existing one. 1030 oldBatteryDiffEntry.mForegroundUsageTimeInMs += 1031 entry.mForegroundUsageTimeInMs; 1032 oldBatteryDiffEntry.mBackgroundUsageTimeInMs += 1033 entry.mBackgroundUsageTimeInMs; 1034 oldBatteryDiffEntry.mConsumePower += entry.mConsumePower; 1035 } 1036 } 1037 1038 // Removes low percentage data and fake usage data, which will be zero value. purgeLowPercentageAndFakeData( final Context context, final Map<Integer, Map<Integer, BatteryDiffData>> resultMap)1039 private static void purgeLowPercentageAndFakeData( 1040 final Context context, 1041 final Map<Integer, Map<Integer, BatteryDiffData>> resultMap) { 1042 final Set<CharSequence> backgroundUsageTimeHideList = 1043 FeatureFactory.getFactory(context) 1044 .getPowerUsageFeatureProvider(context) 1045 .getHideBackgroundUsageTimeSet(context); 1046 final CharSequence[] notAllowShowEntryPackages = 1047 FeatureFactory.getFactory(context) 1048 .getPowerUsageFeatureProvider(context) 1049 .getHideApplicationEntries(context); 1050 resultMap.keySet().forEach(dailyKey -> { 1051 final Map<Integer, BatteryDiffData> dailyUsageMap = resultMap.get(dailyKey); 1052 dailyUsageMap.values().forEach(diffEntryLists -> { 1053 if (diffEntryLists == null) { 1054 return; 1055 } 1056 purgeLowPercentageAndFakeData( 1057 diffEntryLists.getAppDiffEntryList(), backgroundUsageTimeHideList, 1058 notAllowShowEntryPackages); 1059 purgeLowPercentageAndFakeData( 1060 diffEntryLists.getSystemDiffEntryList(), backgroundUsageTimeHideList, 1061 notAllowShowEntryPackages); 1062 }); 1063 }); 1064 } 1065 purgeLowPercentageAndFakeData( final List<BatteryDiffEntry> entries, final Set<CharSequence> backgroundUsageTimeHideList, final CharSequence[] notAllowShowEntryPackages)1066 private static void purgeLowPercentageAndFakeData( 1067 final List<BatteryDiffEntry> entries, 1068 final Set<CharSequence> backgroundUsageTimeHideList, 1069 final CharSequence[] notAllowShowEntryPackages) { 1070 final Iterator<BatteryDiffEntry> iterator = entries.iterator(); 1071 while (iterator.hasNext()) { 1072 final BatteryDiffEntry entry = iterator.next(); 1073 final String packageName = entry.getPackageName(); 1074 if (entry.getPercentOfTotal() < PERCENTAGE_OF_TOTAL_THRESHOLD 1075 || FAKE_PACKAGE_NAME.equals(packageName) 1076 || contains(packageName, notAllowShowEntryPackages)) { 1077 iterator.remove(); 1078 } 1079 if (packageName != null 1080 && !backgroundUsageTimeHideList.isEmpty() 1081 && contains(packageName, backgroundUsageTimeHideList)) { 1082 entry.mBackgroundUsageTimeInMs = 0; 1083 } 1084 } 1085 } 1086 isUsageMapValid( final Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageMap, final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay)1087 private static boolean isUsageMapValid( 1088 final Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageMap, 1089 final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay) { 1090 if (batteryUsageMap.get(SELECTED_INDEX_ALL) == null 1091 || !batteryUsageMap.get(SELECTED_INDEX_ALL).containsKey(SELECTED_INDEX_ALL)) { 1092 Log.e(TAG, "no [SELECTED_INDEX_ALL][SELECTED_INDEX_ALL] in batteryUsageMap"); 1093 return false; 1094 } 1095 for (int dailyIndex = 0; dailyIndex < hourlyBatteryLevelsPerDay.size(); dailyIndex++) { 1096 if (batteryUsageMap.get(dailyIndex) == null 1097 || !batteryUsageMap.get(dailyIndex).containsKey(SELECTED_INDEX_ALL)) { 1098 Log.e(TAG, "no [" + dailyIndex + "][SELECTED_INDEX_ALL] in batteryUsageMap, " 1099 + "daily size is: " + hourlyBatteryLevelsPerDay.size()); 1100 return false; 1101 } 1102 if (hourlyBatteryLevelsPerDay.get(dailyIndex) == null) { 1103 continue; 1104 } 1105 final List<Long> timestamps = hourlyBatteryLevelsPerDay.get(dailyIndex).getTimestamps(); 1106 // Length of hourly usage map should be the length of hourly level data - 1. 1107 for (int hourlyIndex = 0; hourlyIndex < timestamps.size() - 1; hourlyIndex++) { 1108 if (!batteryUsageMap.get(dailyIndex).containsKey(hourlyIndex)) { 1109 Log.e(TAG, "no [" + dailyIndex + "][" + hourlyIndex + "] in batteryUsageMap, " 1110 + "hourly size is: " + (timestamps.size() - 1)); 1111 return false; 1112 } 1113 } 1114 } 1115 return true; 1116 } 1117 loadLabelAndIcon( @ullable final Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageMap)1118 private static void loadLabelAndIcon( 1119 @Nullable final Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageMap) { 1120 if (batteryUsageMap == null) { 1121 return; 1122 } 1123 // Pre-loads each BatteryDiffEntry relative icon and label for all slots. 1124 final BatteryDiffData batteryUsageMapForAll = 1125 batteryUsageMap.get(SELECTED_INDEX_ALL).get(SELECTED_INDEX_ALL); 1126 if (batteryUsageMapForAll != null) { 1127 batteryUsageMapForAll.getAppDiffEntryList().forEach( 1128 entry -> entry.loadLabelAndIcon()); 1129 batteryUsageMapForAll.getSystemDiffEntryList().forEach( 1130 entry -> entry.loadLabelAndIcon()); 1131 } 1132 } 1133 getTimestampWithDayDiff(final long timestamp, final int dayDiff)1134 private static long getTimestampWithDayDiff(final long timestamp, final int dayDiff) { 1135 final Calendar calendar = Calendar.getInstance(); 1136 calendar.setTimeInMillis(timestamp); 1137 calendar.add(Calendar.DAY_OF_YEAR, dayDiff); 1138 calendar.set(Calendar.HOUR_OF_DAY, 0); 1139 calendar.set(Calendar.MINUTE, 0); 1140 calendar.set(Calendar.SECOND, 0); 1141 return calendar.getTimeInMillis(); 1142 } 1143 getCountOfApps(final Map<Integer, Map<Integer, BatteryDiffData>> resultMap)1144 private static int getCountOfApps(final Map<Integer, Map<Integer, BatteryDiffData>> resultMap) { 1145 final BatteryDiffData diffDataList = 1146 resultMap.get(SELECTED_INDEX_ALL).get(SELECTED_INDEX_ALL); 1147 return diffDataList == null 1148 ? 0 1149 : diffDataList.getAppDiffEntryList().size() 1150 + diffDataList.getSystemDiffEntryList().size(); 1151 } 1152 contains(String target, Set<CharSequence> packageNames)1153 private static boolean contains(String target, Set<CharSequence> packageNames) { 1154 if (target != null && packageNames != null) { 1155 for (CharSequence packageName : packageNames) { 1156 if (TextUtils.equals(target, packageName)) { 1157 return true; 1158 } 1159 } 1160 } 1161 return false; 1162 } 1163 getDiffValue(long v1, long v2, long v3)1164 private static long getDiffValue(long v1, long v2, long v3) { 1165 return (v2 > v1 ? v2 - v1 : 0) + (v3 > v2 ? v3 - v2 : 0); 1166 } 1167 getDiffValue(double v1, double v2, double v3)1168 private static double getDiffValue(double v1, double v2, double v3) { 1169 return (v2 > v1 ? v2 - v1 : 0) + (v3 > v2 ? v3 - v2 : 0); 1170 } 1171 1172 @Nullable selectBatteryHistEntry( final BatteryHistEntry... batteryHistEntries)1173 private static BatteryHistEntry selectBatteryHistEntry( 1174 final BatteryHistEntry... batteryHistEntries) { 1175 for (BatteryHistEntry entry : batteryHistEntries) { 1176 if (entry != null && entry != EMPTY_BATTERY_HIST_ENTRY) { 1177 return entry; 1178 } 1179 } 1180 return null; 1181 } 1182 createOtherUsersEntry( Context context, final double consumePower)1183 private static BatteryDiffEntry createOtherUsersEntry( 1184 Context context, final double consumePower) { 1185 final ContentValues values = new ContentValues(); 1186 values.put(BatteryHistEntry.KEY_UID, BatteryUtils.UID_OTHER_USERS); 1187 values.put(BatteryHistEntry.KEY_USER_ID, BatteryUtils.UID_OTHER_USERS); 1188 values.put(BatteryHistEntry.KEY_CONSUMER_TYPE, ConvertUtils.CONSUMER_TYPE_UID_BATTERY); 1189 // We will show the percentage for the "other users" item only, the aggregated 1190 // running time information is useless for users to identify individual apps. 1191 final BatteryDiffEntry batteryDiffEntry = new BatteryDiffEntry( 1192 context, 1193 /*foregroundUsageTimeInMs=*/ 0, 1194 /*backgroundUsageTimeInMs=*/ 0, 1195 consumePower, 1196 new BatteryHistEntry(values)); 1197 return batteryDiffEntry; 1198 } 1199 logAppCountMetrics( Context context, final int countOfAppBeforePurge, final int countOfAppAfterPurge)1200 private static void logAppCountMetrics( 1201 Context context, final int countOfAppBeforePurge, final int countOfAppAfterPurge) { 1202 context = context.getApplicationContext(); 1203 final MetricsFeatureProvider metricsFeatureProvider = 1204 FeatureFactory.getFactory(context).getMetricsFeatureProvider(); 1205 metricsFeatureProvider.action( 1206 context, 1207 SettingsEnums.ACTION_BATTERY_USAGE_SHOWN_APP_COUNT, 1208 countOfAppAfterPurge); 1209 metricsFeatureProvider.action( 1210 context, 1211 SettingsEnums.ACTION_BATTERY_USAGE_HIDDEN_APP_COUNT, 1212 countOfAppBeforePurge - countOfAppAfterPurge); 1213 } 1214 log(Context context, final String content, final long timestamp, final BatteryHistEntry entry)1215 private static void log(Context context, final String content, final long timestamp, 1216 final BatteryHistEntry entry) { 1217 if (DEBUG) { 1218 Log.d(TAG, String.format(entry != null ? "%s %s:\n%s" : "%s %s:%s", 1219 utcToLocalTime(context, timestamp), content, entry)); 1220 } 1221 } 1222 1223 // Compute diff map and loads all items (icon and label) in the background. 1224 private static class ComputeUsageMapAndLoadItemsTask 1225 extends AsyncTask<Void, Void, Map<Integer, Map<Integer, BatteryDiffData>>> { 1226 1227 Context mApplicationContext; 1228 final Handler mHandler; 1229 final UsageMapAsyncResponse mAsyncResponseDelegate; 1230 private List<BatteryLevelData.PeriodBatteryLevelData> mHourlyBatteryLevelsPerDay; 1231 private Map<Long, Map<String, BatteryHistEntry>> mBatteryHistoryMap; 1232 ComputeUsageMapAndLoadItemsTask( Context context, Handler handler, final UsageMapAsyncResponse asyncResponseDelegate, final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay, final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap)1233 private ComputeUsageMapAndLoadItemsTask( 1234 Context context, 1235 Handler handler, 1236 final UsageMapAsyncResponse asyncResponseDelegate, 1237 final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay, 1238 final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) { 1239 mApplicationContext = context.getApplicationContext(); 1240 mHandler = handler; 1241 mAsyncResponseDelegate = asyncResponseDelegate; 1242 mHourlyBatteryLevelsPerDay = hourlyBatteryLevelsPerDay; 1243 mBatteryHistoryMap = batteryHistoryMap; 1244 } 1245 1246 @Override doInBackground(Void... voids)1247 protected Map<Integer, Map<Integer, BatteryDiffData>> doInBackground(Void... voids) { 1248 if (mApplicationContext == null 1249 || mHandler == null 1250 || mAsyncResponseDelegate == null 1251 || mBatteryHistoryMap == null 1252 || mHourlyBatteryLevelsPerDay == null) { 1253 Log.e(TAG, "invalid input for ComputeUsageMapAndLoadItemsTask()"); 1254 return null; 1255 } 1256 final long startTime = System.currentTimeMillis(); 1257 final Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageMap = 1258 getBatteryUsageMap( 1259 mApplicationContext, mHourlyBatteryLevelsPerDay, mBatteryHistoryMap); 1260 loadLabelAndIcon(batteryUsageMap); 1261 Log.d(TAG, String.format("execute ComputeUsageMapAndLoadItemsTask in %d/ms", 1262 (System.currentTimeMillis() - startTime))); 1263 return batteryUsageMap; 1264 } 1265 1266 @Override onPostExecute( final Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageMap)1267 protected void onPostExecute( 1268 final Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageMap) { 1269 mApplicationContext = null; 1270 mHourlyBatteryLevelsPerDay = null; 1271 mBatteryHistoryMap = null; 1272 // Post results back to main thread to refresh UI. 1273 if (mHandler != null && mAsyncResponseDelegate != null) { 1274 mHandler.post(() -> { 1275 mAsyncResponseDelegate.onBatteryUsageMapLoaded(batteryUsageMap); 1276 }); 1277 } 1278 } 1279 } 1280 1281 // Loads battery usage data from battery stats service directly and loads all items (icon and 1282 // label) in the background. 1283 private static final class LoadUsageMapFromBatteryStatsServiceTask 1284 extends ComputeUsageMapAndLoadItemsTask { 1285 LoadUsageMapFromBatteryStatsServiceTask( Context context, Handler handler, final UsageMapAsyncResponse asyncResponseDelegate)1286 private LoadUsageMapFromBatteryStatsServiceTask( 1287 Context context, 1288 Handler handler, 1289 final UsageMapAsyncResponse asyncResponseDelegate) { 1290 super(context, handler, asyncResponseDelegate, /*hourlyBatteryLevelsPerDay=*/ null, 1291 /*batteryHistoryMap=*/ null); 1292 } 1293 1294 @Override doInBackground(Void... voids)1295 protected Map<Integer, Map<Integer, BatteryDiffData>> doInBackground(Void... voids) { 1296 if (mApplicationContext == null 1297 || mHandler == null 1298 || mAsyncResponseDelegate == null) { 1299 Log.e(TAG, "invalid input for ComputeUsageMapAndLoadItemsTask()"); 1300 return null; 1301 } 1302 final long startTime = System.currentTimeMillis(); 1303 final Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageMap = 1304 getBatteryUsageMapFromStatsService(mApplicationContext); 1305 loadLabelAndIcon(batteryUsageMap); 1306 Log.d(TAG, String.format("execute LoadUsageMapFromBatteryStatsServiceTask in %d/ms", 1307 (System.currentTimeMillis() - startTime))); 1308 return batteryUsageMap; 1309 } 1310 } 1311 } 1312