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