/* * 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.content.res.Configuration; import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Looper; import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; import android.view.View; import android.widget.AdapterView; import androidx.preference.Preference; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceGroup; import androidx.preference.PreferenceScreen; import com.android.internal.annotations.VisibleForTesting; import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.Utils; import com.android.settings.core.BasePreferenceController; import com.android.settings.core.InstrumentedPreferenceFragment; import com.android.settings.fuelgauge.AdvancedPowerUsageDetail; import com.android.settings.fuelgauge.BatteryUtils; 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.core.lifecycle.events.OnResume; import com.android.settingslib.widget.FooterPreference; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; /** Controller for battery usage breakdown preference group. */ public class BatteryUsageBreakdownController extends BasePreferenceController implements LifecycleObserver, OnResume, 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 SPINNER_PREFERENCE_KEY = "battery_usage_spinner"; private static final String APP_LIST_PREFERENCE_KEY = "app_list"; private static final String PACKAGE_NAME_NONE = "none"; private static final List EMPTY_ENTRY_LIST = new ArrayList<>(); private static int sUiMode = Configuration.UI_MODE_NIGHT_UNDEFINED; private final SettingsActivity mActivity; private final InstrumentedPreferenceFragment mFragment; private final MetricsFeatureProvider mMetricsFeatureProvider; private final Handler mHandler = new Handler(Looper.getMainLooper()); @VisibleForTesting final Map mPreferenceCache = new HashMap<>(); private int mSpinnerPosition; private String mSlotTimestamp; @VisibleForTesting Context mPrefContext; @VisibleForTesting PreferenceCategory mRootPreference; @VisibleForTesting SpinnerPreference mSpinnerPreference; @VisibleForTesting PreferenceGroup mAppListPreferenceGroup; @VisibleForTesting FooterPreference mFooterPreference; @VisibleForTesting BatteryDiffData mBatteryDiffData; @VisibleForTesting String mPercentLessThanThresholdText; 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 onResume() { final int currentUiMode = mContext.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; if (sUiMode != currentUiMode) { sUiMode = currentUiMode; BatteryDiffEntry.clearCache(); mPreferenceCache.clear(); Log.d(TAG, "clear icon and label cache since uiMode is changed"); } } @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.getPercentage())); Log.d(TAG, String.format("handleClick() label=%s key=%s package=%s", diffEntry.getAppLabel(), histEntry.getKey(), histEntry.mPackageName)); AdvancedPowerUsageDetail.startBatteryDetailPage( mActivity, mFragment, diffEntry, powerPref.getPercentage(), mSlotTimestamp); return true; } @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); mPrefContext = screen.getContext(); mRootPreference = screen.findPreference(ROOT_PREFERENCE_KEY); mSpinnerPreference = screen.findPreference(SPINNER_PREFERENCE_KEY); mAppListPreferenceGroup = screen.findPreference(APP_LIST_PREFERENCE_KEY); mFooterPreference = screen.findPreference(FOOTER_PREFERENCE_KEY); mPercentLessThanThresholdText = mPrefContext.getString( R.string.battery_usage_less_than_percent, Utils.formatPercentage(BatteryDiffData.SMALL_PERCENTAGE_THRESHOLD, false)); mAppListPreferenceGroup.setOrderingAsAdded(false); mSpinnerPreference.initializeSpinner( new String[]{ mPrefContext.getString(R.string.battery_usage_spinner_by_apps), mPrefContext.getString(R.string.battery_usage_spinner_by_systems) }, new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected( AdapterView parent, View view, int position, long id) { if (mSpinnerPosition != position) { mSpinnerPosition = position; mHandler.post(() -> { removeAndCacheAllUnusedPreferences(); addAllPreferences(); mMetricsFeatureProvider.action( mPrefContext, SettingsEnums.ACTION_BATTERY_USAGE_SPINNER, mSpinnerPosition); }); } } @Override public void onNothingSelected(AdapterView parent) { } }); } /** * 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); showSpinnerAndAppList(); showFooterPreference(isAllUsageDataEmpty); } 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 showSpinnerAndAppList() { if (mBatteryDiffData == null) { mHandler.post(() -> { removeAndCacheAllUnusedPreferences(); }); return; } mSpinnerPreference.setVisible(true); mAppListPreferenceGroup.setVisible(true); mHandler.post(() -> { removeAndCacheAllUnusedPreferences(); addAllPreferences(); }); } private List getBatteryDiffEntries() { if (mBatteryDiffData == null) { return EMPTY_ENTRY_LIST; } return mSpinnerPosition == 0 ? mBatteryDiffData.getAppDiffEntryList() : mBatteryDiffData.getSystemDiffEntryList(); } @VisibleForTesting void addAllPreferences() { if (mBatteryDiffData == null) { return; } final long start = System.currentTimeMillis(); final List entries = getBatteryDiffEntries(); 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.getKey(); PowerGaugePreference pref = mAppListPreferenceGroup.findPreference(prefKey); if (pref != null) { isAdded = true; } 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.setSingleLineTitle(true); // Sets the BatteryDiffEntry to preference for launching detailed page. pref.setBatteryDiffEntry(entry); pref.setSelectable(entry.validForRestriction()); setPreferencePercentage(pref, entry); setPreferenceSummary(pref, entry); if (!isAdded) { mAppListPreferenceGroup.addPreference(pref); } prefIndex++; } Log.d(TAG, String.format("addAllPreferences() is finished in %d/ms", (System.currentTimeMillis() - start))); } @VisibleForTesting void removeAndCacheAllUnusedPreferences() { List entries = getBatteryDiffEntries(); Set entryKeySet = new ArraySet<>(entries.size()); entries.forEach(entry -> entryKeySet.add(entry.getKey())); final int prefsCount = mAppListPreferenceGroup.getPreferenceCount(); for (int index = prefsCount - 1; index >= 0; index--) { final Preference pref = mAppListPreferenceGroup.getPreference(index); if (entryKeySet.contains(pref.getKey())) { // The pref is still used, don't remove. continue; } if (!TextUtils.isEmpty(pref.getKey())) { mPreferenceCache.put(pref.getKey(), pref); } mAppListPreferenceGroup.removePreference(pref); } } @VisibleForTesting void setPreferencePercentage( PowerGaugePreference preference, BatteryDiffEntry entry) { preference.setPercentage( entry.getPercentage() < BatteryDiffData.SMALL_PERCENTAGE_THRESHOLD ? mPercentLessThanThresholdText : Utils.formatPercentage( entry.getPercentage() + entry.getAdjustPercentageOffset(), /* round= */ true)); } @VisibleForTesting void setPreferenceSummary( PowerGaugePreference preference, BatteryDiffEntry entry) { preference.setSummary( BatteryUtils.buildBatteryUsageTimeSummary(mPrefContext, entry.isSystemEntry(), entry.mForegroundUsageTimeInMs, entry.mBackgroundUsageTimeInMs, entry.mScreenOnTimeInMs)); } }