• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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