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