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