Support App and System tabs for battery usage breakdown.

design_doc: go/usage-frontend-dd

screen_record: https://drive.google.com/file/d/1I8dnoMf7y9KUg0eVeqLJpHqGGOtaX6_0/view?usp=share_link&resourcekey=0-Mte0-LKN1LULRssg9t7zGg

Bug b/260786962 is also fixed in this cl.

Bug: 258123381
Bug: 260786962
Fix: 258123381
Fix: 260786962
Test: manual
Change-Id: Ifd337331d02dc10c3234c594b2e142bcd459c00f
This commit is contained in:
Zaiyue Xue
2022-11-29 13:46:23 +08:00
parent ec1c9d94f2
commit 00fb008e9c
13 changed files with 1013 additions and 916 deletions

View File

@@ -21,30 +21,22 @@ import android.animation.AnimatorListenerAdapter;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.text.format.DateFormat;
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;
import androidx.preference.Preference;
import androidx.preference.PreferenceGroup;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.core.InstrumentedPreferenceFragment;
import com.android.settings.core.PreferenceControllerMixin;
import com.android.settings.fuelgauge.AdvancedPowerUsageDetail;
import com.android.settings.fuelgauge.BatteryUtils;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
@@ -54,73 +46,68 @@ import com.android.settingslib.core.lifecycle.events.OnCreate;
import com.android.settingslib.core.lifecycle.events.OnDestroy;
import com.android.settingslib.core.lifecycle.events.OnResume;
import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState;
import com.android.settingslib.utils.StringUtil;
import com.android.settingslib.widget.FooterPreference;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** Controls the update for chart graph and the list items. */
public class BatteryChartPreferenceController extends AbstractPreferenceController
implements PreferenceControllerMixin, LifecycleObserver, OnCreate, OnDestroy,
OnSaveInstanceState, OnResume, ExpandDividerPreference.OnExpandListener {
OnSaveInstanceState, OnResume {
private static final String TAG = "BatteryChartPreferenceController";
private static final String KEY_FOOTER_PREF = "battery_graph_footer";
private static final String PACKAGE_NAME_NONE = "none";
private static final int ENABLED_ICON_ALPHA = 255;
private static final int DISABLED_ICON_ALPHA = 255 / 3;
private static final String PREFERENCE_KEY = "battery_chart";
private static final long FADE_IN_ANIMATION_DURATION = 400L;
private static final long FADE_OUT_ANIMATION_DURATION = 200L;
// Keys for bundle instance to restore configurations.
private static final String KEY_EXPAND_SYSTEM_INFO = "expand_system_info";
private static final String KEY_DAILY_CHART_INDEX = "daily_chart_index";
private static final String KEY_HOURLY_CHART_INDEX = "hourly_chart_index";
private static int sUiMode = Configuration.UI_MODE_NIGHT_UNDEFINED;
@VisibleForTesting
Map<Integer, Map<Integer, BatteryDiffData>> mBatteryUsageMap;
/**
* A callback listener for battery usage is updated.
* This happens when battery usage data is ready or the selected index is changed.
*/
public interface OnBatteryUsageUpdatedListener {
/**
* The callback function for battery usage is updated.
* @param slotUsageData The battery usage diff data for the selected slot. This is used in
* the app list.
* @param slotTimestamp The selected slot timestamp information. This is used in the battery
* usage breakdown category.
* @param isAllUsageDataEmpty Whether all the battery usage data is null or empty. This is
* used when showing the footer.
*/
void onBatteryUsageUpdated(
BatteryDiffData slotUsageData, String slotTimestamp, boolean isAllUsageDataEmpty);
}
@VisibleForTesting
Context mPrefContext;
@VisibleForTesting
BatteryUtils mBatteryUtils;
@VisibleForTesting
PreferenceGroup mAppListPrefGroup;
@VisibleForTesting
ExpandDividerPreference mExpandDividerPreference;
@VisibleForTesting
boolean mIsExpanded = false;
@VisibleForTesting
BatteryChartView mDailyChartView;
@VisibleForTesting
BatteryChartView mHourlyChartView;
@VisibleForTesting
int mDailyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
@VisibleForTesting
int mHourlyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
@VisibleForTesting
Map<Integer, Map<Integer, BatteryDiffData>> mBatteryUsageMap;
private boolean mIs24HourFormat;
private boolean mIsFooterPrefAdded = false;
private boolean mHourlyChartVisible = true;
private View mBatteryChartViewGroup;
private View mCategoryTitleView;
private PreferenceScreen mPreferenceScreen;
private FooterPreference mFooterPreference;
private TextView mChartSummaryTextView;
private BatteryChartViewModel mDailyViewModel;
private List<BatteryChartViewModel> mHourlyViewModels;
private OnBatteryUsageUpdatedListener mOnBatteryUsageUpdatedListener;
private final String mPreferenceKey;
private final SettingsActivity mActivity;
private final InstrumentedPreferenceFragment mFragment;
private final MetricsFeatureProvider mMetricsFeatureProvider;
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final AnimatorListenerAdapter mHourlyChartFadeInAdapter =
@@ -135,18 +122,10 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
final HourlyChartLabelTextGenerator mHourlyChartLabelTextGenerator =
new HourlyChartLabelTextGenerator();
// Preference cache to avoid create new instance each time.
@VisibleForTesting
final Map<String, Preference> mPreferenceCache = new HashMap<>();
public BatteryChartPreferenceController(
Context context, String preferenceKey,
Lifecycle lifecycle, SettingsActivity activity,
InstrumentedPreferenceFragment fragment) {
Context context, Lifecycle lifecycle, SettingsActivity activity) {
super(context);
mActivity = activity;
mFragment = fragment;
mPreferenceKey = preferenceKey;
mIs24HourFormat = DateFormat.is24HourFormat(context);
mMetricsFeatureProvider =
FeatureFactory.getFactory(mContext).getMetricsFeatureProvider();
@@ -164,10 +143,8 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
savedInstanceState.getInt(KEY_DAILY_CHART_INDEX, mDailyChartIndex);
mHourlyChartIndex =
savedInstanceState.getInt(KEY_HOURLY_CHART_INDEX, mHourlyChartIndex);
mIsExpanded =
savedInstanceState.getBoolean(KEY_EXPAND_SYSTEM_INFO, mIsExpanded);
Log.d(TAG, String.format("onCreate() dailyIndex=%d hourlyIndex=%d isExpanded=%b",
mDailyChartIndex, mHourlyChartIndex, mIsExpanded));
Log.d(TAG, String.format("onCreate() dailyIndex=%d hourlyIndex=%d",
mDailyChartIndex, mHourlyChartIndex));
}
@Override
@@ -191,9 +168,8 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
}
savedInstance.putInt(KEY_DAILY_CHART_INDEX, mDailyChartIndex);
savedInstance.putInt(KEY_HOURLY_CHART_INDEX, mHourlyChartIndex);
savedInstance.putBoolean(KEY_EXPAND_SYSTEM_INFO, mIsExpanded);
Log.d(TAG, String.format("onSaveInstanceState() dailyIndex=%d hourlyIndex=%d isExpanded=%b",
mDailyChartIndex, mHourlyChartIndex, mIsExpanded));
Log.d(TAG, String.format("onSaveInstanceState() dailyIndex=%d hourlyIndex=%d",
mDailyChartIndex, mHourlyChartIndex));
}
@Override
@@ -202,25 +178,12 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
BatteryDiffEntry.clearCache();
}
mHandler.removeCallbacksAndMessages(/*token=*/ null);
mPreferenceCache.clear();
if (mAppListPrefGroup != null) {
mAppListPrefGroup.removeAll();
}
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
mPreferenceScreen = screen;
mPrefContext = screen.getContext();
mAppListPrefGroup = screen.findPreference(mPreferenceKey);
mAppListPrefGroup.setOrderingAsAdded(false);
mAppListPrefGroup.setTitle("");
mFooterPreference = screen.findPreference(KEY_FOOTER_PREF);
// Removes footer first until usage data is loaded to avoid flashing.
if (mFooterPreference != null) {
screen.removePreference(mFooterPreference);
}
}
@Override
@@ -230,42 +193,11 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
@Override
public String getPreferenceKey() {
return mPreferenceKey;
return PREFERENCE_KEY;
}
@Override
public boolean handlePreferenceTreeClick(Preference preference) {
if (!(preference instanceof PowerGaugePreference)) {
return false;
}
final PowerGaugePreference powerPref = (PowerGaugePreference) preference;
final BatteryDiffEntry diffEntry = powerPref.getBatteryDiffEntry();
final BatteryHistEntry histEntry = diffEntry.mBatteryHistEntry;
final String packageName = histEntry.mPackageName;
final boolean isAppEntry = histEntry.isAppEntry();
mMetricsFeatureProvider.action(
/* attribution */ SettingsEnums.OPEN_BATTERY_USAGE,
/* action */ isAppEntry
? SettingsEnums.ACTION_BATTERY_USAGE_APP_ITEM
: SettingsEnums.ACTION_BATTERY_USAGE_SYSTEM_ITEM,
/* pageId */ SettingsEnums.OPEN_BATTERY_USAGE,
TextUtils.isEmpty(packageName) ? PACKAGE_NAME_NONE : packageName,
(int) Math.round(diffEntry.getPercentOfTotal()));
Log.d(TAG, String.format("handleClick() label=%s key=%s package=%s",
diffEntry.getAppLabel(), histEntry.getKey(), histEntry.mPackageName));
AdvancedPowerUsageDetail.startBatteryDetailPage(
mActivity, mFragment, diffEntry, powerPref.getPercent(), getSlotInformation());
return true;
}
@Override
public void onExpand(boolean isExpanded) {
mIsExpanded = isExpanded;
mMetricsFeatureProvider.action(
mPrefContext,
SettingsEnums.ACTION_BATTERY_USAGE_EXPAND_ITEM,
isExpanded);
refreshExpandUi();
void setOnBatteryUsageUpdatedListener(OnBatteryUsageUpdatedListener listener) {
mOnBatteryUsageUpdatedListener = listener;
}
void setBatteryHistoryMap(
@@ -339,7 +271,6 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
mDailyChartIndex = trapezoidIndex;
mHourlyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
refreshUi();
requestAccessibilityFocusForCategoryTitle(mDailyChartView);
mMetricsFeatureProvider.action(
mPrefContext,
trapezoidIndex == BatteryChartViewModel.SELECTED_INDEX_ALL
@@ -355,7 +286,6 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
Log.d(TAG, "onHourlyChartSelect:" + trapezoidIndex);
mHourlyChartIndex = trapezoidIndex;
refreshUi();
requestAccessibilityFocusForCategoryTitle(mHourlyChartView);
mMetricsFeatureProvider.action(
mPrefContext,
trapezoidIndex == BatteryChartViewModel.SELECTED_INDEX_ALL
@@ -385,14 +315,13 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
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)));
});
if (mOnBatteryUsageUpdatedListener != null) {
final BatteryDiffData slotUsageData =
mBatteryUsageMap.get(mDailyChartIndex).get(mHourlyChartIndex);
mOnBatteryUsageUpdatedListener.onBatteryUsageUpdated(
slotUsageData, getSlotInformation(), isBatteryUsageMapNullOrEmpty());
}
return true;
}
@@ -414,9 +343,6 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
mDailyChartView.setVisibility(View.GONE);
mHourlyChartView.setVisibility(View.VISIBLE);
mHourlyChartView.setViewModel(null);
removeAndCacheAllPrefs();
addFooterPreferenceIfNeeded(false);
return false;
}
return true;
}
@@ -452,154 +378,6 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
return true;
}
private void addAllPreferences() {
final BatteryDiffData batteryDiffData =
mBatteryUsageMap.get(mDailyChartIndex).get(mHourlyChartIndex);
addFooterPreferenceIfNeeded(batteryDiffData != null
&& (!batteryDiffData.getAppDiffEntryList().isEmpty()
|| !batteryDiffData.getSystemDiffEntryList().isEmpty()));
if (batteryDiffData == null) {
Log.w(TAG, "cannot find BatteryDiffEntry for daily_index: " + mDailyChartIndex
+ " hourly_index: " + mHourlyChartIndex);
return;
}
// Adds app entries to the list if it is not empty.
if (!batteryDiffData.getAppDiffEntryList().isEmpty()) {
addPreferenceToScreen(batteryDiffData.getAppDiffEntryList());
}
// Adds the expandable divider if we have system entries data.
if (!batteryDiffData.getSystemDiffEntryList().isEmpty()) {
if (mExpandDividerPreference == null) {
mExpandDividerPreference = new ExpandDividerPreference(mPrefContext);
mExpandDividerPreference.setOnExpandListener(this);
mExpandDividerPreference.setIsExpanded(mIsExpanded);
}
mExpandDividerPreference.setOrder(
mAppListPrefGroup.getPreferenceCount());
mAppListPrefGroup.addPreference(mExpandDividerPreference);
}
refreshExpandUi();
}
@VisibleForTesting
void addPreferenceToScreen(List<BatteryDiffEntry> entries) {
if (mAppListPrefGroup == null || entries.isEmpty()) {
return;
}
int prefIndex = mAppListPrefGroup.getPreferenceCount();
for (BatteryDiffEntry entry : entries) {
boolean isAdded = false;
final String appLabel = entry.getAppLabel();
final Drawable appIcon = entry.getAppIcon();
if (TextUtils.isEmpty(appLabel) || appIcon == null) {
Log.w(TAG, "cannot find app resource for:" + entry.getPackageName());
continue;
}
final String prefKey = entry.mBatteryHistEntry.getKey();
PowerGaugePreference pref = mAppListPrefGroup.findPreference(prefKey);
if (pref != null) {
isAdded = true;
Log.w(TAG, "preference should be removed for:" + entry.getPackageName());
} else {
pref = (PowerGaugePreference) mPreferenceCache.get(prefKey);
}
// Creates new innstance if cached preference is not found.
if (pref == null) {
pref = new PowerGaugePreference(mPrefContext);
pref.setKey(prefKey);
mPreferenceCache.put(prefKey, pref);
}
pref.setIcon(appIcon);
pref.setTitle(appLabel);
pref.setOrder(prefIndex);
pref.setPercent(entry.getPercentOfTotal());
pref.setSingleLineTitle(true);
// Sets the BatteryDiffEntry to preference for launching detailed page.
pref.setBatteryDiffEntry(entry);
pref.setEnabled(entry.validForRestriction());
setPreferenceSummary(pref, entry);
if (!isAdded) {
mAppListPrefGroup.addPreference(pref);
}
appIcon.setAlpha(pref.isEnabled() ? ENABLED_ICON_ALPHA : DISABLED_ICON_ALPHA);
prefIndex++;
}
}
private void removeAndCacheAllPrefs() {
if (mAppListPrefGroup == null
|| mAppListPrefGroup.getPreferenceCount() == 0) {
return;
}
final int prefsCount = mAppListPrefGroup.getPreferenceCount();
for (int index = 0; index < prefsCount; index++) {
final Preference pref = mAppListPrefGroup.getPreference(index);
if (TextUtils.isEmpty(pref.getKey())) {
continue;
}
mPreferenceCache.put(pref.getKey(), pref);
}
mAppListPrefGroup.removeAll();
}
private void refreshExpandUi() {
final List<BatteryDiffEntry> systemEntries = mBatteryUsageMap.get(mDailyChartIndex).get(
mHourlyChartIndex).getSystemDiffEntryList();
if (mIsExpanded) {
addPreferenceToScreen(systemEntries);
} else {
// Removes and recycles all system entries to hide all of them.
for (BatteryDiffEntry entry : systemEntries) {
final String prefKey = entry.mBatteryHistEntry.getKey();
final Preference pref = mAppListPrefGroup.findPreference(prefKey);
if (pref != null) {
mAppListPrefGroup.removePreference(pref);
mPreferenceCache.put(pref.getKey(), pref);
}
}
}
}
@VisibleForTesting
void refreshCategoryTitle() {
final String slotInformation = getSlotInformation();
Log.d(TAG, String.format("refreshCategoryTitle:%s", slotInformation));
if (mAppListPrefGroup != null) {
mAppListPrefGroup.setTitle(
getSlotInformation(/*isApp=*/ true, slotInformation));
}
if (mExpandDividerPreference != null) {
mExpandDividerPreference.setTitle(
getSlotInformation(/*isApp=*/ false, slotInformation));
}
}
private void requestAccessibilityFocusForCategoryTitle(View view) {
if (!AccessibilityManager.getInstance(mContext).isEnabled()) {
return;
}
if (mCategoryTitleView == null) {
mCategoryTitleView = view.getRootView().findViewById(com.android.internal.R.id.title);
}
if (mCategoryTitleView != null) {
mCategoryTitleView.requestAccessibilityFocus();
}
}
private String getSlotInformation(boolean isApp, String slotInformation) {
// TODO: Updates the right slot information from daily and hourly chart selection.
// Null means we show all information without a specific time slot.
if (slotInformation == null) {
return isApp
? mPrefContext.getString(R.string.battery_app_usage)
: mPrefContext.getString(R.string.battery_system_usage);
} else {
return isApp
? mPrefContext.getString(R.string.battery_app_usage_for, slotInformation)
: mPrefContext.getString(R.string.battery_system_usage_for, slotInformation);
}
}
@VisibleForTesting
String getSlotInformation() {
if (mDailyViewModel == null || mHourlyViewModels == null) {
@@ -624,50 +402,6 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
return String.format("%s %s", selectedDayText, selectedHourText);
}
@VisibleForTesting
void setPreferenceSummary(
PowerGaugePreference preference, BatteryDiffEntry entry) {
final long foregroundUsageTimeInMs = entry.mForegroundUsageTimeInMs;
final long backgroundUsageTimeInMs = entry.mBackgroundUsageTimeInMs;
final long totalUsageTimeInMs = foregroundUsageTimeInMs + backgroundUsageTimeInMs;
String usageTimeSummary = null;
// Not shows summary for some system components without usage time.
if (totalUsageTimeInMs == 0) {
preference.setSummary(null);
// Shows background summary only if we don't have foreground usage time.
} else if (foregroundUsageTimeInMs == 0 && backgroundUsageTimeInMs != 0) {
usageTimeSummary = buildUsageTimeInfo(backgroundUsageTimeInMs, true);
// Shows total usage summary only if total usage time is small.
} else if (totalUsageTimeInMs < DateUtils.MINUTE_IN_MILLIS) {
usageTimeSummary = buildUsageTimeInfo(totalUsageTimeInMs, false);
} else {
usageTimeSummary = buildUsageTimeInfo(totalUsageTimeInMs, false);
// Shows background usage time if it is larger than a minute.
if (backgroundUsageTimeInMs > 0) {
usageTimeSummary +=
"\n" + buildUsageTimeInfo(backgroundUsageTimeInMs, true);
}
}
preference.setSummary(usageTimeSummary);
}
private String buildUsageTimeInfo(long usageTimeInMs, boolean isBackground) {
if (usageTimeInMs < DateUtils.MINUTE_IN_MILLIS) {
return mPrefContext.getString(
isBackground
? R.string.battery_usage_background_less_than_one_minute
: R.string.battery_usage_total_less_than_one_minute);
}
final CharSequence timeSequence =
StringUtil.formatElapsedTime(mPrefContext, usageTimeInMs,
/*withSeconds=*/ false, /*collapseTimeUnit=*/ false);
final int resourceId =
isBackground
? R.string.battery_usage_for_background_time
: R.string.battery_usage_for_total_time;
return mPrefContext.getString(resourceId, timeSequence);
}
private void animateBatteryChartViewGroup() {
if (mBatteryChartViewGroup != null && mBatteryChartViewGroup.getAlpha() == 0) {
mBatteryChartViewGroup.animate().alpha(1f).setDuration(FADE_IN_ANIMATION_DURATION)
@@ -725,18 +459,6 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
};
}
private void addFooterPreferenceIfNeeded(boolean containAppItems) {
if (mIsFooterPrefAdded || mFooterPreference == null) {
return;
}
mIsFooterPrefAdded = true;
mFooterPreference.setTitle(mPrefContext.getString(
containAppItems
? R.string.battery_usage_screen_footer
: R.string.battery_usage_screen_footer_empty));
mHandler.post(() -> mPreferenceScreen.addPreference(mFooterPreference));
}
private boolean isBatteryLevelDataInOneDay() {
return mHourlyViewModels != null && mHourlyViewModels.size() == 1;
}
@@ -747,6 +469,19 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
&& mHourlyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL;
}
private boolean isBatteryUsageMapNullOrEmpty() {
if (mBatteryUsageMap == null) {
return true;
}
BatteryDiffData allBatteryDiffData = mBatteryUsageMap
.get(BatteryChartViewModel.SELECTED_INDEX_ALL)
.get(BatteryChartViewModel.SELECTED_INDEX_ALL);
// If all data is null or empty, each slot must be null or empty.
return allBatteryDiffData == null
|| (allBatteryDiffData.getAppDiffEntryList().isEmpty()
&& allBatteryDiffData.getSystemDiffEntryList().isEmpty());
}
@VisibleForTesting
static int getTotalHours(final BatteryLevelData batteryLevelData) {
if (batteryLevelData == null) {

View File

@@ -0,0 +1,326 @@
/*
* Copyright (C) 2022 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.batteryusage;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.Log;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceGroup;
import androidx.preference.PreferenceScreen;
import androidx.viewpager2.widget.ViewPager2;
import com.android.internal.annotations.VisibleForTesting;
import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.core.InstrumentedPreferenceFragment;
import com.android.settings.fuelgauge.AdvancedPowerUsageDetail;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.core.lifecycle.Lifecycle;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnDestroy;
import com.android.settingslib.utils.StringUtil;
import com.android.settingslib.widget.FooterPreference;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** Controller for battery usage breakdown preference group. */
public class BatteryUsageBreakdownController extends BasePreferenceController
implements LifecycleObserver, OnDestroy {
private static final String TAG = "BatteryUsageBreakdownController";
private static final String ROOT_PREFERENCE_KEY = "battery_usage_breakdown";
private static final String FOOTER_PREFERENCE_KEY = "battery_usage_footer";
private static final String TAB_PREFERENCE_KEY = "battery_usage_tab";
private static final String APP_LIST_PREFERENCE_KEY = "app_list";
private static final String PACKAGE_NAME_NONE = "none";
private static final int ENABLED_ICON_ALPHA = 255;
private static final int DISABLED_ICON_ALPHA = 255 / 3;
private final SettingsActivity mActivity;
private final InstrumentedPreferenceFragment mFragment;
private final MetricsFeatureProvider mMetricsFeatureProvider;
private final Handler mHandler = new Handler(Looper.getMainLooper());
@VisibleForTesting
final Map<String, Preference> mPreferenceCache = new HashMap<>();
private int mTabPosition;
private String mSlotTimestamp;
@VisibleForTesting
Context mPrefContext;
@VisibleForTesting
PreferenceCategory mRootPreference;
@VisibleForTesting
TabPreference mTabPreference;
@VisibleForTesting
PreferenceGroup mAppListPreferenceGroup;
@VisibleForTesting
FooterPreference mFooterPreference;
@VisibleForTesting
BatteryDiffData mBatteryDiffData;
public BatteryUsageBreakdownController(
Context context, Lifecycle lifecycle, SettingsActivity activity,
InstrumentedPreferenceFragment fragment) {
super(context, ROOT_PREFERENCE_KEY);
mActivity = activity;
mFragment = fragment;
mMetricsFeatureProvider =
FeatureFactory.getFactory(context).getMetricsFeatureProvider();
if (lifecycle != null) {
lifecycle.addObserver(this);
}
}
@Override
public void onDestroy() {
mHandler.removeCallbacksAndMessages(/*token=*/ null);
mPreferenceCache.clear();
mAppListPreferenceGroup.removeAll();
}
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
}
@Override
public boolean isSliceable() {
return false;
}
@Override
public boolean handlePreferenceTreeClick(Preference preference) {
if (!(preference instanceof PowerGaugePreference)) {
return false;
}
final PowerGaugePreference powerPref = (PowerGaugePreference) preference;
final BatteryDiffEntry diffEntry = powerPref.getBatteryDiffEntry();
final BatteryHistEntry histEntry = diffEntry.mBatteryHistEntry;
final String packageName = histEntry.mPackageName;
final boolean isAppEntry = histEntry.isAppEntry();
mMetricsFeatureProvider.action(
/* attribution */ SettingsEnums.OPEN_BATTERY_USAGE,
/* action */ isAppEntry
? SettingsEnums.ACTION_BATTERY_USAGE_APP_ITEM
: SettingsEnums.ACTION_BATTERY_USAGE_SYSTEM_ITEM,
/* pageId */ SettingsEnums.OPEN_BATTERY_USAGE,
TextUtils.isEmpty(packageName) ? PACKAGE_NAME_NONE : packageName,
(int) Math.round(diffEntry.getPercentOfTotal()));
Log.d(TAG, String.format("handleClick() label=%s key=%s package=%s",
diffEntry.getAppLabel(), histEntry.getKey(), histEntry.mPackageName));
AdvancedPowerUsageDetail.startBatteryDetailPage(
mActivity, mFragment, diffEntry, powerPref.getPercent(), mSlotTimestamp);
return true;
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
mPrefContext = screen.getContext();
mRootPreference = screen.findPreference(ROOT_PREFERENCE_KEY);
mTabPreference = screen.findPreference(TAB_PREFERENCE_KEY);
mAppListPreferenceGroup = screen.findPreference(APP_LIST_PREFERENCE_KEY);
mFooterPreference = screen.findPreference(FOOTER_PREFERENCE_KEY);
mAppListPreferenceGroup.setOrderingAsAdded(false);
mTabPreference.initializeTabs(mFragment, new String[]{
mPrefContext.getString(R.string.battery_usage_app_tab),
mPrefContext.getString(R.string.battery_usage_system_tab)
});
mTabPreference.setOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
super.onPageSelected(position);
mTabPosition = position;
mHandler.post(() -> {
removeAndCacheAllPreferences();
addAllPreferences();
});
}
});
}
/**
* Updates UI when the battery usage is updated.
* @param slotUsageData The battery usage diff data for the selected slot. This is used in
* the app list.
* @param slotTimestamp The selected slot timestamp information. This is used in the battery
* usage breakdown category.
* @param isAllUsageDataEmpty Whether all the battery usage data is null or empty. This is
* used when showing the footer.
*/
void handleBatteryUsageUpdated(
BatteryDiffData slotUsageData, String slotTimestamp, boolean isAllUsageDataEmpty) {
mBatteryDiffData = slotUsageData;
mSlotTimestamp = slotTimestamp;
showCategoryTitle(slotTimestamp);
showTabAndAppList();
showFooterPreference(isAllUsageDataEmpty);
}
// TODO: request accessibility focus on category title when slot selection updated.
private void showCategoryTitle(String slotTimestamp) {
mRootPreference.setTitle(slotTimestamp == null
? mPrefContext.getString(
R.string.battery_usage_breakdown_title_since_last_full_charge)
: mPrefContext.getString(
R.string.battery_usage_breakdown_title_for_slot, slotTimestamp));
mRootPreference.setVisible(true);
}
private void showFooterPreference(boolean isAllBatteryUsageEmpty) {
mFooterPreference.setTitle(mPrefContext.getString(
isAllBatteryUsageEmpty
? R.string.battery_usage_screen_footer_empty
: R.string.battery_usage_screen_footer));
mFooterPreference.setVisible(true);
}
private void showTabAndAppList() {
removeAndCacheAllPreferences();
if (mBatteryDiffData == null) {
return;
}
mTabPreference.setVisible(true);
mAppListPreferenceGroup.setVisible(true);
mHandler.post(() -> {
addAllPreferences();
});
}
@VisibleForTesting
void addAllPreferences() {
if (mBatteryDiffData == null) {
return;
}
final long start = System.currentTimeMillis();
final List<BatteryDiffEntry> entries = mTabPosition == 0
? mBatteryDiffData.getAppDiffEntryList()
: mBatteryDiffData.getSystemDiffEntryList();
int prefIndex = mAppListPreferenceGroup.getPreferenceCount();
for (BatteryDiffEntry entry : entries) {
boolean isAdded = false;
final String appLabel = entry.getAppLabel();
final Drawable appIcon = entry.getAppIcon();
if (TextUtils.isEmpty(appLabel) || appIcon == null) {
Log.w(TAG, "cannot find app resource for:" + entry.getPackageName());
continue;
}
final String prefKey = entry.mBatteryHistEntry.getKey();
PowerGaugePreference pref = mAppListPreferenceGroup.findPreference(prefKey);
if (pref != null) {
isAdded = true;
Log.w(TAG, "preference should be removed for:" + entry.getPackageName());
} else {
pref = (PowerGaugePreference) mPreferenceCache.get(prefKey);
}
// Creates new innstance if cached preference is not found.
if (pref == null) {
pref = new PowerGaugePreference(mPrefContext);
pref.setKey(prefKey);
mPreferenceCache.put(prefKey, pref);
}
pref.setIcon(appIcon);
pref.setTitle(appLabel);
pref.setOrder(prefIndex);
pref.setPercent(entry.getPercentOfTotal());
pref.setSingleLineTitle(true);
// Sets the BatteryDiffEntry to preference for launching detailed page.
pref.setBatteryDiffEntry(entry);
pref.setEnabled(entry.validForRestriction());
setPreferenceSummary(pref, entry);
if (!isAdded) {
mAppListPreferenceGroup.addPreference(pref);
}
appIcon.setAlpha(pref.isEnabled() ? ENABLED_ICON_ALPHA : DISABLED_ICON_ALPHA);
prefIndex++;
}
Log.d(TAG, String.format("addAllPreferences() is finished in %d/ms",
(System.currentTimeMillis() - start)));
}
@VisibleForTesting
void removeAndCacheAllPreferences() {
final int prefsCount = mAppListPreferenceGroup.getPreferenceCount();
for (int index = 0; index < prefsCount; index++) {
final Preference pref = mAppListPreferenceGroup.getPreference(index);
if (TextUtils.isEmpty(pref.getKey())) {
continue;
}
mPreferenceCache.put(pref.getKey(), pref);
}
mAppListPreferenceGroup.removeAll();
}
@VisibleForTesting
void setPreferenceSummary(
PowerGaugePreference preference, BatteryDiffEntry entry) {
final long foregroundUsageTimeInMs = entry.mForegroundUsageTimeInMs;
final long backgroundUsageTimeInMs = entry.mBackgroundUsageTimeInMs;
final long totalUsageTimeInMs = foregroundUsageTimeInMs + backgroundUsageTimeInMs;
String usageTimeSummary = null;
// Not shows summary for some system components without usage time.
if (totalUsageTimeInMs == 0) {
preference.setSummary(null);
// Shows background summary only if we don't have foreground usage time.
} else if (foregroundUsageTimeInMs == 0 && backgroundUsageTimeInMs != 0) {
usageTimeSummary = buildUsageTimeInfo(backgroundUsageTimeInMs, true);
// Shows total usage summary only if total usage time is small.
} else if (totalUsageTimeInMs < DateUtils.MINUTE_IN_MILLIS) {
usageTimeSummary = buildUsageTimeInfo(totalUsageTimeInMs, false);
} else {
usageTimeSummary = buildUsageTimeInfo(totalUsageTimeInMs, false);
// Shows background usage time if it is larger than a minute.
if (backgroundUsageTimeInMs > 0) {
usageTimeSummary +=
"\n" + buildUsageTimeInfo(backgroundUsageTimeInMs, true);
}
}
preference.setSummary(usageTimeSummary);
}
private String buildUsageTimeInfo(long usageTimeInMs, boolean isBackground) {
if (usageTimeInMs < DateUtils.MINUTE_IN_MILLIS) {
return mPrefContext.getString(
isBackground
? R.string.battery_usage_background_less_than_one_minute
: R.string.battery_usage_total_less_than_one_minute);
}
final CharSequence timeSequence =
StringUtil.formatElapsedTime(mPrefContext, (double) usageTimeInMs,
/*withSeconds=*/ false, /*collapseTimeUnit=*/ false);
final int resourceId =
isBackground
? R.string.battery_usage_for_background_time
: R.string.battery_usage_for_total_time;
return mPrefContext.getString(resourceId, timeSequence);
}
}

