• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5  * except in compliance with the License. You may obtain a copy of the License at
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the
10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11  * KIND, either express or implied. See the License for the specific language governing
12  * permissions and limitations under the License.
13  */
14 package com.android.settings.fuelgauge;
15 
16 import android.annotation.IntDef;
17 import android.content.ContentValues;
18 import android.content.Context;
19 import android.os.BatteryUsageStats;
20 import android.os.LocaleList;
21 import android.os.UserHandle;
22 import android.text.format.DateUtils;
23 import android.util.Log;
24 
25 import androidx.annotation.VisibleForTesting;
26 
27 import java.lang.annotation.Retention;
28 import java.lang.annotation.RetentionPolicy;
29 import java.text.SimpleDateFormat;
30 import java.time.Duration;
31 import java.util.ArrayList;
32 import java.util.Date;
33 import java.util.HashMap;
34 import java.util.HashSet;
35 import java.util.Iterator;
36 import java.util.List;
37 import java.util.Locale;
38 import java.util.Map;
39 import java.util.Set;
40 import java.util.TimeZone;
41 
42 /** A utility class to convert data into another types. */
43 public final class ConvertUtils {
44     private static final boolean DEBUG = false;
45     private static final String TAG = "ConvertUtils";
46     private static final Map<String, BatteryHistEntry> EMPTY_BATTERY_MAP = new HashMap<>();
47     private static final BatteryHistEntry EMPTY_BATTERY_HIST_ENTRY =
48         new BatteryHistEntry(new ContentValues());
49     // Maximum total time value for each slot cumulative data at most 2 hours.
50     private static final float TOTAL_TIME_THRESHOLD = DateUtils.HOUR_IN_MILLIS * 2;
51 
52     // Keys for metric metadata.
53     static final int METRIC_KEY_PACKAGE = 1;
54     static final int METRIC_KEY_BATTERY_LEVEL = 2;
55     static final int METRIC_KEY_BATTERY_USAGE = 3;
56 
57     @VisibleForTesting
58     static double PERCENTAGE_OF_TOTAL_THRESHOLD = 1f;
59 
60     /** Invalid system battery consumer drain type. */
61     public static final int INVALID_DRAIN_TYPE = -1;
62     /** A fake package name to represent no BatteryEntry data. */
63     public static final String FAKE_PACKAGE_NAME = "fake_package";
64 
65     @IntDef(prefix = {"CONSUMER_TYPE"}, value = {
66         CONSUMER_TYPE_UNKNOWN,
67         CONSUMER_TYPE_UID_BATTERY,
68         CONSUMER_TYPE_USER_BATTERY,
69         CONSUMER_TYPE_SYSTEM_BATTERY,
70     })
71     @Retention(RetentionPolicy.SOURCE)
72     public static @interface ConsumerType {}
73 
74     public static final int CONSUMER_TYPE_UNKNOWN = 0;
75     public static final int CONSUMER_TYPE_UID_BATTERY = 1;
76     public static final int CONSUMER_TYPE_USER_BATTERY = 2;
77     public static final int CONSUMER_TYPE_SYSTEM_BATTERY = 3;
78 
79     // For language is changed.
80     @VisibleForTesting static Locale sLocale;
81     @VisibleForTesting static Locale sLocaleForHour;
82     // For time zone is changed.
83     @VisibleForTesting static String sZoneId;
84     @VisibleForTesting static String sZoneIdForHour;
85     private static boolean sIs24HourFormat;
86 
87     @VisibleForTesting
88     static SimpleDateFormat sSimpleDateFormat;
89     @VisibleForTesting
90     static SimpleDateFormat sSimpleDateFormatForHour;
91 
ConvertUtils()92     private ConvertUtils() {}
93 
convert( BatteryEntry entry, BatteryUsageStats batteryUsageStats, int batteryLevel, int batteryStatus, int batteryHealth, long bootTimestamp, long timestamp)94     public static ContentValues convert(
95             BatteryEntry entry,
96             BatteryUsageStats batteryUsageStats,
97             int batteryLevel,
98             int batteryStatus,
99             int batteryHealth,
100             long bootTimestamp,
101             long timestamp) {
102         final ContentValues values = new ContentValues();
103         if (entry != null && batteryUsageStats != null) {
104             values.put(BatteryHistEntry.KEY_UID, Long.valueOf(entry.getUid()));
105             values.put(BatteryHistEntry.KEY_USER_ID,
106                 Long.valueOf(UserHandle.getUserId(entry.getUid())));
107             values.put(BatteryHistEntry.KEY_APP_LABEL, entry.getLabel());
108             values.put(BatteryHistEntry.KEY_PACKAGE_NAME,
109                 entry.getDefaultPackageName());
110             values.put(BatteryHistEntry.KEY_IS_HIDDEN, Boolean.valueOf(entry.isHidden()));
111             values.put(BatteryHistEntry.KEY_TOTAL_POWER,
112                 Double.valueOf(batteryUsageStats.getConsumedPower()));
113             values.put(BatteryHistEntry.KEY_CONSUME_POWER,
114                 Double.valueOf(entry.getConsumedPower()));
115             values.put(BatteryHistEntry.KEY_PERCENT_OF_TOTAL,
116                 Double.valueOf(entry.percent));
117             values.put(BatteryHistEntry.KEY_FOREGROUND_USAGE_TIME,
118                 Long.valueOf(entry.getTimeInForegroundMs()));
119             values.put(BatteryHistEntry.KEY_BACKGROUND_USAGE_TIME,
120                 Long.valueOf(entry.getTimeInBackgroundMs()));
121             values.put(BatteryHistEntry.KEY_DRAIN_TYPE,
122                 Integer.valueOf(entry.getPowerComponentId()));
123             values.put(BatteryHistEntry.KEY_CONSUMER_TYPE,
124                 Integer.valueOf(entry.getConsumerType()));
125         } else {
126             values.put(BatteryHistEntry.KEY_PACKAGE_NAME, FAKE_PACKAGE_NAME);
127         }
128         values.put(BatteryHistEntry.KEY_BOOT_TIMESTAMP, Long.valueOf(bootTimestamp));
129         values.put(BatteryHistEntry.KEY_TIMESTAMP, Long.valueOf(timestamp));
130         values.put(BatteryHistEntry.KEY_ZONE_ID, TimeZone.getDefault().getID());
131         values.put(BatteryHistEntry.KEY_BATTERY_LEVEL, Integer.valueOf(batteryLevel));
132         values.put(BatteryHistEntry.KEY_BATTERY_STATUS, Integer.valueOf(batteryStatus));
133         values.put(BatteryHistEntry.KEY_BATTERY_HEALTH, Integer.valueOf(batteryHealth));
134         return values;
135     }
136 
137     /** Converts UTC timestamp to human readable local time string. */
utcToLocalTime(Context context, long timestamp)138     public static String utcToLocalTime(Context context, long timestamp) {
139         final Locale currentLocale = getLocale(context);
140         final String currentZoneId = TimeZone.getDefault().getID();
141         if (!currentZoneId.equals(sZoneId)
142                 || !currentLocale.equals(sLocale)
143                 || sSimpleDateFormat == null) {
144             sLocale = currentLocale;
145             sZoneId = currentZoneId;
146             sSimpleDateFormat =
147                 new SimpleDateFormat("MMM dd,yyyy HH:mm:ss", currentLocale);
148         }
149         return sSimpleDateFormat.format(new Date(timestamp));
150     }
151 
152     /** Converts UTC timestamp to local time hour data. */
utcToLocalTimeHour( Context context, long timestamp, boolean is24HourFormat)153     public static String utcToLocalTimeHour(
154             Context context, long timestamp, boolean is24HourFormat) {
155         final Locale currentLocale = getLocale(context);
156         final String currentZoneId = TimeZone.getDefault().getID();
157         if (!currentZoneId.equals(sZoneIdForHour)
158                 || !currentLocale.equals(sLocaleForHour)
159                 || sIs24HourFormat != is24HourFormat
160                 || sSimpleDateFormatForHour == null) {
161             sLocaleForHour = currentLocale;
162             sZoneIdForHour = currentZoneId;
163             sIs24HourFormat = is24HourFormat;
164             sSimpleDateFormatForHour = new SimpleDateFormat(
165                     sIs24HourFormat ? "HH" : "h", currentLocale);
166         }
167         return sSimpleDateFormatForHour.format(new Date(timestamp))
168             .toLowerCase(currentLocale);
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 HashSet<>();
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(BatteryChartView.SELECTED_INDEX_ALL, resultMap);
290         if (purgeLowPercentageAndFakeData) {
291             purgeLowPercentageAndFakeData(resultMap);
292         }
293         return resultMap;
294     }
295 
insert24HoursData( final int desiredIndex, final Map<Integer, List<BatteryDiffEntry>> indexedUsageMap)296     private static void insert24HoursData(
297             final int desiredIndex,
298             final Map<Integer, List<BatteryDiffEntry>> indexedUsageMap) {
299         final Map<String, BatteryDiffEntry> resultMap = new HashMap<>();
300         double totalConsumePower = 0.0;
301         // Loops for all BatteryDiffEntry and aggregate them together.
302         for (List<BatteryDiffEntry> entryList : indexedUsageMap.values()) {
303             for (BatteryDiffEntry entry : entryList) {
304                 final String key = entry.mBatteryHistEntry.getKey();
305                 final BatteryDiffEntry oldBatteryDiffEntry = resultMap.get(key);
306                 // Creates new BatteryDiffEntry if we don't have it.
307                 if (oldBatteryDiffEntry == null) {
308                     resultMap.put(key, entry.clone());
309                 } else {
310                     // Sums up some fields data into the existing one.
311                     oldBatteryDiffEntry.mForegroundUsageTimeInMs +=
312                         entry.mForegroundUsageTimeInMs;
313                     oldBatteryDiffEntry.mBackgroundUsageTimeInMs +=
314                         entry.mBackgroundUsageTimeInMs;
315                     oldBatteryDiffEntry.mConsumePower += entry.mConsumePower;
316                 }
317                 totalConsumePower += entry.mConsumePower;
318             }
319         }
320         final List<BatteryDiffEntry> resultList = new ArrayList<>(resultMap.values());
321         // Sets total 24 hours consume power data into all BatteryDiffEntry.
322         for (BatteryDiffEntry entry : resultList) {
323             entry.setTotalConsumePower(totalConsumePower);
324         }
325         indexedUsageMap.put(Integer.valueOf(desiredIndex), resultList);
326     }
327 
328     // Removes low percentage data and fake usage data, which will be zero value.
purgeLowPercentageAndFakeData( final Map<Integer, List<BatteryDiffEntry>> indexedUsageMap)329     private static void purgeLowPercentageAndFakeData(
330             final Map<Integer, List<BatteryDiffEntry>> indexedUsageMap) {
331         for (List<BatteryDiffEntry> entries : indexedUsageMap.values()) {
332             final Iterator<BatteryDiffEntry> iterator = entries.iterator();
333             while (iterator.hasNext()) {
334                 final BatteryDiffEntry entry = iterator.next();
335                 if (entry.getPercentOfTotal() < PERCENTAGE_OF_TOTAL_THRESHOLD
336                         || FAKE_PACKAGE_NAME.equals(entry.getPackageName())) {
337                     iterator.remove();
338                 }
339             }
340         }
341     }
342 
getDiffValue(long v1, long v2, long v3)343     private static long getDiffValue(long v1, long v2, long v3) {
344         return (v2 > v1 ? v2 - v1 : 0) + (v3 > v2 ? v3 - v2 : 0);
345     }
346 
getDiffValue(double v1, double v2, double v3)347     private static double getDiffValue(double v1, double v2, double v3) {
348         return (v2 > v1 ? v2 - v1 : 0) + (v3 > v2 ? v3 - v2 : 0);
349     }
350 
selectBatteryHistEntry( BatteryHistEntry entry1, BatteryHistEntry entry2, BatteryHistEntry entry3)351     private static BatteryHistEntry selectBatteryHistEntry(
352             BatteryHistEntry entry1,
353             BatteryHistEntry entry2,
354             BatteryHistEntry entry3) {
355         if (entry1 != null && entry1 != EMPTY_BATTERY_HIST_ENTRY) {
356             return entry1;
357         } else if (entry2 != null && entry2 != EMPTY_BATTERY_HIST_ENTRY) {
358             return entry2;
359         } else {
360             return entry3 != null && entry3 != EMPTY_BATTERY_HIST_ENTRY
361                 ? entry3 : null;
362         }
363     }
364 
365     @VisibleForTesting
getLocale(Context context)366     static Locale getLocale(Context context) {
367         if (context == null) {
368             return Locale.getDefault();
369         }
370         final LocaleList locales =
371             context.getResources().getConfiguration().getLocales();
372         return locales != null && !locales.isEmpty() ? locales.get(0)
373             : Locale.getDefault();
374     }
375 }
376