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.DateFormat;
23 import android.text.format.DateUtils;
24 import android.util.Log;
25 
26 import androidx.annotation.VisibleForTesting;
27 
28 import com.android.settings.overlay.FeatureFactory;
29 
30 import java.lang.annotation.Retention;
31 import java.lang.annotation.RetentionPolicy;
32 import java.time.Duration;
33 import java.util.ArrayList;
34 import java.util.HashMap;
35 import java.util.HashSet;
36 import java.util.Iterator;
37 import java.util.List;
38 import java.util.Locale;
39 import java.util.Map;
40 import java.util.Set;
41 import java.util.TimeZone;
42 
43 /** A utility class to convert data into another types. */
44 public final class ConvertUtils {
45     private static final boolean DEBUG = false;
46     private static final String TAG = "ConvertUtils";
47     private static final Map<String, BatteryHistEntry> EMPTY_BATTERY_MAP = new HashMap<>();
48     private static final BatteryHistEntry EMPTY_BATTERY_HIST_ENTRY =
49         new BatteryHistEntry(new ContentValues());
50     // Maximum total time value for each slot cumulative data at most 2 hours.
51     private static final float TOTAL_TIME_THRESHOLD = DateUtils.HOUR_IN_MILLIS * 2;
52 
53     // Keys for metric metadata.
54     static final int METRIC_KEY_PACKAGE = 1;
55     static final int METRIC_KEY_BATTERY_LEVEL = 2;
56     static final int METRIC_KEY_BATTERY_USAGE = 3;
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     public static final int CONSUMER_TYPE_UNKNOWN = 0;
76     public static final int CONSUMER_TYPE_UID_BATTERY = 1;
77     public static final int CONSUMER_TYPE_USER_BATTERY = 2;
78     public static final int CONSUMER_TYPE_SYSTEM_BATTERY = 3;
79 
ConvertUtils()80     private ConvertUtils() {}
81 
convert( BatteryEntry entry, BatteryUsageStats batteryUsageStats, int batteryLevel, int batteryStatus, int batteryHealth, long bootTimestamp, long timestamp)82     public static ContentValues convert(
83             BatteryEntry entry,
84             BatteryUsageStats batteryUsageStats,
85             int batteryLevel,
86             int batteryStatus,
87             int batteryHealth,
88             long bootTimestamp,
89             long timestamp) {
90         final ContentValues values = new ContentValues();
91         if (entry != null && batteryUsageStats != null) {
92             values.put(BatteryHistEntry.KEY_UID, Long.valueOf(entry.getUid()));
93             values.put(BatteryHistEntry.KEY_USER_ID,
94                 Long.valueOf(UserHandle.getUserId(entry.getUid())));
95             values.put(BatteryHistEntry.KEY_APP_LABEL, entry.getLabel());
96             values.put(BatteryHistEntry.KEY_PACKAGE_NAME,
97                 entry.getDefaultPackageName());
98             values.put(BatteryHistEntry.KEY_IS_HIDDEN, Boolean.valueOf(entry.isHidden()));
99             values.put(BatteryHistEntry.KEY_TOTAL_POWER,
100                 Double.valueOf(batteryUsageStats.getConsumedPower()));
101             values.put(BatteryHistEntry.KEY_CONSUME_POWER,
102                 Double.valueOf(entry.getConsumedPower()));
103             values.put(BatteryHistEntry.KEY_PERCENT_OF_TOTAL,
104                 Double.valueOf(entry.percent));
105             values.put(BatteryHistEntry.KEY_FOREGROUND_USAGE_TIME,
106                 Long.valueOf(entry.getTimeInForegroundMs()));
107             values.put(BatteryHistEntry.KEY_BACKGROUND_USAGE_TIME,
108                 Long.valueOf(entry.getTimeInBackgroundMs()));
109             values.put(BatteryHistEntry.KEY_DRAIN_TYPE,
110                 Integer.valueOf(entry.getPowerComponentId()));
111             values.put(BatteryHistEntry.KEY_CONSUMER_TYPE,
112                 Integer.valueOf(entry.getConsumerType()));
113         } else {
114             values.put(BatteryHistEntry.KEY_PACKAGE_NAME, FAKE_PACKAGE_NAME);
115         }
116         values.put(BatteryHistEntry.KEY_BOOT_TIMESTAMP, Long.valueOf(bootTimestamp));
117         values.put(BatteryHistEntry.KEY_TIMESTAMP, Long.valueOf(timestamp));
118         values.put(BatteryHistEntry.KEY_ZONE_ID, TimeZone.getDefault().getID());
119         values.put(BatteryHistEntry.KEY_BATTERY_LEVEL, Integer.valueOf(batteryLevel));
120         values.put(BatteryHistEntry.KEY_BATTERY_STATUS, Integer.valueOf(batteryStatus));
121         values.put(BatteryHistEntry.KEY_BATTERY_HEALTH, Integer.valueOf(batteryHealth));
122         return values;
123     }
124 
125     /** Converts UTC timestamp to human readable local time string. */
utcToLocalTime(Context context, long timestamp)126     public static String utcToLocalTime(Context context, long timestamp) {
127         final Locale locale = getLocale(context);
128         final String pattern =
129             DateFormat.getBestDateTimePattern(locale, "MMM dd,yyyy HH:mm:ss");
130         return DateFormat.format(pattern, timestamp).toString();
131     }
132 
133     /** Converts UTC timestamp to local time hour data. */
utcToLocalTimeHour( Context context, long timestamp, boolean is24HourFormat)134     public static String utcToLocalTimeHour(
135             Context context, long timestamp, boolean is24HourFormat) {
136         final Locale locale = getLocale(context);
137         // e.g. for 12-hour format: 9 pm
138         // e.g. for 24-hour format: 09:00
139         final String skeleton = is24HourFormat ? "HHm" : "ha";
140         final String pattern = DateFormat.getBestDateTimePattern(locale, skeleton);
141         return DateFormat.format(pattern, timestamp).toString().toLowerCase(locale);
142     }
143 
144     /** 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)145     public static Map<Integer, List<BatteryDiffEntry>> getIndexedUsageMap(
146             final Context context,
147             final int timeSlotSize,
148             final long[] batteryHistoryKeys,
149             final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap,
150             final boolean purgeLowPercentageAndFakeData) {
151         if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) {
152             return new HashMap<>();
153         }
154         final Map<Integer, List<BatteryDiffEntry>> resultMap = new HashMap<>();
155         // Each time slot usage diff data =
156         //     Math.abs(timestamp[i+2] data - timestamp[i+1] data) +
157         //     Math.abs(timestamp[i+1] data - timestamp[i] data);
158         // since we want to aggregate every two hours data into a single time slot.
159         final int timestampStride = 2;
160         for (int index = 0; index < timeSlotSize; index++) {
161             final Long currentTimestamp =
162                 Long.valueOf(batteryHistoryKeys[index * timestampStride]);
163             final Long nextTimestamp =
164                 Long.valueOf(batteryHistoryKeys[index * timestampStride + 1]);
165             final Long nextTwoTimestamp =
166                 Long.valueOf(batteryHistoryKeys[index * timestampStride + 2]);
167             // Fetches BatteryHistEntry data from corresponding time slot.
168             final Map<String, BatteryHistEntry> currentBatteryHistMap =
169                 batteryHistoryMap.getOrDefault(currentTimestamp, EMPTY_BATTERY_MAP);
170             final Map<String, BatteryHistEntry> nextBatteryHistMap =
171                 batteryHistoryMap.getOrDefault(nextTimestamp, EMPTY_BATTERY_MAP);
172             final Map<String, BatteryHistEntry> nextTwoBatteryHistMap =
173                 batteryHistoryMap.getOrDefault(nextTwoTimestamp, EMPTY_BATTERY_MAP);
174             // We should not get the empty list since we have at least one fake data to record
175             // the battery level and status in each time slot, the empty list is used to
176             // represent there is no enough data to apply interpolation arithmetic.
177             if (currentBatteryHistMap.isEmpty()
178                     || nextBatteryHistMap.isEmpty()
179                     || nextTwoBatteryHistMap.isEmpty()) {
180                 resultMap.put(Integer.valueOf(index), new ArrayList<BatteryDiffEntry>());
181                 continue;
182             }
183 
184             // Collects all keys in these three time slot records as all populations.
185             final Set<String> allBatteryHistEntryKeys = new HashSet<>();
186             allBatteryHistEntryKeys.addAll(currentBatteryHistMap.keySet());
187             allBatteryHistEntryKeys.addAll(nextBatteryHistMap.keySet());
188             allBatteryHistEntryKeys.addAll(nextTwoBatteryHistMap.keySet());
189 
190             double totalConsumePower = 0.0;
191             final List<BatteryDiffEntry> batteryDiffEntryList = new ArrayList<>();
192             // Adds a specific time slot BatteryDiffEntry list into result map.
193             resultMap.put(Integer.valueOf(index), batteryDiffEntryList);
194 
195             // Calculates all packages diff usage data in a specific time slot.
196             for (String key : allBatteryHistEntryKeys) {
197                 final BatteryHistEntry currentEntry =
198                     currentBatteryHistMap.getOrDefault(key, EMPTY_BATTERY_HIST_ENTRY);
199                 final BatteryHistEntry nextEntry =
200                     nextBatteryHistMap.getOrDefault(key, EMPTY_BATTERY_HIST_ENTRY);
201                 final BatteryHistEntry nextTwoEntry =
202                     nextTwoBatteryHistMap.getOrDefault(key, EMPTY_BATTERY_HIST_ENTRY);
203                 // Cumulative values is a specific time slot for a specific app.
204                 long foregroundUsageTimeInMs =
205                     getDiffValue(
206                         currentEntry.mForegroundUsageTimeInMs,
207                         nextEntry.mForegroundUsageTimeInMs,
208                         nextTwoEntry.mForegroundUsageTimeInMs);
209                 long backgroundUsageTimeInMs =
210                     getDiffValue(
211                         currentEntry.mBackgroundUsageTimeInMs,
212                         nextEntry.mBackgroundUsageTimeInMs,
213                         nextTwoEntry.mBackgroundUsageTimeInMs);
214                 double consumePower =
215                     getDiffValue(
216                         currentEntry.mConsumePower,
217                         nextEntry.mConsumePower,
218                         nextTwoEntry.mConsumePower);
219                 // Excludes entry since we don't have enough data to calculate.
220                 if (foregroundUsageTimeInMs == 0
221                         && backgroundUsageTimeInMs == 0
222                         && consumePower == 0) {
223                     continue;
224                 }
225                 final BatteryHistEntry selectedBatteryEntry =
226                     selectBatteryHistEntry(currentEntry, nextEntry, nextTwoEntry);
227                 if (selectedBatteryEntry == null) {
228                     continue;
229                 }
230                 // Forces refine the cumulative value since it may introduce deviation
231                 // error since we will apply the interpolation arithmetic.
232                 final float totalUsageTimeInMs =
233                     foregroundUsageTimeInMs + backgroundUsageTimeInMs;
234                 if (totalUsageTimeInMs > TOTAL_TIME_THRESHOLD) {
235                     final float ratio = TOTAL_TIME_THRESHOLD / totalUsageTimeInMs;
236                     if (DEBUG) {
237                         Log.w(TAG, String.format("abnormal usage time %d|%d for:\n%s",
238                                 Duration.ofMillis(foregroundUsageTimeInMs).getSeconds(),
239                                 Duration.ofMillis(backgroundUsageTimeInMs).getSeconds(),
240                                 currentEntry));
241                     }
242                     foregroundUsageTimeInMs =
243                         Math.round(foregroundUsageTimeInMs * ratio);
244                     backgroundUsageTimeInMs =
245                         Math.round(backgroundUsageTimeInMs * ratio);
246                     consumePower = consumePower * ratio;
247                 }
248                 totalConsumePower += consumePower;
249                 batteryDiffEntryList.add(
250                     new BatteryDiffEntry(
251                         context,
252                         foregroundUsageTimeInMs,
253                         backgroundUsageTimeInMs,
254                         consumePower,
255                         selectedBatteryEntry));
256             }
257             // Sets total consume power data into all BatteryDiffEntry in the same slot.
258             for (BatteryDiffEntry diffEntry : batteryDiffEntryList) {
259                 diffEntry.setTotalConsumePower(totalConsumePower);
260             }
261         }
262         insert24HoursData(BatteryChartView.SELECTED_INDEX_ALL, resultMap);
263         if (purgeLowPercentageAndFakeData) {
264             purgeLowPercentageAndFakeData(context, resultMap);
265         }
266         return resultMap;
267     }
268 
insert24HoursData( final int desiredIndex, final Map<Integer, List<BatteryDiffEntry>> indexedUsageMap)269     private static void insert24HoursData(
270             final int desiredIndex,
271             final Map<Integer, List<BatteryDiffEntry>> indexedUsageMap) {
272         final Map<String, BatteryDiffEntry> resultMap = new HashMap<>();
273         double totalConsumePower = 0.0;
274         // Loops for all BatteryDiffEntry and aggregate them together.
275         for (List<BatteryDiffEntry> entryList : indexedUsageMap.values()) {
276             for (BatteryDiffEntry entry : entryList) {
277                 final String key = entry.mBatteryHistEntry.getKey();
278                 final BatteryDiffEntry oldBatteryDiffEntry = resultMap.get(key);
279                 // Creates new BatteryDiffEntry if we don't have it.
280                 if (oldBatteryDiffEntry == null) {
281                     resultMap.put(key, entry.clone());
282                 } else {
283                     // Sums up some fields data into the existing one.
284                     oldBatteryDiffEntry.mForegroundUsageTimeInMs +=
285                         entry.mForegroundUsageTimeInMs;
286                     oldBatteryDiffEntry.mBackgroundUsageTimeInMs +=
287                         entry.mBackgroundUsageTimeInMs;
288                     oldBatteryDiffEntry.mConsumePower += entry.mConsumePower;
289                 }
290                 totalConsumePower += entry.mConsumePower;
291             }
292         }
293         final List<BatteryDiffEntry> resultList = new ArrayList<>(resultMap.values());
294         // Sets total 24 hours consume power data into all BatteryDiffEntry.
295         for (BatteryDiffEntry entry : resultList) {
296             entry.setTotalConsumePower(totalConsumePower);
297         }
298         indexedUsageMap.put(Integer.valueOf(desiredIndex), resultList);
299     }
300 
301     // Removes low percentage data and fake usage data, which will be zero value.
purgeLowPercentageAndFakeData( final Context context, final Map<Integer, List<BatteryDiffEntry>> indexedUsageMap)302     private static void purgeLowPercentageAndFakeData(
303             final Context context,
304             final Map<Integer, List<BatteryDiffEntry>> indexedUsageMap) {
305         final Set<CharSequence> backgroundUsageTimeHideList =
306                 FeatureFactory.getFactory(context)
307                         .getPowerUsageFeatureProvider(context)
308                         .getHideBackgroundUsageTimeSet(context);
309         for (List<BatteryDiffEntry> entries : indexedUsageMap.values()) {
310             final Iterator<BatteryDiffEntry> iterator = entries.iterator();
311             while (iterator.hasNext()) {
312                 final BatteryDiffEntry entry = iterator.next();
313                 if (entry.getPercentOfTotal() < PERCENTAGE_OF_TOTAL_THRESHOLD
314                         || FAKE_PACKAGE_NAME.equals(entry.getPackageName())) {
315                     iterator.remove();
316                 }
317                 final String packageName = entry.getPackageName();
318                 if (packageName != null
319                         && !backgroundUsageTimeHideList.isEmpty()
320                         && backgroundUsageTimeHideList.contains(packageName)) {
321                   entry.mBackgroundUsageTimeInMs = 0;
322                 }
323             }
324         }
325     }
326 
getDiffValue(long v1, long v2, long v3)327     private static long getDiffValue(long v1, long v2, long v3) {
328         return (v2 > v1 ? v2 - v1 : 0) + (v3 > v2 ? v3 - v2 : 0);
329     }
330 
getDiffValue(double v1, double v2, double v3)331     private static double getDiffValue(double v1, double v2, double v3) {
332         return (v2 > v1 ? v2 - v1 : 0) + (v3 > v2 ? v3 - v2 : 0);
333     }
334 
selectBatteryHistEntry( BatteryHistEntry entry1, BatteryHistEntry entry2, BatteryHistEntry entry3)335     private static BatteryHistEntry selectBatteryHistEntry(
336             BatteryHistEntry entry1,
337             BatteryHistEntry entry2,
338             BatteryHistEntry entry3) {
339         if (entry1 != null && entry1 != EMPTY_BATTERY_HIST_ENTRY) {
340             return entry1;
341         } else if (entry2 != null && entry2 != EMPTY_BATTERY_HIST_ENTRY) {
342             return entry2;
343         } else {
344             return entry3 != null && entry3 != EMPTY_BATTERY_HIST_ENTRY
345                 ? entry3 : null;
346         }
347     }
348 
349     @VisibleForTesting
getLocale(Context context)350     static Locale getLocale(Context context) {
351         if (context == null) {
352             return Locale.getDefault();
353         }
354         final LocaleList locales =
355             context.getResources().getConfiguration().getLocales();
356         return locales != null && !locales.isEmpty() ? locales.get(0)
357             : Locale.getDefault();
358     }
359 }
360