• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.fuelgauge.batteryusage;
18 
19 import android.app.settings.SettingsEnums;
20 import android.content.Context;
21 import android.content.res.Configuration;
22 import android.graphics.drawable.Drawable;
23 import android.os.Handler;
24 import android.os.Looper;
25 import android.text.TextUtils;
26 import android.util.ArraySet;
27 import android.util.Log;
28 import android.view.View;
29 import android.widget.AdapterView;
30 
31 import androidx.preference.Preference;
32 import androidx.preference.PreferenceCategory;
33 import androidx.preference.PreferenceGroup;
34 import androidx.preference.PreferenceScreen;
35 
36 import com.android.internal.annotations.VisibleForTesting;
37 import com.android.settings.R;
38 import com.android.settings.SettingsActivity;
39 import com.android.settings.Utils;
40 import com.android.settings.core.BasePreferenceController;
41 import com.android.settings.core.InstrumentedPreferenceFragment;
42 import com.android.settings.fuelgauge.AdvancedPowerUsageDetail;
43 import com.android.settings.fuelgauge.BatteryUtils;
44 import com.android.settings.overlay.FeatureFactory;
45 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
46 import com.android.settingslib.core.lifecycle.Lifecycle;
47 import com.android.settingslib.core.lifecycle.LifecycleObserver;
48 import com.android.settingslib.core.lifecycle.events.OnDestroy;
49 import com.android.settingslib.core.lifecycle.events.OnResume;
50 import com.android.settingslib.widget.FooterPreference;
51 
52 import java.util.ArrayList;
53 import java.util.HashMap;
54 import java.util.List;
55 import java.util.Map;
56 import java.util.Optional;
57 import java.util.Set;
58 
59 /** Controller for battery usage breakdown preference group. */
60 public class BatteryUsageBreakdownController extends BasePreferenceController
61         implements LifecycleObserver, OnResume, OnDestroy  {
62     private static final String TAG = "BatteryUsageBreakdownController";
63     private static final String ROOT_PREFERENCE_KEY = "battery_usage_breakdown";
64     private static final String FOOTER_PREFERENCE_KEY = "battery_usage_footer";
65     private static final String SPINNER_PREFERENCE_KEY = "battery_usage_spinner";
66     private static final String APP_LIST_PREFERENCE_KEY = "app_list";
67     private static final String PACKAGE_NAME_NONE = "none";
68     private static final List<BatteryDiffEntry> EMPTY_ENTRY_LIST = new ArrayList<>();
69 
70     private static int sUiMode = Configuration.UI_MODE_NIGHT_UNDEFINED;
71 
72     private final SettingsActivity mActivity;
73     private final InstrumentedPreferenceFragment mFragment;
74     private final MetricsFeatureProvider mMetricsFeatureProvider;
75     private final Handler mHandler = new Handler(Looper.getMainLooper());
76 
77     @VisibleForTesting
78     final Map<String, Preference> mPreferenceCache = new HashMap<>();
79 
80     private int mSpinnerPosition;
81     private String mSlotTimestamp;
82 
83     @VisibleForTesting
84     Context mPrefContext;
85     @VisibleForTesting
86     PreferenceCategory mRootPreference;
87     @VisibleForTesting
88     SpinnerPreference mSpinnerPreference;
89     @VisibleForTesting
90     PreferenceGroup mAppListPreferenceGroup;
91     @VisibleForTesting
92     FooterPreference mFooterPreference;
93     @VisibleForTesting
94     BatteryDiffData mBatteryDiffData;
95     @VisibleForTesting
96     String mPercentLessThanThresholdText;
97     @VisibleForTesting
98     boolean mIsHighlightSlot;
99     @VisibleForTesting
100     String mAnomalyEventId;
101     @VisibleForTesting
102     String mAnomalyEntryKey;
103     @VisibleForTesting
104     String mAnomalyHintString;
105 
BatteryUsageBreakdownController( Context context, Lifecycle lifecycle, SettingsActivity activity, InstrumentedPreferenceFragment fragment)106     public BatteryUsageBreakdownController(
107             Context context, Lifecycle lifecycle, SettingsActivity activity,
108             InstrumentedPreferenceFragment fragment) {
109         super(context, ROOT_PREFERENCE_KEY);
110         mActivity = activity;
111         mFragment = fragment;
112         mMetricsFeatureProvider =
113                 FeatureFactory.getFactory(context).getMetricsFeatureProvider();
114         if (lifecycle != null) {
115             lifecycle.addObserver(this);
116         }
117     }
118 
119     @Override
onResume()120     public void onResume() {
121         final int currentUiMode =
122                 mContext.getResources().getConfiguration().uiMode
123                         & Configuration.UI_MODE_NIGHT_MASK;
124         if (sUiMode != currentUiMode) {
125             sUiMode = currentUiMode;
126             BatteryDiffEntry.clearCache();
127             mPreferenceCache.clear();
128             Log.d(TAG, "clear icon and label cache since uiMode is changed");
129         }
130     }
131 
132     @Override
onDestroy()133     public void onDestroy() {
134         mHandler.removeCallbacksAndMessages(/*token=*/ null);
135         mPreferenceCache.clear();
136         mAppListPreferenceGroup.removeAll();
137     }
138 
139     @Override
getAvailabilityStatus()140     public int getAvailabilityStatus() {
141         return AVAILABLE;
142     }
143 
144     @Override
isSliceable()145     public boolean isSliceable() {
146         return false;
147     }
148 
getActionKey(String packageName)149     private String getActionKey(String packageName) {
150         final String actionKey = TextUtils.isEmpty(packageName)
151                 ? PACKAGE_NAME_NONE : packageName;
152         return mAnomalyEventId == null ? actionKey : actionKey + "|"  + mAnomalyEventId;
153     }
154 
155     @Override
handlePreferenceTreeClick(Preference preference)156     public boolean handlePreferenceTreeClick(Preference preference) {
157         if (!(preference instanceof PowerGaugePreference)) {
158             return false;
159         }
160         final PowerGaugePreference powerPref = (PowerGaugePreference) preference;
161         final BatteryDiffEntry diffEntry = powerPref.getBatteryDiffEntry();
162         final String packageName = diffEntry.getPackageName();
163         mMetricsFeatureProvider.action(
164                 /* attribution */ SettingsEnums.OPEN_BATTERY_USAGE,
165                 /* action */ diffEntry.isSystemEntry()
166                         ? SettingsEnums.ACTION_BATTERY_USAGE_SYSTEM_ITEM
167                         : SettingsEnums.ACTION_BATTERY_USAGE_APP_ITEM,
168                 /* pageId */ SettingsEnums.OPEN_BATTERY_USAGE,
169                 getActionKey(packageName),
170                 (int) Math.round(diffEntry.getPercentage()));
171         Log.d(TAG, String.format("handleClick() label=%s key=%s package=%s",
172                 diffEntry.getAppLabel(), diffEntry.getKey(), packageName));
173         AdvancedPowerUsageDetail.startBatteryDetailPage(
174                 mActivity, mFragment, diffEntry, powerPref.getPercentage(), mSlotTimestamp);
175         return true;
176     }
177 
178     @Override
displayPreference(PreferenceScreen screen)179     public void displayPreference(PreferenceScreen screen) {
180         super.displayPreference(screen);
181         mPrefContext = screen.getContext();
182         mRootPreference = screen.findPreference(ROOT_PREFERENCE_KEY);
183         mSpinnerPreference = screen.findPreference(SPINNER_PREFERENCE_KEY);
184         mAppListPreferenceGroup = screen.findPreference(APP_LIST_PREFERENCE_KEY);
185         mFooterPreference = screen.findPreference(FOOTER_PREFERENCE_KEY);
186         mPercentLessThanThresholdText = mPrefContext.getString(
187                 R.string.battery_usage_less_than_percent,
188                 Utils.formatPercentage(BatteryDiffData.SMALL_PERCENTAGE_THRESHOLD, false));
189 
190         mAppListPreferenceGroup.setOrderingAsAdded(false);
191         mSpinnerPreference.initializeSpinner(
192                 new String[]{
193                         mPrefContext.getString(R.string.battery_usage_spinner_view_by_apps),
194                         mPrefContext.getString(R.string.battery_usage_spinner_view_by_systems)
195                 },
196                 new AdapterView.OnItemSelectedListener() {
197                     @Override
198                     public void onItemSelected(
199                             AdapterView<?> parent, View view, int position, long id) {
200                         if (mSpinnerPosition != position) {
201                             mSpinnerPosition = position;
202                             mHandler.post(() -> {
203                                 removeAndCacheAllUnusedPreferences();
204                                 addAllPreferences();
205                                 mMetricsFeatureProvider.action(
206                                         mPrefContext,
207                                         SettingsEnums.ACTION_BATTERY_USAGE_SPINNER,
208                                         mSpinnerPosition);
209                             });
210                         }
211                     }
212 
213                     @Override
214                     public void onNothingSelected(AdapterView<?> parent) {
215                     }
216                 });
217     }
218 
219     /**
220      * Updates UI when the battery usage is updated.
221      * @param slotUsageData The battery usage diff data for the selected slot. This is used in
222      *                      the app list.
223      * @param slotTimestamp The selected slot timestamp information. This is used in the battery
224      *                      usage breakdown category.
225      * @param isAllUsageDataEmpty Whether all the battery usage data is null or empty. This is
226      *                            used when showing the footer.
227      */
handleBatteryUsageUpdated( BatteryDiffData slotUsageData, String slotTimestamp, boolean isAllUsageDataEmpty, boolean isHighlightSlot, Optional<AnomalyEventWrapper> optionalAnomalyEventWrapper)228     void handleBatteryUsageUpdated(
229             BatteryDiffData slotUsageData, String slotTimestamp,
230             boolean isAllUsageDataEmpty, boolean isHighlightSlot,
231             Optional<AnomalyEventWrapper> optionalAnomalyEventWrapper) {
232         mBatteryDiffData = slotUsageData;
233         mSlotTimestamp = slotTimestamp;
234         mIsHighlightSlot = isHighlightSlot;
235 
236         if (optionalAnomalyEventWrapper != null) {
237             final AnomalyEventWrapper anomalyEventWrapper =
238                     optionalAnomalyEventWrapper.orElse(null);
239             mAnomalyEventId = anomalyEventWrapper != null
240                     ? anomalyEventWrapper.getEventId() : null;
241             mAnomalyEntryKey = anomalyEventWrapper != null
242                     ? anomalyEventWrapper.getAnomalyEntryKey() : null;
243             mAnomalyHintString = anomalyEventWrapper != null
244                     ? anomalyEventWrapper.getAnomalyHintString() : null;
245         }
246 
247         showCategoryTitle(slotTimestamp);
248         showSpinnerAndAppList();
249         showFooterPreference(isAllUsageDataEmpty);
250     }
251 
showCategoryTitle(String slotTimestamp)252     private void showCategoryTitle(String slotTimestamp) {
253         mRootPreference.setTitle(slotTimestamp == null
254                 ? mPrefContext.getString(
255                         R.string.battery_usage_breakdown_title_since_last_full_charge)
256                 : mPrefContext.getString(
257                         R.string.battery_usage_breakdown_title_for_slot, slotTimestamp));
258         mRootPreference.setVisible(true);
259     }
260 
showFooterPreference(boolean isAllBatteryUsageEmpty)261     private void showFooterPreference(boolean isAllBatteryUsageEmpty) {
262         mFooterPreference.setTitle(mPrefContext.getString(
263                 isAllBatteryUsageEmpty
264                         ? R.string.battery_usage_screen_footer_empty
265                         : R.string.battery_usage_screen_footer));
266         mFooterPreference.setVisible(true);
267     }
268 
showSpinnerAndAppList()269     private void showSpinnerAndAppList() {
270         if (mBatteryDiffData == null) {
271             mHandler.post(() -> {
272                 removeAndCacheAllUnusedPreferences();
273             });
274             return;
275         }
276         mSpinnerPreference.setVisible(true);
277         mAppListPreferenceGroup.setVisible(true);
278         mHandler.post(() -> {
279             removeAndCacheAllUnusedPreferences();
280             addAllPreferences();
281         });
282     }
283 
getBatteryDiffEntries()284     private List<BatteryDiffEntry> getBatteryDiffEntries() {
285         if (mBatteryDiffData == null) {
286             return EMPTY_ENTRY_LIST;
287         }
288         return mSpinnerPosition == 0
289                 ? mBatteryDiffData.getAppDiffEntryList()
290                 : mBatteryDiffData.getSystemDiffEntryList();
291     }
292 
293     @VisibleForTesting
addAllPreferences()294     void addAllPreferences() {
295         if (mBatteryDiffData == null) {
296             return;
297         }
298         final long start = System.currentTimeMillis();
299         final List<BatteryDiffEntry> entries = getBatteryDiffEntries();
300         int prefIndex = mAppListPreferenceGroup.getPreferenceCount();
301         for (BatteryDiffEntry entry : entries) {
302             boolean isAdded = false;
303             final String appLabel = entry.getAppLabel();
304             final Drawable appIcon = entry.getAppIcon();
305             if (TextUtils.isEmpty(appLabel) || appIcon == null) {
306                 Log.w(TAG, "cannot find app resource for:" + entry.getPackageName());
307                 continue;
308             }
309             final String prefKey = entry.getKey();
310             AnomalyAppItemPreference pref = mAppListPreferenceGroup.findPreference(prefKey);
311             if (pref != null) {
312                 isAdded = true;
313             } else {
314                 pref = (AnomalyAppItemPreference) mPreferenceCache.get(prefKey);
315             }
316             // Creates new instance if cached preference is not found.
317             if (pref == null) {
318                 pref = new AnomalyAppItemPreference(mPrefContext);
319                 pref.setKey(prefKey);
320                 mPreferenceCache.put(prefKey, pref);
321             }
322             pref.setIcon(appIcon);
323             pref.setTitle(appLabel);
324             pref.setOrder(prefIndex);
325             pref.setSingleLineTitle(true);
326             // Updates App item preference style
327             pref.setAnomalyHint(mIsHighlightSlot && mAnomalyEntryKey != null
328                     && mAnomalyEntryKey.equals(entry.getKey())
329                     ? mAnomalyHintString : null);
330             // Sets the BatteryDiffEntry to preference for launching detailed page.
331             pref.setBatteryDiffEntry(entry);
332             pref.setSelectable(entry.validForRestriction());
333             setPreferencePercentage(pref, entry);
334             setPreferenceSummary(pref, entry);
335             if (!isAdded) {
336                 mAppListPreferenceGroup.addPreference(pref);
337             }
338             prefIndex++;
339         }
340         Log.d(TAG, String.format("addAllPreferences() is finished in %d/ms",
341                 (System.currentTimeMillis() - start)));
342     }
343 
344     @VisibleForTesting
removeAndCacheAllUnusedPreferences()345     void removeAndCacheAllUnusedPreferences() {
346         List<BatteryDiffEntry> entries = getBatteryDiffEntries();
347         Set<String> entryKeySet = new ArraySet<>(entries.size());
348         entries.forEach(entry -> entryKeySet.add(entry.getKey()));
349         final int prefsCount = mAppListPreferenceGroup.getPreferenceCount();
350         for (int index = prefsCount - 1; index >= 0; index--) {
351             final Preference pref = mAppListPreferenceGroup.getPreference(index);
352             if (entryKeySet.contains(pref.getKey())) {
353                 // The pref is still used, don't remove.
354                 continue;
355             }
356             if (!TextUtils.isEmpty(pref.getKey())) {
357                 mPreferenceCache.put(pref.getKey(), pref);
358             }
359             mAppListPreferenceGroup.removePreference(pref);
360         }
361     }
362 
363     @VisibleForTesting
setPreferencePercentage( PowerGaugePreference preference, BatteryDiffEntry entry)364     void setPreferencePercentage(
365             PowerGaugePreference preference, BatteryDiffEntry entry) {
366         preference.setPercentage(
367                 entry.getPercentage() < BatteryDiffData.SMALL_PERCENTAGE_THRESHOLD
368                         ? mPercentLessThanThresholdText
369                         : Utils.formatPercentage(
370                                 entry.getPercentage() + entry.getAdjustPercentageOffset(),
371                                 /* round= */ true));
372     }
373 
374     @VisibleForTesting
375     void setPreferenceSummary(
376             PowerGaugePreference preference, BatteryDiffEntry entry) {
377         preference.setSummary(
378                 BatteryUtils.buildBatteryUsageTimeSummary(mPrefContext, entry.isSystemEntry(),
379                         entry.mForegroundUsageTimeInMs, entry.mBackgroundUsageTimeInMs,
380                         entry.mScreenOnTimeInMs));
381     }
382 }
383