hide camera background running time in the battery usage screen to avoid users confuse about it, since it will standby in the backgound to sspeed up launching time, not always running in the background Bug: 196744910 Test: make SettingsRoboTests Change-Id: Ia391d661bca44435fc5a68b1c86e5ca32dd0fcfe
360 lines
17 KiB
Java
360 lines
17 KiB
Java
/*
|
|
* 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.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.HashSet;
|
|
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.percent));
|
|
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 HashSet<>();
|
|
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 List<CharSequence> backgroundUsageTimeHideList =
|
|
FeatureFactory.getFactory(context)
|
|
.getPowerUsageFeatureProvider(context)
|
|
.getHideBackgroundUsageTimeList(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();
|
|
}
|
|
}
|