View File

@@ -1,100 +0,0 @@
/*
* Copyright (C) 2022 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.batteryusage;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
/** A preference for expandable section divider. */
public class ExpandDividerPreference extends Preference {
private static final String TAG = "ExpandDividerPreference";
@VisibleForTesting
static final String PREFERENCE_KEY = "expandable_divider";
@VisibleForTesting
TextView mTextView;
@VisibleForTesting
ImageView mImageView;
private OnExpandListener mOnExpandListener;
private boolean mIsExpanded = false;
private String mTitleContent = null;
/** A callback listener for expand state is changed by users. */
public interface OnExpandListener {
/** Callback function for expand state is changed by users. */
void onExpand(boolean isExpanded);
}
public ExpandDividerPreference(Context context) {
this(context, /*attrs=*/ null);
}
public ExpandDividerPreference(Context context, AttributeSet attrs) {
super(context, attrs);
setLayoutResource(R.layout.preference_expand_divider);
setKey(PREFERENCE_KEY);
}
@Override
public void onBindViewHolder(PreferenceViewHolder view) {
super.onBindViewHolder(view);
mTextView = (TextView) view.findViewById(R.id.expand_title);
mImageView = (ImageView) view.findViewById(R.id.expand_icon);
refreshState();
}
@Override
public void onClick() {
setIsExpanded(!mIsExpanded);
if (mOnExpandListener != null) {
mOnExpandListener.onExpand(mIsExpanded);
}
}
void setTitle(final String titleContent) {
mTitleContent = titleContent;
refreshState();
}
void setIsExpanded(boolean isExpanded) {
mIsExpanded = isExpanded;
refreshState();
}
void setOnExpandListener(OnExpandListener listener) {
mOnExpandListener = listener;
}
private void refreshState() {
if (mImageView != null) {
mImageView.setImageResource(mIsExpanded
? R.drawable.ic_settings_expand_less
: R.drawable.ic_settings_expand_more);
}
if (mTextView != null) {
mTextView.setText(mTitleContent);
}
}
}

