• 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.Bundle;
24 import android.os.Handler;
25 import android.os.Looper;
26 import android.text.TextUtils;
27 import android.util.ArrayMap;
28 import android.util.ArraySet;
29 import android.util.Log;
30 import android.view.View;
31 import android.widget.AdapterView;
32 
33 import androidx.preference.Preference;
34 import androidx.preference.PreferenceGroup;
35 import androidx.preference.PreferenceScreen;
36 
37 import com.android.internal.annotations.VisibleForTesting;
38 import com.android.settings.R;
39 import com.android.settings.SettingsActivity;
40 import com.android.settings.Utils;
41 import com.android.settings.core.BasePreferenceController;
42 import com.android.settings.core.InstrumentedPreferenceFragment;
43 import com.android.settings.fuelgauge.AdvancedPowerUsageDetail;
44 import com.android.settings.fuelgauge.BatteryUtils;
45 import com.android.settings.overlay.FeatureFactory;
46 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
47 import com.android.settingslib.core.lifecycle.Lifecycle;
48 import com.android.settingslib.core.lifecycle.LifecycleObserver;
49 import com.android.settingslib.core.lifecycle.events.OnCreate;
50 import com.android.settingslib.core.lifecycle.events.OnDestroy;
51 import com.android.settingslib.core.lifecycle.events.OnResume;
52 import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState;
53 import com.android.settingslib.widget.FooterPreference;
54 import com.android.settingslib.widget.SettingsSpinnerAdapter;
55 import com.android.settingslib.widget.SettingsSpinnerPreference;
56 
57 import java.util.ArrayList;
58 import java.util.List;
59 import java.util.Map;
60 import java.util.Optional;
61 import java.util.Set;
62 
63 /** Controller for battery usage breakdown preference group. */
64 public class BatteryUsageBreakdownController extends BasePreferenceController
65         implements LifecycleObserver, OnResume, OnDestroy, OnCreate, OnSaveInstanceState {
66     private static final String TAG = "BatteryUsageBreakdownController";
67     private static final String ROOT_PREFERENCE_KEY = "battery_usage_breakdown";
68     private static final String FOOTER_PREFERENCE_KEY = "battery_usage_footer";
69     private static final String SPINNER_PREFERENCE_KEY = "battery_usage_spinner";
70     private static final String PACKAGE_NAME_NONE = "none";
71     private static final String SLOT_TIMESTAMP = "slot_timestamp";
72     private static final String ANOMALY_KEY = "anomaly_key";
73     private static final String KEY_SPINNER_POSITION = "spinner_position";
74     private static final int ENTRY_PREF_ORDER_OFFSET = 100;
75     private static final List<BatteryDiffEntry> EMPTY_ENTRY_LIST = new ArrayList<>();
76 
77     private static int sUiMode = Configuration.UI_MODE_NIGHT_UNDEFINED;
78 
79     private final SettingsActivity mActivity;
80     private final InstrumentedPreferenceFragment mFragment;
81     private final MetricsFeatureProvider mMetricsFeatureProvider;
82     private final Handler mHandler = new Handler(Looper.getMainLooper());
83 
84     @VisibleForTesting final Map<String, Preference> mPreferenceCache = new ArrayMap<>();
85 
86     private String mSlotInformation;
87     private SettingsSpinnerPreference mSpinnerPreference;
88     private SettingsSpinnerAdapter<CharSequence> mSpinnerAdapter;
89 
90     @VisibleForTesting Context mPrefContext;
91     @VisibleForTesting PreferenceGroup mRootPreferenceGroup;
92     @VisibleForTesting FooterPreference mFooterPreference;
93     @VisibleForTesting BatteryDiffData mBatteryDiffData;
94     @VisibleForTesting String mBatteryUsageBreakdownTitleLastFullChargeText;
95     @VisibleForTesting String mPercentLessThanThresholdText;
96     @VisibleForTesting String mPercentLessThanThresholdContentDescription;
97     @VisibleForTesting boolean mIsHighlightSlot;
98     @VisibleForTesting int mAnomalyKeyNumber;
99     @VisibleForTesting int mSpinnerPosition;
100     @VisibleForTesting String mAnomalyEntryKey;
101     @VisibleForTesting String mAnomalyHintString;
102     @VisibleForTesting String mAnomalyHintPrefKey;
103 
BatteryUsageBreakdownController( Context context, Lifecycle lifecycle, SettingsActivity activity, InstrumentedPreferenceFragment fragment)104     public BatteryUsageBreakdownController(
105             Context context,
106             Lifecycle lifecycle,
107             SettingsActivity activity,
108             InstrumentedPreferenceFragment fragment) {
109         super(context, ROOT_PREFERENCE_KEY);
110         mActivity = activity;
111         mFragment = fragment;
112         mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
113         if (lifecycle != null) {
114             lifecycle.addObserver(this);
115         }
116     }
117 
118     @Override
onCreate(Bundle savedInstanceState)119     public void onCreate(Bundle savedInstanceState) {
120         if (savedInstanceState == null) {
121             return;
122         }
123         mSpinnerPosition = savedInstanceState.getInt(KEY_SPINNER_POSITION, mSpinnerPosition);
124         Log.d(TAG, "onCreate() spinnerPosition=" + mSpinnerPosition);
125     }
126 
127     @Override
onResume()128     public void onResume() {
129         final int currentUiMode =
130                 mContext.getResources().getConfiguration().uiMode
131                         & Configuration.UI_MODE_NIGHT_MASK;
132         if (sUiMode != currentUiMode) {
133             sUiMode = currentUiMode;
134             BatteryDiffEntry.clearCache();
135             mPreferenceCache.clear();
136             Log.d(TAG, "clear icon and label cache since uiMode is changed");
137         }
138     }
139 
140     @Override
onDestroy()141     public void onDestroy() {
142         mHandler.removeCallbacksAndMessages(/* token= */ null);
143         mPreferenceCache.clear();
144         mRootPreferenceGroup.removeAll();
145     }
146 
147     @Override
getAvailabilityStatus()148     public int getAvailabilityStatus() {
149         return AVAILABLE;
150     }
151 
152     @Override
isSliceable()153     public boolean isSliceable() {
154         return false;
155     }
156 
157     @Override
onSaveInstanceState(Bundle savedInstanceState)158     public void onSaveInstanceState(Bundle savedInstanceState) {
159         if (savedInstanceState == null) {
160             return;
161         }
162         savedInstanceState.putInt(KEY_SPINNER_POSITION, mSpinnerPosition);
163         Log.d(TAG, "onSaveInstanceState() spinnerPosition=" + mSpinnerPosition);
164     }
165 
isAnomalyBatteryDiffEntry(BatteryDiffEntry entry)166     private boolean isAnomalyBatteryDiffEntry(BatteryDiffEntry entry) {
167         return mIsHighlightSlot
168                 && mAnomalyEntryKey != null
169                 && mAnomalyEntryKey.equals(entry.getKey());
170     }
171 
logPreferenceClickedMetrics(BatteryDiffEntry entry)172     private void logPreferenceClickedMetrics(BatteryDiffEntry entry) {
173         final int attribution = SettingsEnums.OPEN_BATTERY_USAGE;
174         final int action =
175                 entry.isSystemEntry()
176                         ? SettingsEnums.ACTION_BATTERY_USAGE_SYSTEM_ITEM
177                         : SettingsEnums.ACTION_BATTERY_USAGE_APP_ITEM;
178         final int pageId = SettingsEnums.OPEN_BATTERY_USAGE;
179         final String packageName =
180                 TextUtils.isEmpty(entry.getPackageName())
181                         ? PACKAGE_NAME_NONE
182                         : entry.getPackageName();
183         final int percentage = (int) Math.round(entry.getPercentage());
184         final int slotTimestamp = (int) (mBatteryDiffData.getStartTimestamp() / 1000);
185         mMetricsFeatureProvider.action(attribution, action, pageId, packageName, percentage);
186         mMetricsFeatureProvider.action(attribution, action, pageId, SLOT_TIMESTAMP, slotTimestamp);
187 
188         if (isAnomalyBatteryDiffEntry(entry)) {
189             mMetricsFeatureProvider.action(
190                     attribution, action, pageId, ANOMALY_KEY, mAnomalyKeyNumber);
191         }
192     }
193 
194     @Override
handlePreferenceTreeClick(Preference preference)195     public boolean handlePreferenceTreeClick(Preference preference) {
196         if (!(preference instanceof PowerGaugePreference)) {
197             return false;
198         }
199         final PowerGaugePreference powerPref = (PowerGaugePreference) preference;
200         final BatteryDiffEntry diffEntry = powerPref.getBatteryDiffEntry();
201         logPreferenceClickedMetrics(diffEntry);
202         Log.d(
203                 TAG,
204                 String.format(
205                         "handleClick() label=%s key=%s package=%s",
206                         diffEntry.getAppLabel(), diffEntry.getKey(), diffEntry.getPackageName()));
207         final String anomalyHintPrefKey =
208                 isAnomalyBatteryDiffEntry(diffEntry) ? mAnomalyHintPrefKey : null;
209         final String anomalyHintText =
210                 isAnomalyBatteryDiffEntry(diffEntry) ? mAnomalyHintString : null;
211         AdvancedPowerUsageDetail.startBatteryDetailPage(
212                 mActivity,
213                 mFragment.getMetricsCategory(),
214                 diffEntry,
215                 powerPref.getPercentage(),
216                 mSlotInformation,
217                 /* showTimeInformation= */ true,
218                 anomalyHintPrefKey,
219                 anomalyHintText);
220         return true;
221     }
222 
223     @Override
displayPreference(PreferenceScreen screen)224     public void displayPreference(PreferenceScreen screen) {
225         super.displayPreference(screen);
226         mPrefContext = screen.getContext();
227         mRootPreferenceGroup = screen.findPreference(ROOT_PREFERENCE_KEY);
228         mSpinnerPreference = screen.findPreference(SPINNER_PREFERENCE_KEY);
229         mFooterPreference = screen.findPreference(FOOTER_PREFERENCE_KEY);
230         mBatteryUsageBreakdownTitleLastFullChargeText =
231                 mPrefContext.getString(
232                         R.string.battery_usage_breakdown_title_since_last_full_charge);
233         final String formatPercentage =
234                 Utils.formatPercentage(BatteryDiffData.SMALL_PERCENTAGE_THRESHOLD, false);
235         mPercentLessThanThresholdText =
236                 mPrefContext.getString(R.string.battery_usage_less_than_percent, formatPercentage);
237         mPercentLessThanThresholdContentDescription =
238                 mPrefContext.getString(
239                         R.string.battery_usage_less_than_percent_content_description,
240                         formatPercentage);
241 
242         mRootPreferenceGroup.setOrderingAsAdded(false);
243         mSpinnerAdapter = new SettingsSpinnerAdapter<>(mPrefContext);
244         mSpinnerAdapter.addAll(
245                 new String[] {
246                     mPrefContext.getString(R.string.battery_usage_spinner_view_by_apps),
247                     mPrefContext.getString(R.string.battery_usage_spinner_view_by_systems)
248                 });
249         mSpinnerPreference.setAdapter(mSpinnerAdapter);
250         mSpinnerPreference.setOnItemSelectedListener(
251                 new AdapterView.OnItemSelectedListener() {
252                     @Override
253                     public void onItemSelected(
254                             AdapterView<?> parent, View view, int position, long id) {
255                         if (mSpinnerPosition != position) {
256                             mSpinnerPosition = position;
257                             mHandler.post(
258                                     () -> {
259                                         removeAndCacheAllUnusedPreferences();
260                                         addAllPreferences();
261                                         mMetricsFeatureProvider.action(
262                                                 mPrefContext,
263                                                 SettingsEnums.ACTION_BATTERY_USAGE_SPINNER,
264                                                 mSpinnerPosition);
265                                     });
266                         }
267                     }
268 
269                     @Override
270                     public void onNothingSelected(AdapterView<?> parent) {}
271                 });
272         mSpinnerPreference.setSelection(mSpinnerPosition);
273     }
274 
275     /**
276      * Updates UI when the battery usage is updated.
277      *
278      * @param slotUsageData The battery usage diff data for the selected slot. This is used in the
279      *     app list.
280      * @param slotTimestamp The selected slot timestamp information. This is used in the battery
281      *     usage breakdown category.
282      * @param isAllUsageDataEmpty Whether all the battery usage data is null or empty. This is used
283      *     when showing the footer.
284      */
handleBatteryUsageUpdated( BatteryDiffData slotUsageData, String slotTimestamp, String accessibilitySlotTimestamp, boolean isAllUsageDataEmpty, boolean isHighlightSlot, Optional<AnomalyEventWrapper> optionalAnomalyEventWrapper)285     void handleBatteryUsageUpdated(
286             BatteryDiffData slotUsageData,
287             String slotTimestamp,
288             String accessibilitySlotTimestamp,
289             boolean isAllUsageDataEmpty,
290             boolean isHighlightSlot,
291             Optional<AnomalyEventWrapper> optionalAnomalyEventWrapper) {
292         mBatteryDiffData = slotUsageData;
293         mSlotInformation = slotTimestamp;
294         mIsHighlightSlot = isHighlightSlot;
295 
296         if (optionalAnomalyEventWrapper != null) {
297             final AnomalyEventWrapper anomalyEventWrapper =
298                     optionalAnomalyEventWrapper.orElse(null);
299             mAnomalyKeyNumber =
300                     anomalyEventWrapper != null ? anomalyEventWrapper.getAnomalyKeyNumber() : -1;
301             mAnomalyEntryKey =
302                     anomalyEventWrapper != null ? anomalyEventWrapper.getAnomalyEntryKey() : null;
303             mAnomalyHintString =
304                     anomalyEventWrapper != null ? anomalyEventWrapper.getAnomalyHintString() : null;
305             mAnomalyHintPrefKey =
306                     anomalyEventWrapper != null
307                             ? anomalyEventWrapper.getAnomalyHintPrefKey()
308                             : null;
309         }
310 
311         showCategoryTitle(slotTimestamp, accessibilitySlotTimestamp);
312         showSpinnerAndAppList();
313         showFooterPreference(isAllUsageDataEmpty);
314     }
315 
showCategoryTitle(String slotTimestamp, String accessibilitySlotTimestamp)316     private void showCategoryTitle(String slotTimestamp, String accessibilitySlotTimestamp) {
317         final String displayTitle =
318                 slotTimestamp == null
319                         ? mBatteryUsageBreakdownTitleLastFullChargeText
320                         : mPrefContext.getString(
321                                 R.string.battery_usage_breakdown_title_for_slot, slotTimestamp);
322         final String accessibilityTitle =
323                 accessibilitySlotTimestamp == null
324                         ? mBatteryUsageBreakdownTitleLastFullChargeText
325                         : mPrefContext.getString(
326                                 R.string.battery_usage_breakdown_title_for_slot,
327                                 accessibilitySlotTimestamp);
328         mRootPreferenceGroup.setTitle(
329                 Utils.createAccessibleSequence(displayTitle, accessibilityTitle));
330         mRootPreferenceGroup.setVisible(true);
331     }
332 
showFooterPreference(boolean isAllBatteryUsageEmpty)333     private void showFooterPreference(boolean isAllBatteryUsageEmpty) {
334         mFooterPreference.setTitle(
335                 mPrefContext.getString(
336                         isAllBatteryUsageEmpty
337                                 ? R.string.battery_usage_screen_footer_empty
338                                 : R.string.battery_usage_screen_footer));
339         mFooterPreference.setVisible(true);
340     }
341 
showSpinnerAndAppList()342     private void showSpinnerAndAppList() {
343         if (mBatteryDiffData == null) {
344             mHandler.post(
345                     () -> {
346                         removeAndCacheAllUnusedPreferences();
347                     });
348             return;
349         }
350         mSpinnerPreference.setVisible(true);
351         mHandler.post(
352                 () -> {
353                     removeAndCacheAllUnusedPreferences();
354                     addAllPreferences();
355                 });
356     }
357 
getBatteryDiffEntries()358     private List<BatteryDiffEntry> getBatteryDiffEntries() {
359         if (mBatteryDiffData == null) {
360             return EMPTY_ENTRY_LIST;
361         }
362         return mSpinnerPosition == 0
363                 ? mBatteryDiffData.getAppDiffEntryList()
364                 : mBatteryDiffData.getSystemDiffEntryList();
365     }
366 
367     @VisibleForTesting
addAllPreferences()368     void addAllPreferences() {
369         if (mBatteryDiffData == null) {
370             return;
371         }
372         final long start = System.currentTimeMillis();
373         final List<BatteryDiffEntry> entries = getBatteryDiffEntries();
374         int preferenceOrder = ENTRY_PREF_ORDER_OFFSET;
375         for (BatteryDiffEntry entry : entries) {
376             boolean isAdded = false;
377             final String appLabel = entry.getAppLabel();
378             final Drawable appIcon = entry.getAppIcon();
379             if (TextUtils.isEmpty(appLabel) || appIcon == null) {
380                 Log.w(TAG, "cannot find app resource for:" + entry.getPackageName());
381                 continue;
382             }
383             final String prefKey = entry.getKey();
384             PowerGaugePreference preference = mRootPreferenceGroup.findPreference(prefKey);
385             if (preference != null) {
386                 isAdded = true;
387             } else {
388                 preference = (PowerGaugePreference) mPreferenceCache.get(prefKey);
389             }
390             // Creates new instance if cached preference is not found.
391             if (preference == null) {
392                 preference = new PowerGaugePreference(mPrefContext);
393                 preference.setKey(prefKey);
394                 mPreferenceCache.put(prefKey, preference);
395             }
396             preference.setIcon(appIcon);
397             preference.setTitle(appLabel);
398             preference.setOrder(++preferenceOrder);
399             preference.setSingleLineTitle(true);
400             // Updates App item preference style
401             preference.setHint(isAnomalyBatteryDiffEntry(entry) ? mAnomalyHintString : null);
402             // Sets the BatteryDiffEntry to preference for launching detailed page.
403             preference.setBatteryDiffEntry(entry);
404             preference.setSelectable(entry.validForRestriction());
405             setPreferencePercentage(preference, entry);
406             setPreferenceSummary(preference, entry);
407             if (!isAdded) {
408                 mRootPreferenceGroup.addPreference(preference);
409             }
410         }
411         Log.d(
412                 TAG,
413                 String.format(
414                         "addAllPreferences() is finished in %d/ms",
415                         (System.currentTimeMillis() - start)));
416     }
417 
418     @VisibleForTesting
removeAndCacheAllUnusedPreferences()419     void removeAndCacheAllUnusedPreferences() {
420         List<BatteryDiffEntry> entries = getBatteryDiffEntries();
421         Set<String> entryKeySet = new ArraySet<>(entries.size());
422         entries.forEach(entry -> entryKeySet.add(entry.getKey()));
423         final int preferenceCount = mRootPreferenceGroup.getPreferenceCount();
424         for (int index = preferenceCount - 1; index >= 0; index--) {
425             final Preference preference = mRootPreferenceGroup.getPreference(index);
426             if ((preference instanceof SettingsSpinnerPreference)
427                     || (preference instanceof FooterPreference)) {
428                 // Consider the app preference only and skip others
429                 continue;
430             }
431             if (entryKeySet.contains(preference.getKey())) {
432                 // Don't remove the preference if it is still in use
433                 continue;
434             }
435             if (!TextUtils.isEmpty(preference.getKey())) {
436                 mPreferenceCache.put(preference.getKey(), preference);
437             }
438             mRootPreferenceGroup.removePreference(preference);
439         }
440     }
441 
442     @VisibleForTesting
setPreferencePercentage(PowerGaugePreference preference, BatteryDiffEntry entry)443     void setPreferencePercentage(PowerGaugePreference preference, BatteryDiffEntry entry) {
444         if (entry.getPercentage() < BatteryDiffData.SMALL_PERCENTAGE_THRESHOLD) {
445             preference.setPercentage(mPercentLessThanThresholdText);
446             preference.setPercentageContentDescription(mPercentLessThanThresholdContentDescription);
447         } else {
448             preference.setPercentage(
449                     Utils.formatPercentage(
450                             entry.getPercentage() + entry.getAdjustPercentageOffset(),
451                             /* round= */ true));
452         }
453     }
454 
455     @VisibleForTesting
setPreferenceSummary(PowerGaugePreference preference, BatteryDiffEntry entry)456     void setPreferenceSummary(PowerGaugePreference preference, BatteryDiffEntry entry) {
457         preference.setSummary(
458                 BatteryUtils.buildBatteryUsageTimeSummary(
459                         mPrefContext,
460                         entry.isSystemEntry(),
461                         entry.mForegroundUsageTimeInMs,
462                         entry.mBackgroundUsageTimeInMs + entry.mForegroundServiceUsageTimeInMs,
463                         entry.mScreenOnTimeInMs));
464     }
465 }
466