• 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 package com.android.settings.fuelgauge.batteryusage;
17 
18 import android.annotation.IntDef;
19 import android.content.ContentValues;
20 import android.content.Context;
21 import android.os.BatteryUsageStats;
22 import android.os.LocaleList;
23 import android.os.UserHandle;
24 import android.os.UserManager;
25 import android.text.format.DateFormat;
26 import android.text.format.DateUtils;
27 import android.util.ArraySet;
28 import android.util.Log;
29 
30 import androidx.annotation.VisibleForTesting;
31 
32 import com.android.settings.Utils;
33 import com.android.settings.fuelgauge.BatteryUtils;
34 import com.android.settings.overlay.FeatureFactory;
35 
36 import java.lang.annotation.Retention;
37 import java.lang.annotation.RetentionPolicy;
38 import java.time.Duration;
39 import java.util.ArrayList;
40 import java.util.HashMap;
41 import java.util.Iterator;
42 import java.util.List;
43 import java.util.Locale;
44 import java.util.Map;
45 import java.util.Set;
46 import java.util.TimeZone;
47 
48 /** A utility class to convert data into another types. */
49 public final class ConvertUtils {
50     private static final boolean DEBUG = false;
51     private static final String TAG = "ConvertUtils";
52     private static final Map<String, BatteryHistEntry> EMPTY_BATTERY_MAP = new HashMap<>();
53     private static final BatteryHistEntry EMPTY_BATTERY_HIST_ENTRY =
54             new BatteryHistEntry(new ContentValues());
55     // Maximum total time value for each slot cumulative data at most 2 hours.
56     private static final float TOTAL_TIME_THRESHOLD = DateUtils.HOUR_IN_MILLIS * 2;
57 
58     @VisibleForTesting
59     static double PERCENTAGE_OF_TOTAL_THRESHOLD = 1f;
60 
61     /** Invalid system battery consumer drain type. */
62     public static final int INVALID_DRAIN_TYPE = -1;
63     /** A fake package name to represent no BatteryEntry data. */
64     public static final String FAKE_PACKAGE_NAME = "fake_package";
65 
66     @IntDef(prefix = {"CONSUMER_TYPE"}, value = {
67             CONSUMER_TYPE_UNKNOWN,
68             CONSUMER_TYPE_UID_BATTERY,
69             CONSUMER_TYPE_USER_BATTERY,
70             CONSUMER_TYPE_SYSTEM_BATTERY,
71     })
72     @Retention(RetentionPolicy.SOURCE)
73     public static @interface ConsumerType {
74     }
75 
76     public static final int CONSUMER_TYPE_UNKNOWN = 0;
77     public static final int CONSUMER_TYPE_UID_BATTERY = 1;
78     public static final int CONSUMER_TYPE_USER_BATTERY = 2;
79     public static final int CONSUMER_TYPE_SYSTEM_BATTERY = 3;
80 
ConvertUtils()81     private ConvertUtils() {
82     }
83 
84     /** Converts to content values */
convertToContentValues( BatteryEntry entry, BatteryUsageStats batteryUsageStats, int batteryLevel, int batteryStatus, int batteryHealth, long bootTimestamp, long timestamp)85     public static ContentValues convertToContentValues(
86             BatteryEntry entry,
87             BatteryUsageStats batteryUsageStats,
88             int batteryLevel,
89             int batteryStatus,
90             int batteryHealth,
91             long bootTimestamp,
92             long timestamp) {
93         final ContentValues values = new ContentValues();
94         if (entry != null && batteryUsageStats != null) {
95             values.put(BatteryHistEntry.KEY_UID, Long.valueOf(entry.getUid()));
96             values.put(BatteryHistEntry.KEY_USER_ID,
97                     Long.valueOf(UserHandle.getUserId(entry.getUid())));
98             values.put(BatteryHistEntry.KEY_APP_LABEL, entry.getLabel());
99             values.put(BatteryHistEntry.KEY_PACKAGE_NAME,
100                     entry.getDefaultPackageName());
101             values.put(BatteryHistEntry.KEY_IS_HIDDEN, Boolean.valueOf(entry.isHidden()));
102             values.put(BatteryHistEntry.KEY_TOTAL_POWER,
103                     Double.valueOf(batteryUsageStats.getConsumedPower()));
104             values.put(BatteryHistEntry.KEY_CONSUME_POWER,
105                     Double.valueOf(entry.getConsumedPower()));
106             values.put(BatteryHistEntry.KEY_PERCENT_OF_TOTAL,
107                     Double.valueOf(entry.mPercent));
108             values.put(BatteryHistEntry.KEY_FOREGROUND_USAGE_TIME,
109                     Long.valueOf(entry.getTimeInForegroundMs()));
110             values.put(BatteryHistEntry.KEY_BACKGROUND_USAGE_TIME,
111                     Long.valueOf(entry.getTimeInBackgroundMs()));
112             values.put(BatteryHistEntry.KEY_DRAIN_TYPE,
113                     Integer.valueOf(entry.getPowerComponentId()));
114             values.put(BatteryHistEntry.KEY_CONSUMER_TYPE,
115                     Integer.valueOf(entry.getConsumerType()));
116         } else {
117             values.put(BatteryHistEntry.KEY_PACKAGE_NAME, FAKE_PACKAGE_NAME);
118         }
119         values.put(BatteryHistEntry.KEY_BOOT_TIMESTAMP, Long.valueOf(bootTimestamp));
120         values.put(BatteryHistEntry.KEY_TIMESTAMP, Long.valueOf(timestamp));
121         values.put(BatteryHistEntry.KEY_ZONE_ID, TimeZone.getDefault().getID());
122         values.put(BatteryHistEntry.KEY_BATTERY_LEVEL, Integer.valueOf(batteryLevel));
123         values.put(BatteryHistEntry.KEY_BATTERY_STATUS, Integer.valueOf(batteryStatus));
124         values.put(BatteryHistEntry.KEY_BATTERY_HEALTH, Integer.valueOf(batteryHealth));
125         return values;
126     }
127 
128     /** Converts to {@link BatteryHistEntry} */
convertToBatteryHistEntry( BatteryEntry entry, BatteryUsageStats batteryUsageStats)129     public static BatteryHistEntry convertToBatteryHistEntry(
130             BatteryEntry entry,
131             BatteryUsageStats batteryUsageStats) {
132         return new BatteryHistEntry(
133                 convertToContentValues(
134                         entry,
135                         batteryUsageStats,
136                         /*batteryLevel=*/ 0,
137                         /*batteryStatus=*/ 0,
138                         /*batteryHealth=*/ 0,
139                         /*bootTimestamp=*/ 0,
140                         /*timestamp=*/ 0));
141     }
142 
143     /** Converts UTC timestamp to human readable local time string. */
utcToLocalTime(Context context, long timestamp)144     public static String utcToLocalTime(Context context, long timestamp) {
145         final Locale locale = getLocale(context);
146         final String pattern =
147                 DateFormat.getBestDateTimePattern(locale, "MMM dd,yyyy HH:mm:ss");
148         return DateFormat.format(pattern, timestamp).toString();
149     }
150 
151     /** Converts UTC timestamp to local time hour data. */
utcToLocalTimeHour( final Context context, final long timestamp, final boolean is24HourFormat)152     public static String utcToLocalTimeHour(
153             final Context context, final long timestamp, final boolean is24HourFormat) {
154         final Locale locale = getLocale(context);
155         // e.g. for 12-hour format: 9 PM
156         // e.g. for 24-hour format: 09:00
157         final String skeleton = is24HourFormat ? "HHm" : "ha";
158         final String pattern = DateFormat.getBestDateTimePattern(locale, skeleton);
159         return DateFormat.format(pattern, timestamp).toString();
160     }
161 
162     /** Converts UTC timestamp to local time day of week data. */
utcToLocalTimeDayOfWeek( final Context context, final long timestamp, final boolean isAbbreviation)163     public static String utcToLocalTimeDayOfWeek(
164             final Context context, final long timestamp, final boolean isAbbreviation) {
165         final Locale locale = getLocale(context);
166         final String pattern = DateFormat.getBestDateTimePattern(locale,
167                 isAbbreviation ? "E" : "EEEE");
168         return DateFormat.format(pattern, timestamp).toString();
169     }
170 
171     /** Gets indexed battery usage data for each corresponding time slot. */
getIndexedUsageMap( final Context context, final int timeSlotSize, final long[] batteryHistoryKeys, final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap, final boolean purgeLowPercentageAndFakeData)172     public static Map<Integer, List<BatteryDiffEntry>> getIndexedUsageMap(
173             final Context context,
174             final int timeSlotSize,
175             final long[] batteryHistoryKeys,
176             final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap,
177             final boolean purgeLowPercentageAndFakeData) {
178         if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) {
179             return new HashMap<>();
180         }
181         final Map<Integer, List<BatteryDiffEntry>> resultMap = new HashMap<>();
182         // Each time slot usage diff data =
183         //     Math.abs(timestamp[i+2] data - timestamp[i+1] data) +
184         //     Math.abs(timestamp[i+1] data - timestamp[i] data);
185         // since we want to aggregate every two hours data into a single time slot.
186         final int timestampStride = 2;
187         for (int index = 0; index < timeSlotSize; index++) {
188             final Long currentTimestamp =
189                     Long.valueOf(batteryHistoryKeys[index * timestampStride]);
190             final Long nextTimestamp =
191                     Long.valueOf(batteryHistoryKeys[index * timestampStride + 1]);
192             final Long nextTwoTimestamp =
193                     Long.valueOf(batteryHistoryKeys[index * timestampStride + 2]);
194             // Fetches BatteryHistEntry data from corresponding time slot.
195             final Map<String, BatteryHistEntry> currentBatteryHistMap =
196                     batteryHistoryMap.getOrDefault(currentTimestamp, EMPTY_BATTERY_MAP);
197             final Map<String, BatteryHistEntry> nextBatteryHistMap =
198                     batteryHistoryMap.getOrDefault(nextTimestamp, EMPTY_BATTERY_MAP);
199             final Map<String, BatteryHistEntry> nextTwoBatteryHistMap =
200                     batteryHistoryMap.getOrDefault(nextTwoTimestamp, EMPTY_BATTERY_MAP);
201             // We should not get the empty list since we have at least one fake data to record
202             // the battery level and status in each time slot, the empty list is used to
203             // represent there is no enough data to apply interpolation arithmetic.
204             if (currentBatteryHistMap.isEmpty()
205                     || nextBatteryHistMap.isEmpty()
206                     || nextTwoBatteryHistMap.isEmpty()) {
207                 resultMap.put(Integer.valueOf(index), new ArrayList<BatteryDiffEntry>());
208                 continue;
209             }
210 
211             // Collects all keys in these three time slot records as all populations.
212             final Set<String> allBatteryHistEntryKeys = new ArraySet<>();
213             allBatteryHistEntryKeys.addAll(currentBatteryHistMap.keySet());
214             allBatteryHistEntryKeys.addAll(nextBatteryHistMap.keySet());
215             allBatteryHistEntryKeys.addAll(nextTwoBatteryHistMap.keySet());
216 
217             double totalConsumePower = 0.0;
218             final List<BatteryDiffEntry> batteryDiffEntryList = new ArrayList<>();
219             // Adds a specific time slot BatteryDiffEntry list into result map.
220             resultMap.put(Integer.valueOf(index), batteryDiffEntryList);
221 
222             // Calculates all packages diff usage data in a specific time slot.
223             for (String key : allBatteryHistEntryKeys) {
224                 final BatteryHistEntry currentEntry =
225                         currentBatteryHistMap.getOrDefault(key, EMPTY_BATTERY_HIST_ENTRY);
226                 final BatteryHistEntry nextEntry =
227                         nextBatteryHistMap.getOrDefault(key, EMPTY_BATTERY_HIST_ENTRY);
228                 final BatteryHistEntry nextTwoEntry =
229                         nextTwoBatteryHistMap.getOrDefault(key, EMPTY_BATTERY_HIST_ENTRY);
230                 // Cumulative values is a specific time slot for a specific app.
231                 long foregroundUsageTimeInMs =
232                         getDiffValue(
233                                 currentEntry.mForegroundUsageTimeInMs,
234                                 nextEntry.mForegroundUsageTimeInMs,
235                                 nextTwoEntry.mForegroundUsageTimeInMs);
236                 long backgroundUsageTimeInMs =
237                         getDiffValue(
238                                 currentEntry.mBackgroundUsageTimeInMs,
239                                 nextEntry.mBackgroundUsageTimeInMs,
240                                 nextTwoEntry.mBackgroundUsageTimeInMs);
241                 double consumePower =
242                         getDiffValue(
243                                 currentEntry.mConsumePower,
244                                 nextEntry.mConsumePower,
245                                 nextTwoEntry.mConsumePower);
246                 // Excludes entry since we don't have enough data to calculate.
247                 if (foregroundUsageTimeInMs == 0
248                         && backgroundUsageTimeInMs == 0
249                         && consumePower == 0) {
250                     continue;
251                 }
252                 final BatteryHistEntry selectedBatteryEntry =
253                         selectBatteryHistEntry(currentEntry, nextEntry, nextTwoEntry);
254                 if (selectedBatteryEntry == null) {
255                     continue;
256                 }
257                 // Forces refine the cumulative value since it may introduce deviation
258                 // error since we will apply the interpolation arithmetic.
259                 final float totalUsageTimeInMs =
260                         foregroundUsageTimeInMs + backgroundUsageTimeInMs;
261                 if (totalUsageTimeInMs > TOTAL_TIME_THRESHOLD) {
262                     final float ratio = TOTAL_TIME_THRESHOLD / totalUsageTimeInMs;
263                     if (DEBUG) {
264                         Log.w(TAG, String.format("abnormal usage time %d|%d for:\n%s",
265                                 Duration.ofMillis(foregroundUsageTimeInMs).getSeconds(),
266                                 Duration.ofMillis(backgroundUsageTimeInMs).getSeconds(),
267                                 currentEntry));
268                     }
269                     foregroundUsageTimeInMs =
270                             Math.round(foregroundUsageTimeInMs * ratio);
271                     backgroundUsageTimeInMs =
272                             Math.round(backgroundUsageTimeInMs * ratio);
273                     consumePower = consumePower * ratio;
274                 }
275                 totalConsumePower += consumePower;
276                 batteryDiffEntryList.add(
277                         new BatteryDiffEntry(
278                                 context,
279                                 foregroundUsageTimeInMs,
280                                 backgroundUsageTimeInMs,
281                                 consumePower,
282                                 selectedBatteryEntry));
283             }
284             // Sets total consume power data into all BatteryDiffEntry in the same slot.
285             for (BatteryDiffEntry diffEntry : batteryDiffEntryList) {
286                 diffEntry.setTotalConsumePower(totalConsumePower);
287             }
288         }
289         insert24HoursData(BatteryChartViewModel.SELECTED_INDEX_ALL, resultMap);
290         resolveMultiUsersData(context, resultMap);
291         if (purgeLowPercentageAndFakeData) {
292             purgeLowPercentageAndFakeData(context, resultMap);
293         }
294         return resultMap;
295     }
296 
297     @VisibleForTesting
resolveMultiUsersData( final Context context, final Map<Integer, List<BatteryDiffEntry>> indexedUsageMap)298     static void resolveMultiUsersData(
299             final Context context,
300             final Map<Integer, List<BatteryDiffEntry>> indexedUsageMap) {
301         final int currentUserId = context.getUserId();
302         final UserHandle userHandle =
303                 Utils.getManagedProfile(context.getSystemService(UserManager.class));
304         final int workProfileUserId =
305                 userHandle != null ? userHandle.getIdentifier() : Integer.MIN_VALUE;
306         // Loops for all BatteryDiffEntry in the different slots.
307         for (List<BatteryDiffEntry> entryList : indexedUsageMap.values()) {
308             double consumePowerFromOtherUsers = 0f;
309             double consumePercentageFromOtherUsers = 0f;
310             final Iterator<BatteryDiffEntry> iterator = entryList.iterator();
311             while (iterator.hasNext()) {
312                 final BatteryDiffEntry entry = iterator.next();
313                 final BatteryHistEntry batteryHistEntry = entry.mBatteryHistEntry;
314                 if (batteryHistEntry.mConsumerType != CONSUMER_TYPE_UID_BATTERY) {
315                     continue;
316                 }
317                 // Whether the BatteryHistEntry represents the current user data?
318                 if (batteryHistEntry.mUserId == currentUserId
319                         || batteryHistEntry.mUserId == workProfileUserId) {
320                     continue;
321                 }
322                 // Removes and aggregates non-current users data from the list.
323                 iterator.remove();
324                 consumePowerFromOtherUsers += entry.mConsumePower;
325                 consumePercentageFromOtherUsers += entry.getPercentOfTotal();
326             }
327             if (consumePercentageFromOtherUsers != 0) {
328                 entryList.add(createOtherUsersEntry(context, consumePowerFromOtherUsers,
329                         consumePercentageFromOtherUsers));
330             }
331         }
332     }
333 
insert24HoursData( final int desiredIndex, final Map<Integer, List<BatteryDiffEntry>> indexedUsageMap)334     private static void insert24HoursData(
335             final int desiredIndex,
336             final Map<Integer, List<BatteryDiffEntry>> indexedUsageMap) {
337         final Map<String, BatteryDiffEntry> resultMap = new HashMap<>();
338         double totalConsumePower = 0f;
339         // Loops for all BatteryDiffEntry and aggregate them together.
340         for (List<BatteryDiffEntry> entryList : indexedUsageMap.values()) {
341             for (BatteryDiffEntry entry : entryList) {
342                 final String key = entry.mBatteryHistEntry.getKey();
343                 final BatteryDiffEntry oldBatteryDiffEntry = resultMap.get(key);
344                 // Creates new BatteryDiffEntry if we don't have it.
345                 if (oldBatteryDiffEntry == null) {
346                     resultMap.put(key, entry.clone());
347                 } else {
348                     // Sums up some fields data into the existing one.
349                     oldBatteryDiffEntry.mForegroundUsageTimeInMs +=
350                             entry.mForegroundUsageTimeInMs;
351                     oldBatteryDiffEntry.mBackgroundUsageTimeInMs +=
352                             entry.mBackgroundUsageTimeInMs;
353                     oldBatteryDiffEntry.mConsumePower += entry.mConsumePower;
354                 }
355                 totalConsumePower += entry.mConsumePower;
356             }
357         }
358         final List<BatteryDiffEntry> resultList = new ArrayList<>(resultMap.values());
359         // Sets total 24 hours consume power data into all BatteryDiffEntry.
360         for (BatteryDiffEntry entry : resultList) {
361             entry.setTotalConsumePower(totalConsumePower);
362         }
363         indexedUsageMap.put(Integer.valueOf(desiredIndex), resultList);
364     }
365 
366     // Removes low percentage data and fake usage data, which will be zero value.
purgeLowPercentageAndFakeData( final Context context, final Map<Integer, List<BatteryDiffEntry>> indexedUsageMap)367     private static void purgeLowPercentageAndFakeData(
368             final Context context,
369             final Map<Integer, List<BatteryDiffEntry>> indexedUsageMap) {
370         final Set<CharSequence> backgroundUsageTimeHideList =
371                 FeatureFactory.getFactory(context)
372                         .getPowerUsageFeatureProvider(context)
373                         .getHideBackgroundUsageTimeSet(context);
374         for (List<BatteryDiffEntry> entries : indexedUsageMap.values()) {
375             final Iterator<BatteryDiffEntry> iterator = entries.iterator();
376             while (iterator.hasNext()) {
377                 final BatteryDiffEntry entry = iterator.next();
378                 if (entry.getPercentOfTotal() < PERCENTAGE_OF_TOTAL_THRESHOLD
379                         || FAKE_PACKAGE_NAME.equals(entry.getPackageName())) {
380                     iterator.remove();
381                 }
382                 final String packageName = entry.getPackageName();
383                 if (packageName != null
384                         && !backgroundUsageTimeHideList.isEmpty()
385                         && backgroundUsageTimeHideList.contains(packageName)) {
386                     entry.mBackgroundUsageTimeInMs = 0;
387                 }
388             }
389         }
390     }
391 
getDiffValue(long v1, long v2, long v3)392     private static long getDiffValue(long v1, long v2, long v3) {
393         return (v2 > v1 ? v2 - v1 : 0) + (v3 > v2 ? v3 - v2 : 0);
394     }
395 
getDiffValue(double v1, double v2, double v3)396     private static double getDiffValue(double v1, double v2, double v3) {
397         return (v2 > v1 ? v2 - v1 : 0) + (v3 > v2 ? v3 - v2 : 0);
398     }
399 
selectBatteryHistEntry( BatteryHistEntry entry1, BatteryHistEntry entry2, BatteryHistEntry entry3)400     private static BatteryHistEntry selectBatteryHistEntry(
401             BatteryHistEntry entry1,
402             BatteryHistEntry entry2,
403             BatteryHistEntry entry3) {
404         if (entry1 != null && entry1 != EMPTY_BATTERY_HIST_ENTRY) {
405             return entry1;
406         } else if (entry2 != null && entry2 != EMPTY_BATTERY_HIST_ENTRY) {
407             return entry2;
408         } else {
409             return entry3 != null && entry3 != EMPTY_BATTERY_HIST_ENTRY
410                     ? entry3 : null;
411         }
412     }
413 
414     @VisibleForTesting
getLocale(Context context)415     static Locale getLocale(Context context) {
416         if (context == null) {
417             return Locale.getDefault();
418         }
419         final LocaleList locales =
420                 context.getResources().getConfiguration().getLocales();
421         return locales != null && !locales.isEmpty() ? locales.get(0)
422                 : Locale.getDefault();
423     }
424 
createOtherUsersEntry( Context context, double consumePower, double consumePercentage)425     private static BatteryDiffEntry createOtherUsersEntry(
426             Context context, double consumePower, double consumePercentage) {
427         final ContentValues values = new ContentValues();
428         values.put(BatteryHistEntry.KEY_UID, BatteryUtils.UID_OTHER_USERS);
429         values.put(BatteryHistEntry.KEY_USER_ID, BatteryUtils.UID_OTHER_USERS);
430         values.put(BatteryHistEntry.KEY_CONSUMER_TYPE, CONSUMER_TYPE_UID_BATTERY);
431         // We will show the percentage for the "other users" item only, the aggregated
432         // running time information is useless for users to identify individual apps.
433         final BatteryDiffEntry batteryDiffEntry = new BatteryDiffEntry(
434                 context,
435                 /*foregroundUsageTimeInMs=*/ 0,
436                 /*backgroundUsageTimeInMs=*/ 0,
437                 consumePower,
438                 new BatteryHistEntry(values));
439         batteryDiffEntry.setTotalConsumePower(100 * consumePower / consumePercentage);
440         return batteryDiffEntry;
441     }
442 }
443