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