| /* |
| * Copyright (C) 2021 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file |
| * except in compliance with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software distributed under the |
| * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the specific language governing |
| * permissions and limitations under the License. |
| */ |
| package com.android.settings.fuelgauge; |
| |
| import android.annotation.IntDef; |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.os.BatteryUsageStats; |
| import android.os.LocaleList; |
| import android.os.UserHandle; |
| import android.text.format.DateFormat; |
| import android.text.format.DateUtils; |
| import android.util.ArraySet; |
| import android.util.Log; |
| |
| import androidx.annotation.VisibleForTesting; |
| |
| import com.android.settings.overlay.FeatureFactory; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.time.Duration; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.TimeZone; |
| |
| /** A utility class to convert data into another types. */ |
| public final class ConvertUtils { |
| private static final boolean DEBUG = false; |
| private static final String TAG = "ConvertUtils"; |
| private static final Map<String, BatteryHistEntry> EMPTY_BATTERY_MAP = new HashMap<>(); |
| private static final BatteryHistEntry EMPTY_BATTERY_HIST_ENTRY = |
| new BatteryHistEntry(new ContentValues()); |
| // Maximum total time value for each slot cumulative data at most 2 hours. |
| private static final float TOTAL_TIME_THRESHOLD = DateUtils.HOUR_IN_MILLIS * 2; |
| |
| // Keys for metric metadata. |
| static final int METRIC_KEY_PACKAGE = 1; |
| static final int METRIC_KEY_BATTERY_LEVEL = 2; |
| static final int METRIC_KEY_BATTERY_USAGE = 3; |
| |
| @VisibleForTesting |
| static double PERCENTAGE_OF_TOTAL_THRESHOLD = 1f; |
| |
| /** Invalid system battery consumer drain type. */ |
| public static final int INVALID_DRAIN_TYPE = -1; |
| /** A fake package name to represent no BatteryEntry data. */ |
| public static final String FAKE_PACKAGE_NAME = "fake_package"; |
| |
| @IntDef(prefix = {"CONSUMER_TYPE"}, value = { |
| CONSUMER_TYPE_UNKNOWN, |
| CONSUMER_TYPE_UID_BATTERY, |
| CONSUMER_TYPE_USER_BATTERY, |
| CONSUMER_TYPE_SYSTEM_BATTERY, |
| }) |
| @Retention(RetentionPolicy.SOURCE) |
| public static @interface ConsumerType {} |
| |
| public static final int CONSUMER_TYPE_UNKNOWN = 0; |
| public static final int CONSUMER_TYPE_UID_BATTERY = 1; |
| public static final int CONSUMER_TYPE_USER_BATTERY = 2; |
| public static final int CONSUMER_TYPE_SYSTEM_BATTERY = 3; |
| |
| private ConvertUtils() {} |
| |
| public static ContentValues convert( |
| BatteryEntry entry, |
| BatteryUsageStats batteryUsageStats, |
| int batteryLevel, |
| int batteryStatus, |
| int batteryHealth, |
| long bootTimestamp, |
| long timestamp) { |
| final ContentValues values = new ContentValues(); |
| if (entry != null && batteryUsageStats != null) { |
| values.put(BatteryHistEntry.KEY_UID, Long.valueOf(entry.getUid())); |
| values.put(BatteryHistEntry.KEY_USER_ID, |
| Long.valueOf(UserHandle.getUserId(entry.getUid()))); |
| values.put(BatteryHistEntry.KEY_APP_LABEL, entry.getLabel()); |
| values.put(BatteryHistEntry.KEY_PACKAGE_NAME, |
| entry.getDefaultPackageName()); |
| values.put(BatteryHistEntry.KEY_IS_HIDDEN, Boolean.valueOf(entry.isHidden())); |
| values.put(BatteryHistEntry.KEY_TOTAL_POWER, |
| Double.valueOf(batteryUsageStats.getConsumedPower())); |
| values.put(BatteryHistEntry.KEY_CONSUME_POWER, |
| Double.valueOf(entry.getConsumedPower())); |
| values.put(BatteryHistEntry.KEY_PERCENT_OF_TOTAL, |
| Double.valueOf(entry.mPercent)); |
| values.put(BatteryHistEntry.KEY_FOREGROUND_USAGE_TIME, |
| Long.valueOf(entry.getTimeInForegroundMs())); |
| values.put(BatteryHistEntry.KEY_BACKGROUND_USAGE_TIME, |
| Long.valueOf(entry.getTimeInBackgroundMs())); |
| values.put(BatteryHistEntry.KEY_DRAIN_TYPE, |
| Integer.valueOf(entry.getPowerComponentId())); |
| values.put(BatteryHistEntry.KEY_CONSUMER_TYPE, |
| Integer.valueOf(entry.getConsumerType())); |
| } else { |
| values.put(BatteryHistEntry.KEY_PACKAGE_NAME, FAKE_PACKAGE_NAME); |
| } |
| values.put(BatteryHistEntry.KEY_BOOT_TIMESTAMP, Long.valueOf(bootTimestamp)); |
| values.put(BatteryHistEntry.KEY_TIMESTAMP, Long.valueOf(timestamp)); |
| values.put(BatteryHistEntry.KEY_ZONE_ID, TimeZone.getDefault().getID()); |
| values.put(BatteryHistEntry.KEY_BATTERY_LEVEL, Integer.valueOf(batteryLevel)); |
| values.put(BatteryHistEntry.KEY_BATTERY_STATUS, Integer.valueOf(batteryStatus)); |
| values.put(BatteryHistEntry.KEY_BATTERY_HEALTH, Integer.valueOf(batteryHealth)); |
| return values; |
| } |
| |
| /** Converts UTC timestamp to human readable local time string. */ |
| public static String utcToLocalTime(Context context, long timestamp) { |
| final Locale locale = getLocale(context); |
| final String pattern = |
| DateFormat.getBestDateTimePattern(locale, "MMM dd,yyyy HH:mm:ss"); |
| return DateFormat.format(pattern, timestamp).toString(); |
| } |
| |
| /** Converts UTC timestamp to local time hour data. */ |
| public static String utcToLocalTimeHour( |
| Context context, long timestamp, boolean is24HourFormat) { |
| final Locale locale = getLocale(context); |
| // e.g. for 12-hour format: 9 pm |
| // e.g. for 24-hour format: 09:00 |
| final String skeleton = is24HourFormat ? "HHm" : "ha"; |
| final String pattern = DateFormat.getBestDateTimePattern(locale, skeleton); |
| return DateFormat.format(pattern, timestamp).toString().toLowerCase(locale); |
| } |
| |
| /** Gets indexed battery usage data for each corresponding time slot. */ |
| public static Map<Integer, List<BatteryDiffEntry>> getIndexedUsageMap( |
| final Context context, |
| final int timeSlotSize, |
| final long[] batteryHistoryKeys, |
| final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap, |
| final boolean purgeLowPercentageAndFakeData) { |
| if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) { |
| return new HashMap<>(); |
| } |
| final Map<Integer, List<BatteryDiffEntry>> resultMap = new HashMap<>(); |
| // Each time slot usage diff data = |
| // Math.abs(timestamp[i+2] data - timestamp[i+1] data) + |
| // Math.abs(timestamp[i+1] data - timestamp[i] data); |
| // since we want to aggregate every two hours data into a single time slot. |
| final int timestampStride = 2; |
| for (int index = 0; index < timeSlotSize; index++) { |
| final Long currentTimestamp = |
| Long.valueOf(batteryHistoryKeys[index * timestampStride]); |
| final Long nextTimestamp = |
| Long.valueOf(batteryHistoryKeys[index * timestampStride + 1]); |
| final Long nextTwoTimestamp = |
| Long.valueOf(batteryHistoryKeys[index * timestampStride + 2]); |
| // Fetches BatteryHistEntry data from corresponding time slot. |
| final Map<String, BatteryHistEntry> currentBatteryHistMap = |
| batteryHistoryMap.getOrDefault(currentTimestamp, EMPTY_BATTERY_MAP); |
| final Map<String, BatteryHistEntry> nextBatteryHistMap = |
| batteryHistoryMap.getOrDefault(nextTimestamp, EMPTY_BATTERY_MAP); |
| final Map<String, BatteryHistEntry> nextTwoBatteryHistMap = |
| batteryHistoryMap.getOrDefault(nextTwoTimestamp, EMPTY_BATTERY_MAP); |
| // We should not get the empty list since we have at least one fake data to record |
| // the battery level and status in each time slot, the empty list is used to |
| // represent there is no enough data to apply interpolation arithmetic. |
| if (currentBatteryHistMap.isEmpty() |
| || nextBatteryHistMap.isEmpty() |
| || nextTwoBatteryHistMap.isEmpty()) { |
| resultMap.put(Integer.valueOf(index), new ArrayList<BatteryDiffEntry>()); |
| continue; |
| } |
| |
| // Collects all keys in these three time slot records as all populations. |
| final Set<String> allBatteryHistEntryKeys = new ArraySet<>(); |
| allBatteryHistEntryKeys.addAll(currentBatteryHistMap.keySet()); |
| allBatteryHistEntryKeys.addAll(nextBatteryHistMap.keySet()); |
| allBatteryHistEntryKeys.addAll(nextTwoBatteryHistMap.keySet()); |
| |
| double totalConsumePower = 0.0; |
| final List<BatteryDiffEntry> batteryDiffEntryList = new ArrayList<>(); |
| // Adds a specific time slot BatteryDiffEntry list into result map. |
| resultMap.put(Integer.valueOf(index), batteryDiffEntryList); |
| |
| // Calculates all packages diff usage data in a specific time slot. |
| for (String key : allBatteryHistEntryKeys) { |
| final BatteryHistEntry currentEntry = |
| currentBatteryHistMap.getOrDefault(key, EMPTY_BATTERY_HIST_ENTRY); |
| final BatteryHistEntry nextEntry = |
| nextBatteryHistMap.getOrDefault(key, EMPTY_BATTERY_HIST_ENTRY); |
| final BatteryHistEntry nextTwoEntry = |
| nextTwoBatteryHistMap.getOrDefault(key, EMPTY_BATTERY_HIST_ENTRY); |
| // Cumulative values is a specific time slot for a specific app. |
| long foregroundUsageTimeInMs = |
| getDiffValue( |
| currentEntry.mForegroundUsageTimeInMs, |
| nextEntry.mForegroundUsageTimeInMs, |
| nextTwoEntry.mForegroundUsageTimeInMs); |
| long backgroundUsageTimeInMs = |
| getDiffValue( |
| currentEntry.mBackgroundUsageTimeInMs, |
| nextEntry.mBackgroundUsageTimeInMs, |
| nextTwoEntry.mBackgroundUsageTimeInMs); |
| double consumePower = |
| getDiffValue( |
| currentEntry.mConsumePower, |
| nextEntry.mConsumePower, |
| nextTwoEntry.mConsumePower); |
| // Excludes entry since we don't have enough data to calculate. |
| if (foregroundUsageTimeInMs == 0 |
| && backgroundUsageTimeInMs == 0 |
| && consumePower == 0) { |
| continue; |
| } |
| final BatteryHistEntry selectedBatteryEntry = |
| selectBatteryHistEntry(currentEntry, nextEntry, nextTwoEntry); |
| if (selectedBatteryEntry == null) { |
| continue; |
| } |
| // Forces refine the cumulative value since it may introduce deviation |
| // error since we will apply the interpolation arithmetic. |
| final float totalUsageTimeInMs = |
| foregroundUsageTimeInMs + backgroundUsageTimeInMs; |
| if (totalUsageTimeInMs > TOTAL_TIME_THRESHOLD) { |
| final float ratio = TOTAL_TIME_THRESHOLD / totalUsageTimeInMs; |
| if (DEBUG) { |
| Log.w(TAG, String.format("abnormal usage time %d|%d for:\n%s", |
| Duration.ofMillis(foregroundUsageTimeInMs).getSeconds(), |
| Duration.ofMillis(backgroundUsageTimeInMs).getSeconds(), |
| currentEntry)); |
| } |
| foregroundUsageTimeInMs = |
| Math.round(foregroundUsageTimeInMs * ratio); |
| backgroundUsageTimeInMs = |
| Math.round(backgroundUsageTimeInMs * ratio); |
| consumePower = consumePower * ratio; |
| } |
| totalConsumePower += consumePower; |
| batteryDiffEntryList.add( |
| new BatteryDiffEntry( |
| context, |
| foregroundUsageTimeInMs, |
| backgroundUsageTimeInMs, |
| consumePower, |
| selectedBatteryEntry)); |
| } |
| // Sets total consume power data into all BatteryDiffEntry in the same slot. |
| for (BatteryDiffEntry diffEntry : batteryDiffEntryList) { |
| diffEntry.setTotalConsumePower(totalConsumePower); |
| } |
| } |
| insert24HoursData(BatteryChartView.SELECTED_INDEX_ALL, resultMap); |
| if (purgeLowPercentageAndFakeData) { |
| purgeLowPercentageAndFakeData(context, resultMap); |
| } |
| return resultMap; |
| } |
| |
| private static void insert24HoursData( |
| final int desiredIndex, |
| final Map<Integer, List<BatteryDiffEntry>> indexedUsageMap) { |
| final Map<String, BatteryDiffEntry> resultMap = new HashMap<>(); |
| double totalConsumePower = 0.0; |
| // Loops for all BatteryDiffEntry and aggregate them together. |
| for (List<BatteryDiffEntry> entryList : indexedUsageMap.values()) { |
| for (BatteryDiffEntry entry : entryList) { |
| final String key = entry.mBatteryHistEntry.getKey(); |
| final BatteryDiffEntry oldBatteryDiffEntry = resultMap.get(key); |
| // Creates new BatteryDiffEntry if we don't have it. |
| if (oldBatteryDiffEntry == null) { |
| resultMap.put(key, entry.clone()); |
| } else { |
| // Sums up some fields data into the existing one. |
| oldBatteryDiffEntry.mForegroundUsageTimeInMs += |
| entry.mForegroundUsageTimeInMs; |
| oldBatteryDiffEntry.mBackgroundUsageTimeInMs += |
| entry.mBackgroundUsageTimeInMs; |
| oldBatteryDiffEntry.mConsumePower += entry.mConsumePower; |
| } |
| totalConsumePower += entry.mConsumePower; |
| } |
| } |
| final List<BatteryDiffEntry> resultList = new ArrayList<>(resultMap.values()); |
| // Sets total 24 hours consume power data into all BatteryDiffEntry. |
| for (BatteryDiffEntry entry : resultList) { |
| entry.setTotalConsumePower(totalConsumePower); |
| } |
| indexedUsageMap.put(Integer.valueOf(desiredIndex), resultList); |
| } |
| |
| // Removes low percentage data and fake usage data, which will be zero value. |
| private static void purgeLowPercentageAndFakeData( |
| final Context context, |
| final Map<Integer, List<BatteryDiffEntry>> indexedUsageMap) { |
| final Set<CharSequence> backgroundUsageTimeHideList = |
| FeatureFactory.getFactory(context) |
| .getPowerUsageFeatureProvider(context) |
| .getHideBackgroundUsageTimeSet(context); |
| for (List<BatteryDiffEntry> entries : indexedUsageMap.values()) { |
| final Iterator<BatteryDiffEntry> iterator = entries.iterator(); |
| while (iterator.hasNext()) { |
| final BatteryDiffEntry entry = iterator.next(); |
| if (entry.getPercentOfTotal() < PERCENTAGE_OF_TOTAL_THRESHOLD |
| || FAKE_PACKAGE_NAME.equals(entry.getPackageName())) { |
| iterator.remove(); |
| } |
| final String packageName = entry.getPackageName(); |
| if (packageName != null |
| && !backgroundUsageTimeHideList.isEmpty() |
| && backgroundUsageTimeHideList.contains(packageName)) { |
| entry.mBackgroundUsageTimeInMs = 0; |
| } |
| } |
| } |
| } |
| |
| private static long getDiffValue(long v1, long v2, long v3) { |
| return (v2 > v1 ? v2 - v1 : 0) + (v3 > v2 ? v3 - v2 : 0); |
| } |
| |
| private static double getDiffValue(double v1, double v2, double v3) { |
| return (v2 > v1 ? v2 - v1 : 0) + (v3 > v2 ? v3 - v2 : 0); |
| } |
| |
| private static BatteryHistEntry selectBatteryHistEntry( |
| BatteryHistEntry entry1, |
| BatteryHistEntry entry2, |
| BatteryHistEntry entry3) { |
| if (entry1 != null && entry1 != EMPTY_BATTERY_HIST_ENTRY) { |
| return entry1; |
| } else if (entry2 != null && entry2 != EMPTY_BATTERY_HIST_ENTRY) { |
| return entry2; |
| } else { |
| return entry3 != null && entry3 != EMPTY_BATTERY_HIST_ENTRY |
| ? entry3 : null; |
| } |
| } |
| |
| @VisibleForTesting |
| static Locale getLocale(Context context) { |
| if (context == null) { |
| return Locale.getDefault(); |
| } |
| final LocaleList locales = |
| context.getResources().getConfiguration().getLocales(); |
| return locales != null && !locales.isEmpty() ? locales.get(0) |
| : Locale.getDefault(); |
| } |
| } |