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