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