Merge "Show app list in Battery Usage page when there is no battery level data." into tm-qpr-dev

This commit is contained in:
TreeHugger Robot
2022-09-21 05:50:45 +00:00
committed by Android (Google) Code Review
6 changed files with 468 additions and 65 deletions

View File

@@ -31,6 +31,7 @@ import android.text.format.DateUtils;
import android.util.Log;
import android.view.View;
import android.view.accessibility.AccessibilityManager;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
@@ -111,6 +112,7 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
private View mCategoryTitleView;
private PreferenceScreen mPreferenceScreen;
private FooterPreference mFooterPreference;
private TextView mChartSummaryTextView;
private BatteryChartViewModel mDailyViewModel;
private List<BatteryChartViewModel> mHourlyViewModels;
@@ -121,9 +123,9 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
private final MetricsFeatureProvider mMetricsFeatureProvider;
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final AnimatorListenerAdapter mHourlyChartFadeInAdapter =
createHourlyChartAnimatorListenerAdapter(/*isToShow=*/ true);
createHourlyChartAnimatorListenerAdapter(/*visible=*/ true);
private final AnimatorListenerAdapter mHourlyChartFadeOutAdapter =
createHourlyChartAnimatorListenerAdapter(/*isToShow=*/ false);
createHourlyChartAnimatorListenerAdapter(/*visible=*/ false);
@VisibleForTesting
final DailyChartLabelTextGenerator mDailyChartLabelTextGenerator =
@@ -289,6 +291,8 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
getTotalHours(batteryLevelData));
if (batteryLevelData == null) {
mDailyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
mHourlyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
mDailyViewModel = null;
mHourlyViewModels = null;
refreshUi();
@@ -321,6 +325,11 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
mHandler.post(() -> setBatteryChartViewInner(dailyChartView, hourlyChartView));
animateBatteryChartViewGroup();
}
if (mBatteryChartViewGroup != null) {
final View grandparentView = (View) mBatteryChartViewGroup.getParent();
mChartSummaryTextView = grandparentView != null
? grandparentView.findViewById(R.id.chart_summary) : null;
}
}
private void setBatteryChartViewInner(@NonNull final BatteryChartView dailyChartView,
@@ -367,8 +376,45 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
// Chart views are not initialized.
return false;
}
if (mDailyViewModel == null || mHourlyViewModels == null) {
// Fail to get battery level data, show an empty hourly chart view.
// When mDailyViewModel or mHourlyViewModels is null, there is no battery level data.
// This is mainly in 2 cases:
// 1) battery data is within 2 hours
// 2) no battery data in the latest 7 days (power off >= 7 days)
final boolean refreshUiResult = mDailyViewModel == null || mHourlyViewModels == null
? refreshUiWithNoLevelDataCase()
: refreshUiWithLevelDataCase();
if (!refreshUiResult) {
return false;
}
mHandler.post(() -> {
final long start = System.currentTimeMillis();
removeAndCacheAllPrefs();
addAllPreferences();
refreshCategoryTitle();
Log.d(TAG, String.format("refreshUi is finished in %d/ms",
(System.currentTimeMillis() - start)));
});
return true;
}
private boolean refreshUiWithNoLevelDataCase() {
setChartSummaryVisible(false);
if (mBatteryUsageMap == null) {
// There is no battery level data and battery usage data is not ready, wait for data
// ready to refresh UI. Show nothing temporarily.
mDailyChartView.setVisibility(View.GONE);
mHourlyChartView.setVisibility(View.GONE);
mDailyChartView.setViewModel(null);
mHourlyChartView.setViewModel(null);
return false;
} else if (mBatteryUsageMap
.get(BatteryChartViewModel.SELECTED_INDEX_ALL)
.get(BatteryChartViewModel.SELECTED_INDEX_ALL) == null) {
// There is no battery level data and battery usage data, show an empty hourly chart
// view.
mDailyChartView.setVisibility(View.GONE);
mHourlyChartView.setVisibility(View.VISIBLE);
mHourlyChartView.setViewModel(null);
@@ -376,7 +422,12 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
addFooterPreferenceIfNeeded(false);
return false;
}
return true;
}
private boolean refreshUiWithLevelDataCase() {
setChartSummaryVisible(true);
// Gets valid battery level data.
if (isBatteryLevelDataInOneDay()) {
// Only 1 day data, hide the daily chart view.
mDailyChartView.setVisibility(View.GONE);
@@ -389,10 +440,11 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
if (mDailyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL) {
// Multiple days are selected, hide the hourly chart view.
animateBatteryHourlyChartView(/*isToShow=*/ false);
animateBatteryHourlyChartView(/*visible=*/ false);
} else {
animateBatteryHourlyChartView(/*isToShow=*/ true);
final BatteryChartViewModel hourlyViewModel = mHourlyViewModels.get(mDailyChartIndex);
animateBatteryHourlyChartView(/*visible=*/ true);
final BatteryChartViewModel hourlyViewModel =
mHourlyViewModels.get(mDailyChartIndex);
hourlyViewModel.setSelectedIndex(mHourlyChartIndex);
mHourlyChartView.setViewModel(hourlyViewModel);
}
@@ -401,14 +453,6 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
// Battery usage data is not ready, wait for data ready to refresh UI.
return false;
}
mHandler.post(() -> {
final long start = System.currentTimeMillis();
removeAndCacheAllPrefs();
addAllPreferences();
refreshCategoryTitle();
Log.d(TAG, String.format("refreshUi is finished in %d/ms",
(System.currentTimeMillis() - start)));
});
return true;
}
@@ -427,7 +471,7 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
if (!batteryDiffData.getAppDiffEntryList().isEmpty()) {
addPreferenceToScreen(batteryDiffData.getAppDiffEntryList());
}
// Adds the expabable divider if we have system entries data.
// Adds the expandable divider if we have system entries data.
if (!batteryDiffData.getSystemDiffEntryList().isEmpty()) {
if (mExpandDividerPreference == null) {
mExpandDividerPreference = new ExpandDividerPreference(mPrefContext);
@@ -645,12 +689,12 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
}
}
private void animateBatteryHourlyChartView(final boolean isToShow) {
private void animateBatteryHourlyChartView(final boolean visible) {
if (mHourlyChartView == null) {
return;
}
if (isToShow) {
if (visible) {
mHourlyChartView.setAlpha(0f);
mHourlyChartView.setVisibility(View.VISIBLE);
mHourlyChartView.animate()
@@ -667,9 +711,15 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
}
}
private void setChartSummaryVisible(final boolean visible) {
if (mChartSummaryTextView != null) {
mChartSummaryTextView.setVisibility(visible ? View.VISIBLE : View.GONE);
}
}
private AnimatorListenerAdapter createHourlyChartAnimatorListenerAdapter(
final boolean isToShow) {
final int visibility = isToShow ? View.VISIBLE : View.GONE;
final boolean visible) {
final int visibility = visible ? View.VISIBLE : View.GONE;
return new AnimatorListenerAdapter() {
@Override

View File

@@ -55,11 +55,6 @@ public final class ConvertUtils {
// 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;
@@ -87,7 +82,7 @@ public final class ConvertUtils {
}
/** Converts to content values */
public static ContentValues convert(
public static ContentValues convertToContentValues(
BatteryEntry entry,
BatteryUsageStats batteryUsageStats,
int batteryLevel,
@@ -130,6 +125,21 @@ public final class ConvertUtils {
return values;
}
/** Converts to {@link BatteryHistEntry} */
public static BatteryHistEntry convertToBatteryHistEntry(
BatteryEntry entry,
BatteryUsageStats batteryUsageStats) {
return new BatteryHistEntry(
convertToContentValues(
entry,
batteryUsageStats,
/*batteryLevel=*/ 0,
/*batteryStatus=*/ 0,
/*batteryHealth=*/ 0,
/*bootTimestamp=*/ 0,
/*timestamp=*/ 0));
}
/** Converts UTC timestamp to human readable local time string. */
public static String utcToLocalTime(Context context, long timestamp) {
final Locale locale = getLocale(context);

View File

@@ -22,6 +22,9 @@ import android.app.settings.SettingsEnums;
import android.content.ContentValues;
import android.content.Context;
import android.os.AsyncTask;
import android.os.BatteryStatsManager;
import android.os.BatteryUsageStats;
import android.os.BatteryUsageStatsQuery;
import android.os.Handler;
import android.os.Looper;
import android.os.UserHandle;
@@ -50,6 +53,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* A utility class to process data loaded from database and make the data easy to use for battery
@@ -97,7 +101,9 @@ public final class DataProcessor {
@Nullable final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap,
final UsageMapAsyncResponse asyncResponseDelegate) {
if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) {
Log.d(TAG, "getBatteryLevelData() returns null");
Log.d(TAG, "batteryHistoryMap is null in getBatteryLevelData()");
loadBatteryUsageDataFromBatteryStatsService(
context, handler, asyncResponseDelegate);
return null;
}
handler = handler != null ? handler : new Handler(Looper.getMainLooper());
@@ -107,16 +113,20 @@ public final class DataProcessor {
// Wrap and processed history map into easy-to-use format for UI rendering.
final BatteryLevelData batteryLevelData =
getLevelDataThroughProcessedHistoryMap(context, processedBatteryHistoryMap);
if (batteryLevelData == null) {
loadBatteryUsageDataFromBatteryStatsService(
context, handler, asyncResponseDelegate);
Log.d(TAG, "getBatteryLevelData() returns null");
return null;
}
// Start the async task to compute diff usage data and load labels and icons.
if (batteryLevelData != null) {
new ComputeUsageMapAndLoadItemsTask(
context,
handler,
asyncResponseDelegate,
batteryLevelData.getHourlyBatteryLevelsPerDay(),
processedBatteryHistoryMap).execute();
}
new ComputeUsageMapAndLoadItemsTask(
context,
handler,
asyncResponseDelegate,
batteryLevelData.getHourlyBatteryLevelsPerDay(),
processedBatteryHistoryMap).execute();
return batteryLevelData;
}
@@ -365,19 +375,165 @@ public final class DataProcessor {
return null;
}
final MetricsFeatureProvider metricsFeatureProvider =
FeatureFactory.getFactory(context).getMetricsFeatureProvider();
metricsFeatureProvider.action(
context,
SettingsEnums.ACTION_BATTERY_USAGE_SHOWN_APP_COUNT,
countOfAppAfterPurge);
metricsFeatureProvider.action(
context,
SettingsEnums.ACTION_BATTERY_USAGE_HIDDEN_APP_COUNT,
countOfAppBeforePurge - countOfAppAfterPurge);
logAppCountMetrics(context, countOfAppBeforePurge, countOfAppAfterPurge);
return resultMap;
}
@VisibleForTesting
@Nullable
static BatteryDiffData generateBatteryDiffData(
final Context context,
@Nullable final List<BatteryEntry> batteryEntryList,
final BatteryUsageStats batteryUsageStats) {
final List<BatteryHistEntry> batteryHistEntryList =
convertToBatteryHistEntry(batteryEntryList, batteryUsageStats);
if (batteryHistEntryList == null || batteryHistEntryList.isEmpty()) {
Log.w(TAG, "batteryHistEntryList is null or empty in generateBatteryDiffData()");
return null;
}
final int currentUserId = context.getUserId();
final UserHandle userHandle =
Utils.getManagedProfile(context.getSystemService(UserManager.class));
final int workProfileUserId =
userHandle != null ? userHandle.getIdentifier() : Integer.MIN_VALUE;
final List<BatteryDiffEntry> appEntries = new ArrayList<>();
final List<BatteryDiffEntry> systemEntries = new ArrayList<>();
double totalConsumePower = 0f;
double consumePowerFromOtherUsers = 0f;
for (BatteryHistEntry entry : batteryHistEntryList) {
final boolean isFromOtherUsers = isConsumedFromOtherUsers(
currentUserId, workProfileUserId, entry);
totalConsumePower += entry.mConsumePower;
if (isFromOtherUsers) {
consumePowerFromOtherUsers += entry.mConsumePower;
} else {
final BatteryDiffEntry currentBatteryDiffEntry = new BatteryDiffEntry(
context,
entry.mForegroundUsageTimeInMs,
entry.mBackgroundUsageTimeInMs,
entry.mConsumePower,
entry);
if (currentBatteryDiffEntry.isSystemEntry()) {
systemEntries.add(currentBatteryDiffEntry);
} else {
appEntries.add(currentBatteryDiffEntry);
}
}
}
if (consumePowerFromOtherUsers != 0) {
systemEntries.add(createOtherUsersEntry(context, consumePowerFromOtherUsers));
}
// If there is no data, return null instead of empty item.
if (appEntries.isEmpty() && systemEntries.isEmpty()) {
return null;
}
return new BatteryDiffData(appEntries, systemEntries, totalConsumePower);
}
/**
* Starts the async task to load battery diff usage data and load app labels + icons.
*/
private static void loadBatteryUsageDataFromBatteryStatsService(
Context context,
@Nullable Handler handler,
final UsageMapAsyncResponse asyncResponseDelegate) {
new LoadUsageMapFromBatteryStatsServiceTask(
context,
handler,
asyncResponseDelegate).execute();
}
/**
* @return Returns the overall battery usage data from battery stats service directly.
*
* The returned value should be always a 2d map and composed by only 1 part:
* - [SELECTED_INDEX_ALL][SELECTED_INDEX_ALL]
*/
@Nullable
private static Map<Integer, Map<Integer, BatteryDiffData>> getBatteryUsageMapFromStatsService(
final Context context) {
final Map<Integer, Map<Integer, BatteryDiffData>> resultMap = new HashMap<>();
final Map<Integer, BatteryDiffData> allUsageMap = new HashMap<>();
// Always construct the map whether the value is null or not.
allUsageMap.put(SELECTED_INDEX_ALL,
getBatteryDiffDataFromBatteryStatsService(context));
resultMap.put(SELECTED_INDEX_ALL, allUsageMap);
// Compute the apps number before purge. Must put before purgeLowPercentageAndFakeData.
final int countOfAppBeforePurge = getCountOfApps(resultMap);
purgeLowPercentageAndFakeData(context, resultMap);
// Compute the apps number after purge. Must put after purgeLowPercentageAndFakeData.
final int countOfAppAfterPurge = getCountOfApps(resultMap);
logAppCountMetrics(context, countOfAppBeforePurge, countOfAppAfterPurge);
return resultMap;
}
@Nullable
private static BatteryDiffData getBatteryDiffDataFromBatteryStatsService(
final Context context) {
BatteryDiffData batteryDiffData = null;
try {
final BatteryUsageStatsQuery batteryUsageStatsQuery =
new BatteryUsageStatsQuery.Builder().includeBatteryHistory().build();
final BatteryUsageStats batteryUsageStats =
context.getSystemService(BatteryStatsManager.class)
.getBatteryUsageStats(batteryUsageStatsQuery);
if (batteryUsageStats == null) {
Log.w(TAG, "batteryUsageStats is null content");
return null;
}
final List<BatteryEntry> batteryEntryList =
generateBatteryEntryListFromBatteryUsageStats(context, batteryUsageStats);
batteryDiffData = generateBatteryDiffData(context, batteryEntryList, batteryUsageStats);
} catch (RuntimeException e) {
Log.e(TAG, "load batteryUsageStats:" + e);
}
return batteryDiffData;
}
@Nullable
private static List<BatteryEntry> generateBatteryEntryListFromBatteryUsageStats(
final Context context, final BatteryUsageStats batteryUsageStats) {
// Loads the battery consuming data.
final BatteryAppListPreferenceController controller =
new BatteryAppListPreferenceController(
context,
/*preferenceKey=*/ null,
/*lifecycle=*/ null,
/*activity*=*/ null,
/*fragment=*/ null);
return controller.getBatteryEntryList(batteryUsageStats, /*showAllApps=*/ true);
}
@Nullable
private static List<BatteryHistEntry> convertToBatteryHistEntry(
@Nullable final List<BatteryEntry> batteryEntryList,
final BatteryUsageStats batteryUsageStats) {
if (batteryEntryList == null || batteryEntryList.isEmpty()) {
Log.w(TAG, "batteryEntryList is null or empty in convertToBatteryHistEntry()");
return null;
}
return batteryEntryList.stream()
.filter(entry -> {
final long foregroundMs = entry.getTimeInForegroundMs();
final long backgroundMs = entry.getTimeInBackgroundMs();
return entry.getConsumedPower() > 0
|| (entry.getConsumedPower() == 0
&& (foregroundMs != 0 || backgroundMs != 0));
})
.map(entry -> ConvertUtils.convertToBatteryHistEntry(
entry,
batteryUsageStats))
.collect(Collectors.toList());
}
/**
* Interpolates history map based on expected timestamp slots and processes the corner case when
* the expected start timestamp is earlier than what we have.
@@ -940,6 +1096,22 @@ public final class DataProcessor {
return true;
}
private static void loadLabelAndIcon(
@Nullable final Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageMap) {
if (batteryUsageMap == null) {
return;
}
// Pre-loads each BatteryDiffEntry relative icon and label for all slots.
final BatteryDiffData batteryUsageMapForAll =
batteryUsageMap.get(SELECTED_INDEX_ALL).get(SELECTED_INDEX_ALL);
if (batteryUsageMapForAll != null) {
batteryUsageMapForAll.getAppDiffEntryList().forEach(
entry -> entry.loadLabelAndIcon());
batteryUsageMapForAll.getSystemDiffEntryList().forEach(
entry -> entry.loadLabelAndIcon());
}
}
private static long getTimestampWithDayDiff(final long timestamp, final int dayDiff) {
final Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(timestamp);
@@ -1006,6 +1178,21 @@ public final class DataProcessor {
return batteryDiffEntry;
}
private static void logAppCountMetrics(
Context context, final int countOfAppBeforePurge, final int countOfAppAfterPurge) {
context = context.getApplicationContext();
final MetricsFeatureProvider metricsFeatureProvider =
FeatureFactory.getFactory(context).getMetricsFeatureProvider();
metricsFeatureProvider.action(
context,
SettingsEnums.ACTION_BATTERY_USAGE_SHOWN_APP_COUNT,
countOfAppAfterPurge);
metricsFeatureProvider.action(
context,
SettingsEnums.ACTION_BATTERY_USAGE_HIDDEN_APP_COUNT,
countOfAppBeforePurge - countOfAppAfterPurge);
}
private static void log(Context context, final String content, final long timestamp,
final BatteryHistEntry entry) {
if (DEBUG) {
@@ -1015,12 +1202,12 @@ public final class DataProcessor {
}
// Compute diff map and loads all items (icon and label) in the background.
private static final class ComputeUsageMapAndLoadItemsTask
private static class ComputeUsageMapAndLoadItemsTask
extends AsyncTask<Void, Void, Map<Integer, Map<Integer, BatteryDiffData>>> {
private Context mApplicationContext;
private Handler mHandler;
private UsageMapAsyncResponse mAsyncResponseDelegate;
Context mApplicationContext;
final Handler mHandler;
final UsageMapAsyncResponse mAsyncResponseDelegate;
private List<BatteryLevelData.PeriodBatteryLevelData> mHourlyBatteryLevelsPerDay;
private Map<Long, Map<String, BatteryHistEntry>> mBatteryHistoryMap;
@@ -1051,17 +1238,7 @@ public final class DataProcessor {
final Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageMap =
getBatteryUsageMap(
mApplicationContext, mHourlyBatteryLevelsPerDay, mBatteryHistoryMap);
if (batteryUsageMap != null) {
// Pre-loads each BatteryDiffEntry relative icon and label for all slots.
final BatteryDiffData batteryUsageMapForAll =
batteryUsageMap.get(SELECTED_INDEX_ALL).get(SELECTED_INDEX_ALL);
if (batteryUsageMapForAll != null) {
batteryUsageMapForAll.getAppDiffEntryList().forEach(
entry -> entry.loadLabelAndIcon());
batteryUsageMapForAll.getSystemDiffEntryList().forEach(
entry -> entry.loadLabelAndIcon());
}
}
loadLabelAndIcon(batteryUsageMap);
Log.d(TAG, String.format("execute ComputeUsageMapAndLoadItemsTask in %d/ms",
(System.currentTimeMillis() - startTime)));
return batteryUsageMap;
@@ -1081,4 +1258,35 @@ public final class DataProcessor {
}
}
}
// Loads battery usage data from battery stats service directly and loads all items (icon and
// label) in the background.
private static final class LoadUsageMapFromBatteryStatsServiceTask
extends ComputeUsageMapAndLoadItemsTask {
private LoadUsageMapFromBatteryStatsServiceTask(
Context context,
Handler handler,
final UsageMapAsyncResponse asyncResponseDelegate) {
super(context, handler, asyncResponseDelegate, /*hourlyBatteryLevelsPerDay=*/ null,
/*batteryHistoryMap=*/ null);
}
@Override
protected Map<Integer, Map<Integer, BatteryDiffData>> doInBackground(Void... voids) {
if (mApplicationContext == null
|| mHandler == null
|| mAsyncResponseDelegate == null) {
Log.e(TAG, "invalid input for ComputeUsageMapAndLoadItemsTask()");
return null;
}
final long startTime = System.currentTimeMillis();
final Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageMap =
getBatteryUsageMapFromStatsService(mApplicationContext);
loadLabelAndIcon(batteryUsageMap);
Log.d(TAG, String.format("execute LoadUsageMapFromBatteryStatsServiceTask in %d/ms",
(System.currentTimeMillis() - startTime)));
return batteryUsageMap;
}
}
}