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