View File

@@ -50,8 +50,7 @@ import java.util.Map;
public class PowerUsageAdvanced extends PowerUsageBase {
private static final String TAG = "AdvancedBatteryUsage";
private static final String KEY_REFRESH_TYPE = "refresh_type";
private static final String KEY_BATTERY_GRAPH = "battery_graph";
private static final String KEY_APP_LIST = "app_list";
private static final String KEY_BATTERY_CHART = "battery_chart";
@VisibleForTesting
BatteryHistoryPreference mHistPref;
@@ -81,7 +80,7 @@ public class PowerUsageAdvanced extends PowerUsageBase {
super.onCreate(icicle);
final Context context = getContext();
refreshFeatureFlag(context);
mHistPref = (BatteryHistoryPreference) findPreference(KEY_BATTERY_GRAPH);
mHistPref = (BatteryHistoryPreference) findPreference(KEY_BATTERY_CHART);
setBatteryChartPreferenceController();
}
@@ -134,9 +133,17 @@ public class PowerUsageAdvanced extends PowerUsageBase {
refreshFeatureFlag(context);
final List<AbstractPreferenceController> controllers = new ArrayList<>();
mBatteryChartPreferenceController =
new BatteryChartPreferenceController(context, KEY_APP_LIST,
getSettingsLifecycle(), (SettingsActivity) getActivity(), this);
new BatteryChartPreferenceController(
context, getSettingsLifecycle(), (SettingsActivity) getActivity());
BatteryUsageBreakdownController batteryUsageBreakdownController =
new BatteryUsageBreakdownController(
context, getSettingsLifecycle(), (SettingsActivity) getActivity(), this);
mBatteryChartPreferenceController.setOnBatteryUsageUpdatedListener(
batteryUsageBreakdownController::handleBatteryUsageUpdated);
controllers.add(mBatteryChartPreferenceController);
controllers.add(batteryUsageBreakdownController);
setBatteryChartPreferenceController();
return controllers;
}
@@ -196,8 +203,10 @@ public class PowerUsageAdvanced extends PowerUsageBase {
public List<AbstractPreferenceController> createPreferenceControllers(
Context context) {
final List<AbstractPreferenceController> controllers = new ArrayList<>();
controllers.add(new BatteryChartPreferenceController(context,
KEY_APP_LIST, null /* lifecycle */, null /* activity */,
controllers.add(new BatteryChartPreferenceController(
context, null /* lifecycle */, null /* activity */));
controllers.add(new BatteryUsageBreakdownController(
context, null /* lifecycle */, null /* activity */,
null /* fragment */));
return controllers;
}

View File

@@ -0,0 +1,152 @@
/*
* Copyright (C) 2022 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.batteryusage;
import android.content.Context;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
import com.android.internal.annotations.VisibleForTesting;
import com.android.settings.R;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
/** A preference which contains a tab selection. */
public class TabPreference extends Preference {
private static final String TAG = "TabPreference";
private Fragment mRootFragment;
private ViewPager2 mViewPager;
private ViewPager2.OnPageChangeCallback mOnPageChangeCallback;
@VisibleForTesting
String[] mTabTitles;
@VisibleForTesting
int mSavedTabPosition;
@VisibleForTesting
TabLayout mTabLayout;
public TabPreference(Context context, AttributeSet attrs) {
super(context, attrs);
setLayoutResource(R.layout.preference_tab);
}
void initializeTabs(Fragment rootFragment, String[] tabTitles) {
mRootFragment = rootFragment;
mTabTitles = tabTitles;
}
void setOnPageChangeCallback(ViewPager2.OnPageChangeCallback callback) {
mOnPageChangeCallback = callback;
}
@Override
public void onBindViewHolder(PreferenceViewHolder view) {
super.onBindViewHolder(view);
if (mViewPager != null && mTabLayout != null) {
return;
}
mViewPager = (ViewPager2) view.findViewById(R.id.view_pager);
mViewPager.setAdapter(new FragmentAdapter(mRootFragment, mTabTitles.length));
mViewPager.setUserInputEnabled(false);
if (mOnPageChangeCallback != null) {
mViewPager.registerOnPageChangeCallback(mOnPageChangeCallback);
}
mTabLayout = (TabLayout) view.findViewById(R.id.tabs);
new TabLayoutMediator(
mTabLayout, mViewPager, /* autoRefresh= */ true, /* smoothScroll= */ false,
(tab, position) -> tab.setText(mTabTitles[position])).attach();
mTabLayout.getTabAt(mSavedTabPosition).select();
}
@Override
public void onDetached() {
super.onDetached();
if (mOnPageChangeCallback != null) {
mViewPager.unregisterOnPageChangeCallback(mOnPageChangeCallback);
}
}
@Override
protected Parcelable onSaveInstanceState() {
Log.d(TAG, "onSaveInstanceState() tabPosition=" + mTabLayout.getSelectedTabPosition());
return new SavedState(super.onSaveInstanceState(), mTabLayout.getSelectedTabPosition());
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (state == null || !state.getClass().equals(SavedState.class)) {
super.onRestoreInstanceState(state);
return;
}
SavedState savedState = (SavedState) state;
super.onRestoreInstanceState(savedState.getSuperState());
mSavedTabPosition = savedState.getTabPosition();
Log.d(TAG, "onRestoreInstanceState() tabPosition=" + savedState.getTabPosition());
}
@VisibleForTesting
static class SavedState extends BaseSavedState {
private int mTabPosition;
SavedState(Parcelable superState, int tabPosition) {
super(superState);
mTabPosition = tabPosition;
}
int getTabPosition() {
return mTabPosition;
}
}
private static class FragmentAdapter extends FragmentStateAdapter {
private final int mItemCount;
private final Fragment[] mItemFragments;
FragmentAdapter(@NonNull Fragment rootFragment, int itemCount) {
super(rootFragment);
mItemCount = itemCount;
mItemFragments = new Fragment[mItemCount];
for (int i = 0; i < mItemCount; i++) {
// Empty tab pages.
mItemFragments[i] = new Fragment();
}
}
@NonNull
@Override
public Fragment createFragment(int position) {
return mItemFragments[position];
}
@Override
public int getItemCount() {
return mItemCount;
}
}
}