1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 * 15 */ 16 17 package com.android.settings.fuelgauge; 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.AsyncTask; 24 import android.os.Bundle; 25 import android.os.Handler; 26 import android.os.Looper; 27 import android.text.TextUtils; 28 import android.text.format.DateFormat; 29 import android.text.format.DateUtils; 30 import android.util.Log; 31 32 import androidx.annotation.VisibleForTesting; 33 import androidx.preference.Preference; 34 import androidx.preference.PreferenceGroup; 35 import androidx.preference.PreferenceScreen; 36 37 import com.android.settings.R; 38 import com.android.settings.SettingsActivity; 39 import com.android.settings.core.InstrumentedPreferenceFragment; 40 import com.android.settings.core.PreferenceControllerMixin; 41 import com.android.settings.overlay.FeatureFactory; 42 import com.android.settingslib.core.AbstractPreferenceController; 43 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; 44 import com.android.settingslib.core.lifecycle.Lifecycle; 45 import com.android.settingslib.core.lifecycle.LifecycleObserver; 46 import com.android.settingslib.core.lifecycle.events.OnCreate; 47 import com.android.settingslib.core.lifecycle.events.OnDestroy; 48 import com.android.settingslib.core.lifecycle.events.OnResume; 49 import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState; 50 import com.android.settingslib.utils.StringUtil; 51 import com.android.settingslib.widget.FooterPreference; 52 53 import java.util.ArrayList; 54 import java.util.Arrays; 55 import java.util.Collections; 56 import java.util.HashMap; 57 import java.util.List; 58 import java.util.Map; 59 60 /** Controls the update for chart graph and the list items. */ 61 public class BatteryChartPreferenceController extends AbstractPreferenceController 62 implements PreferenceControllerMixin, LifecycleObserver, OnCreate, OnDestroy, 63 OnSaveInstanceState, BatteryChartView.OnSelectListener, OnResume, 64 ExpandDividerPreference.OnExpandListener { 65 private static final String TAG = "BatteryChartPreferenceController"; 66 private static final String KEY_FOOTER_PREF = "battery_graph_footer"; 67 private static final String PACKAGE_NAME_NONE = "none"; 68 69 /** Desired battery history size for timestamp slots. */ 70 public static final int DESIRED_HISTORY_SIZE = 25; 71 private static final int CHART_LEVEL_ARRAY_SIZE = 13; 72 private static final int CHART_KEY_ARRAY_SIZE = DESIRED_HISTORY_SIZE; 73 private static final long VALID_USAGE_TIME_DURATION = DateUtils.HOUR_IN_MILLIS * 2; 74 private static final long VALID_DIFF_DURATION = DateUtils.MINUTE_IN_MILLIS * 3; 75 76 // Keys for bundle instance to restore configurations. 77 private static final String KEY_EXPAND_SYSTEM_INFO = "expand_system_info"; 78 private static final String KEY_CURRENT_TIME_SLOT = "current_time_slot"; 79 80 private static int sUiMode = Configuration.UI_MODE_NIGHT_UNDEFINED; 81 82 @VisibleForTesting 83 Map<Integer, List<BatteryDiffEntry>> mBatteryIndexedMap; 84 85 @VisibleForTesting Context mPrefContext; 86 @VisibleForTesting BatteryUtils mBatteryUtils; 87 @VisibleForTesting PreferenceGroup mAppListPrefGroup; 88 @VisibleForTesting BatteryChartView mBatteryChartView; 89 @VisibleForTesting ExpandDividerPreference mExpandDividerPreference; 90 91 @VisibleForTesting boolean mIsExpanded = false; 92 @VisibleForTesting int[] mBatteryHistoryLevels; 93 @VisibleForTesting long[] mBatteryHistoryKeys; 94 @VisibleForTesting int mTrapezoidIndex = BatteryChartView.SELECTED_INDEX_INVALID; 95 96 private boolean mIs24HourFormat = false; 97 private boolean mIsFooterPrefAdded = false; 98 private PreferenceScreen mPreferenceScreen; 99 private FooterPreference mFooterPreference; 100 101 private final String mPreferenceKey; 102 private final SettingsActivity mActivity; 103 private final InstrumentedPreferenceFragment mFragment; 104 private final CharSequence[] mNotAllowShowEntryPackages; 105 private final CharSequence[] mNotAllowShowSummaryPackages; 106 private final MetricsFeatureProvider mMetricsFeatureProvider; 107 private final Handler mHandler = new Handler(Looper.getMainLooper()); 108 109 // Preference cache to avoid create new instance each time. 110 @VisibleForTesting 111 final Map<String, Preference> mPreferenceCache = new HashMap<>(); 112 @VisibleForTesting 113 final List<BatteryDiffEntry> mSystemEntries = new ArrayList<>(); 114 BatteryChartPreferenceController( Context context, String preferenceKey, Lifecycle lifecycle, SettingsActivity activity, InstrumentedPreferenceFragment fragment)115 public BatteryChartPreferenceController( 116 Context context, String preferenceKey, 117 Lifecycle lifecycle, SettingsActivity activity, 118 InstrumentedPreferenceFragment fragment) { 119 super(context); 120 mActivity = activity; 121 mFragment = fragment; 122 mPreferenceKey = preferenceKey; 123 mIs24HourFormat = DateFormat.is24HourFormat(context); 124 mMetricsFeatureProvider = 125 FeatureFactory.getFactory(mContext).getMetricsFeatureProvider(); 126 mNotAllowShowEntryPackages = 127 FeatureFactory.getFactory(context) 128 .getPowerUsageFeatureProvider(context) 129 .getHideApplicationEntries(context); 130 mNotAllowShowSummaryPackages = 131 FeatureFactory.getFactory(context) 132 .getPowerUsageFeatureProvider(context) 133 .getHideApplicationSummary(context); 134 if (lifecycle != null) { 135 lifecycle.addObserver(this); 136 } 137 } 138 139 @Override onCreate(Bundle savedInstanceState)140 public void onCreate(Bundle savedInstanceState) { 141 if (savedInstanceState == null) { 142 return; 143 } 144 mTrapezoidIndex = 145 savedInstanceState.getInt(KEY_CURRENT_TIME_SLOT, mTrapezoidIndex); 146 mIsExpanded = 147 savedInstanceState.getBoolean(KEY_EXPAND_SYSTEM_INFO, mIsExpanded); 148 Log.d(TAG, String.format("onCreate() slotIndex=%d isExpanded=%b", 149 mTrapezoidIndex, mIsExpanded)); 150 } 151 152 @Override onResume()153 public void onResume() { 154 final int currentUiMode = 155 mContext.getResources().getConfiguration().uiMode 156 & Configuration.UI_MODE_NIGHT_MASK; 157 if (sUiMode != currentUiMode) { 158 sUiMode = currentUiMode; 159 BatteryDiffEntry.clearCache(); 160 Log.d(TAG, "clear icon and label cache since uiMode is changed"); 161 } 162 mIs24HourFormat = DateFormat.is24HourFormat(mContext); 163 mMetricsFeatureProvider.action(mPrefContext, SettingsEnums.OPEN_BATTERY_USAGE); 164 } 165 166 @Override onSaveInstanceState(Bundle savedInstance)167 public void onSaveInstanceState(Bundle savedInstance) { 168 if (savedInstance == null) { 169 return; 170 } 171 savedInstance.putInt(KEY_CURRENT_TIME_SLOT, mTrapezoidIndex); 172 savedInstance.putBoolean(KEY_EXPAND_SYSTEM_INFO, mIsExpanded); 173 Log.d(TAG, String.format("onSaveInstanceState() slotIndex=%d isExpanded=%b", 174 mTrapezoidIndex, mIsExpanded)); 175 } 176 177 @Override onDestroy()178 public void onDestroy() { 179 if (mActivity.isChangingConfigurations()) { 180 BatteryDiffEntry.clearCache(); 181 } 182 mHandler.removeCallbacksAndMessages(/*token=*/ null); 183 mPreferenceCache.clear(); 184 if (mAppListPrefGroup != null) { 185 mAppListPrefGroup.removeAll(); 186 } 187 } 188 189 @Override displayPreference(PreferenceScreen screen)190 public void displayPreference(PreferenceScreen screen) { 191 super.displayPreference(screen); 192 mPreferenceScreen = screen; 193 mPrefContext = screen.getContext(); 194 mAppListPrefGroup = screen.findPreference(mPreferenceKey); 195 mAppListPrefGroup.setOrderingAsAdded(false); 196 mAppListPrefGroup.setTitle( 197 mPrefContext.getString(R.string.battery_app_usage_for_past_24)); 198 mFooterPreference = screen.findPreference(KEY_FOOTER_PREF); 199 // Removes footer first until usage data is loaded to avoid flashing. 200 if (mFooterPreference != null) { 201 screen.removePreference(mFooterPreference); 202 } 203 } 204 205 @Override isAvailable()206 public boolean isAvailable() { 207 return true; 208 } 209 210 @Override getPreferenceKey()211 public String getPreferenceKey() { 212 return mPreferenceKey; 213 } 214 215 @Override handlePreferenceTreeClick(Preference preference)216 public boolean handlePreferenceTreeClick(Preference preference) { 217 if (!(preference instanceof PowerGaugePreference)) { 218 return false; 219 } 220 final PowerGaugePreference powerPref = (PowerGaugePreference) preference; 221 final BatteryDiffEntry diffEntry = powerPref.getBatteryDiffEntry(); 222 final BatteryHistEntry histEntry = diffEntry.mBatteryHistEntry; 223 final String packageName = histEntry.mPackageName; 224 final boolean isAppEntry = histEntry.isAppEntry(); 225 mMetricsFeatureProvider.action( 226 /* attribution */ SettingsEnums.OPEN_BATTERY_USAGE, 227 /* action */ isAppEntry 228 ? SettingsEnums.ACTION_BATTERY_USAGE_APP_ITEM 229 : SettingsEnums.ACTION_BATTERY_USAGE_SYSTEM_ITEM, 230 /* pageId */ SettingsEnums.OPEN_BATTERY_USAGE, 231 TextUtils.isEmpty(packageName) ? PACKAGE_NAME_NONE : packageName, 232 (int) Math.round(diffEntry.getPercentOfTotal())); 233 Log.d(TAG, String.format("handleClick() label=%s key=%s package=%s", 234 diffEntry.getAppLabel(), histEntry.getKey(), histEntry.mPackageName)); 235 AdvancedPowerUsageDetail.startBatteryDetailPage( 236 mActivity, mFragment, diffEntry, powerPref.getPercent(), 237 isValidToShowSummary(packageName), getSlotInformation()); 238 return true; 239 } 240 241 @Override onSelect(int trapezoidIndex)242 public void onSelect(int trapezoidIndex) { 243 Log.d(TAG, "onChartSelect:" + trapezoidIndex); 244 refreshUi(trapezoidIndex, /*isForce=*/ false); 245 mMetricsFeatureProvider.action( 246 mPrefContext, 247 trapezoidIndex == BatteryChartView.SELECTED_INDEX_ALL 248 ? SettingsEnums.ACTION_BATTERY_USAGE_SHOW_ALL 249 : SettingsEnums.ACTION_BATTERY_USAGE_TIME_SLOT); 250 } 251 252 @Override onExpand(boolean isExpanded)253 public void onExpand(boolean isExpanded) { 254 mIsExpanded = isExpanded; 255 mMetricsFeatureProvider.action( 256 mPrefContext, 257 SettingsEnums.ACTION_BATTERY_USAGE_EXPAND_ITEM, 258 isExpanded); 259 refreshExpandUi(); 260 } 261 setBatteryHistoryMap( final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap)262 void setBatteryHistoryMap( 263 final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) { 264 // Resets all battery history data relative variables. 265 if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) { 266 mBatteryIndexedMap = null; 267 mBatteryHistoryKeys = null; 268 mBatteryHistoryLevels = null; 269 addFooterPreferenceIfNeeded(false); 270 return; 271 } 272 mBatteryHistoryKeys = getBatteryHistoryKeys(batteryHistoryMap); 273 mBatteryHistoryLevels = new int[CHART_LEVEL_ARRAY_SIZE]; 274 for (int index = 0; index < CHART_LEVEL_ARRAY_SIZE; index++) { 275 final long timestamp = mBatteryHistoryKeys[index * 2]; 276 final Map<String, BatteryHistEntry> entryMap = batteryHistoryMap.get(timestamp); 277 if (entryMap == null || entryMap.isEmpty()) { 278 Log.e(TAG, "abnormal entry list in the timestamp:" 279 + ConvertUtils.utcToLocalTime(mPrefContext, timestamp)); 280 continue; 281 } 282 // Averages the battery level in each time slot to avoid corner conditions. 283 float batteryLevelCounter = 0; 284 for (BatteryHistEntry entry : entryMap.values()) { 285 batteryLevelCounter += entry.mBatteryLevel; 286 } 287 mBatteryHistoryLevels[index] = 288 Math.round(batteryLevelCounter / entryMap.size()); 289 } 290 forceRefreshUi(); 291 Log.d(TAG, String.format( 292 "setBatteryHistoryMap() size=%d key=%s\nlevels=%s", 293 batteryHistoryMap.size(), 294 ConvertUtils.utcToLocalTime(mPrefContext, 295 mBatteryHistoryKeys[mBatteryHistoryKeys.length - 1]), 296 Arrays.toString(mBatteryHistoryLevels))); 297 298 // Loads item icon and label in the background. 299 new LoadAllItemsInfoTask(batteryHistoryMap).execute(); 300 } 301 setBatteryChartView(final BatteryChartView batteryChartView)302 void setBatteryChartView(final BatteryChartView batteryChartView) { 303 if (mBatteryChartView != batteryChartView) { 304 mHandler.post(() -> setBatteryChartViewInner(batteryChartView)); 305 } 306 } 307 setBatteryChartViewInner(final BatteryChartView batteryChartView)308 private void setBatteryChartViewInner(final BatteryChartView batteryChartView) { 309 mBatteryChartView = batteryChartView; 310 mBatteryChartView.setOnSelectListener(this); 311 forceRefreshUi(); 312 } 313 forceRefreshUi()314 private void forceRefreshUi() { 315 final int refreshIndex = 316 mTrapezoidIndex == BatteryChartView.SELECTED_INDEX_INVALID 317 ? BatteryChartView.SELECTED_INDEX_ALL 318 : mTrapezoidIndex; 319 if (mBatteryChartView != null) { 320 mBatteryChartView.setLevels(mBatteryHistoryLevels); 321 mBatteryChartView.setSelectedIndex(refreshIndex); 322 setTimestampLabel(); 323 } 324 refreshUi(refreshIndex, /*isForce=*/ true); 325 } 326 327 @VisibleForTesting refreshUi(int trapezoidIndex, boolean isForce)328 boolean refreshUi(int trapezoidIndex, boolean isForce) { 329 // Invalid refresh condition. 330 if (mBatteryIndexedMap == null 331 || mBatteryChartView == null 332 || (mTrapezoidIndex == trapezoidIndex && !isForce)) { 333 return false; 334 } 335 Log.d(TAG, String.format("refreshUi: index=%d size=%d isForce:%b", 336 trapezoidIndex, mBatteryIndexedMap.size(), isForce)); 337 338 mTrapezoidIndex = trapezoidIndex; 339 mHandler.post(() -> { 340 final long start = System.currentTimeMillis(); 341 removeAndCacheAllPrefs(); 342 addAllPreferences(); 343 refreshCategoryTitle(); 344 Log.d(TAG, String.format("refreshUi is finished in %d/ms", 345 (System.currentTimeMillis() - start))); 346 }); 347 return true; 348 } 349 addAllPreferences()350 private void addAllPreferences() { 351 final List<BatteryDiffEntry> entries = 352 mBatteryIndexedMap.get(Integer.valueOf(mTrapezoidIndex)); 353 addFooterPreferenceIfNeeded(entries != null && !entries.isEmpty()); 354 if (entries == null) { 355 Log.w(TAG, "cannot find BatteryDiffEntry for:" + mTrapezoidIndex); 356 return; 357 } 358 // Separates data into two groups and sort them individually. 359 final List<BatteryDiffEntry> appEntries = new ArrayList<>(); 360 mSystemEntries.clear(); 361 entries.forEach(entry -> { 362 final String packageName = entry.getPackageName(); 363 if (!isValidToShowEntry(packageName)) { 364 Log.w(TAG, "ignore showing item:" + packageName); 365 return; 366 } 367 if (entry.isSystemEntry()) { 368 mSystemEntries.add(entry); 369 } else { 370 appEntries.add(entry); 371 } 372 // Validates the usage time if users click a specific slot. 373 if (mTrapezoidIndex >= 0) { 374 validateUsageTime(entry); 375 } 376 }); 377 Collections.sort(appEntries, BatteryDiffEntry.COMPARATOR); 378 Collections.sort(mSystemEntries, BatteryDiffEntry.COMPARATOR); 379 Log.d(TAG, String.format("addAllPreferences() app=%d system=%d", 380 appEntries.size(), mSystemEntries.size())); 381 382 // Adds app entries to the list if it is not empty. 383 if (!appEntries.isEmpty()) { 384 addPreferenceToScreen(appEntries); 385 } 386 // Adds the expabable divider if we have system entries data. 387 if (!mSystemEntries.isEmpty()) { 388 if (mExpandDividerPreference == null) { 389 mExpandDividerPreference = new ExpandDividerPreference(mPrefContext); 390 mExpandDividerPreference.setOnExpandListener(this); 391 mExpandDividerPreference.setIsExpanded(mIsExpanded); 392 } 393 mExpandDividerPreference.setOrder( 394 mAppListPrefGroup.getPreferenceCount()); 395 mAppListPrefGroup.addPreference(mExpandDividerPreference); 396 } 397 refreshExpandUi(); 398 } 399 400 @VisibleForTesting addPreferenceToScreen(List<BatteryDiffEntry> entries)401 void addPreferenceToScreen(List<BatteryDiffEntry> entries) { 402 if (mAppListPrefGroup == null || entries.isEmpty()) { 403 return; 404 } 405 int prefIndex = mAppListPrefGroup.getPreferenceCount(); 406 for (BatteryDiffEntry entry : entries) { 407 boolean isAdded = false; 408 final String appLabel = entry.getAppLabel(); 409 final Drawable appIcon = entry.getAppIcon(); 410 if (TextUtils.isEmpty(appLabel) || appIcon == null) { 411 Log.w(TAG, "cannot find app resource for:" + entry.getPackageName()); 412 continue; 413 } 414 final String prefKey = entry.mBatteryHistEntry.getKey(); 415 PowerGaugePreference pref = mAppListPrefGroup.findPreference(prefKey); 416 if (pref != null) { 417 isAdded = true; 418 Log.w(TAG, "preference should be removed for:" + entry.getPackageName()); 419 } else { 420 pref = (PowerGaugePreference) mPreferenceCache.get(prefKey); 421 } 422 // Creates new innstance if cached preference is not found. 423 if (pref == null) { 424 pref = new PowerGaugePreference(mPrefContext); 425 pref.setKey(prefKey); 426 mPreferenceCache.put(prefKey, pref); 427 } 428 pref.setIcon(appIcon); 429 pref.setTitle(appLabel); 430 pref.setOrder(prefIndex); 431 pref.setPercent(entry.getPercentOfTotal()); 432 pref.setSingleLineTitle(true); 433 // Sets the BatteryDiffEntry to preference for launching detailed page. 434 pref.setBatteryDiffEntry(entry); 435 pref.setEnabled(entry.validForRestriction()); 436 setPreferenceSummary(pref, entry); 437 if (!isAdded) { 438 mAppListPrefGroup.addPreference(pref); 439 } 440 prefIndex++; 441 } 442 } 443 removeAndCacheAllPrefs()444 private void removeAndCacheAllPrefs() { 445 if (mAppListPrefGroup == null 446 || mAppListPrefGroup.getPreferenceCount() == 0) { 447 return; 448 } 449 final int prefsCount = mAppListPrefGroup.getPreferenceCount(); 450 for (int index = 0; index < prefsCount; index++) { 451 final Preference pref = mAppListPrefGroup.getPreference(index); 452 if (TextUtils.isEmpty(pref.getKey())) { 453 continue; 454 } 455 mPreferenceCache.put(pref.getKey(), pref); 456 } 457 mAppListPrefGroup.removeAll(); 458 } 459 refreshExpandUi()460 private void refreshExpandUi() { 461 if (mIsExpanded) { 462 addPreferenceToScreen(mSystemEntries); 463 } else { 464 // Removes and recycles all system entries to hide all of them. 465 for (BatteryDiffEntry entry : mSystemEntries) { 466 final String prefKey = entry.mBatteryHistEntry.getKey(); 467 final Preference pref = mAppListPrefGroup.findPreference(prefKey); 468 if (pref != null) { 469 mAppListPrefGroup.removePreference(pref); 470 mPreferenceCache.put(pref.getKey(), pref); 471 } 472 } 473 } 474 } 475 476 @VisibleForTesting refreshCategoryTitle()477 void refreshCategoryTitle() { 478 final String slotInformation = getSlotInformation(); 479 Log.d(TAG, String.format("refreshCategoryTitle:%s", slotInformation)); 480 if (mAppListPrefGroup != null) { 481 mAppListPrefGroup.setTitle( 482 getSlotInformation(/*isApp=*/ true, slotInformation)); 483 } 484 if (mExpandDividerPreference != null) { 485 mExpandDividerPreference.setTitle( 486 getSlotInformation(/*isApp=*/ false, slotInformation)); 487 } 488 } 489 getSlotInformation(boolean isApp, String slotInformation)490 private String getSlotInformation(boolean isApp, String slotInformation) { 491 // Null means we show all information without a specific time slot. 492 if (slotInformation == null) { 493 return isApp 494 ? mPrefContext.getString(R.string.battery_app_usage_for_past_24) 495 : mPrefContext.getString(R.string.battery_system_usage_for_past_24); 496 } else { 497 return isApp 498 ? mPrefContext.getString(R.string.battery_app_usage_for, slotInformation) 499 : mPrefContext.getString(R.string.battery_system_usage_for ,slotInformation); 500 } 501 } 502 getSlotInformation()503 private String getSlotInformation() { 504 if (mTrapezoidIndex < 0) { 505 return null; 506 } 507 final String fromHour = ConvertUtils.utcToLocalTimeHour(mPrefContext, 508 mBatteryHistoryKeys[mTrapezoidIndex * 2], mIs24HourFormat); 509 final String toHour = ConvertUtils.utcToLocalTimeHour(mPrefContext, 510 mBatteryHistoryKeys[(mTrapezoidIndex + 1) * 2], mIs24HourFormat); 511 return String.format("%s - %s", fromHour, toHour); 512 } 513 514 @VisibleForTesting setPreferenceSummary( PowerGaugePreference preference, BatteryDiffEntry entry)515 void setPreferenceSummary( 516 PowerGaugePreference preference, BatteryDiffEntry entry) { 517 final long foregroundUsageTimeInMs = entry.mForegroundUsageTimeInMs; 518 final long backgroundUsageTimeInMs = entry.mBackgroundUsageTimeInMs; 519 final long totalUsageTimeInMs = foregroundUsageTimeInMs + backgroundUsageTimeInMs; 520 // Checks whether the package is allowed to show summary or not. 521 if (!isValidToShowSummary(entry.getPackageName())) { 522 preference.setSummary(null); 523 return; 524 } 525 String usageTimeSummary = null; 526 // Not shows summary for some system components without usage time. 527 if (totalUsageTimeInMs == 0) { 528 preference.setSummary(null); 529 // Shows background summary only if we don't have foreground usage time. 530 } else if (foregroundUsageTimeInMs == 0 && backgroundUsageTimeInMs != 0) { 531 usageTimeSummary = buildUsageTimeInfo(backgroundUsageTimeInMs, true); 532 // Shows total usage summary only if total usage time is small. 533 } else if (totalUsageTimeInMs < DateUtils.MINUTE_IN_MILLIS) { 534 usageTimeSummary = buildUsageTimeInfo(totalUsageTimeInMs, false); 535 } else { 536 usageTimeSummary = buildUsageTimeInfo(totalUsageTimeInMs, false); 537 // Shows background usage time if it is larger than a minute. 538 if (backgroundUsageTimeInMs > 0) { 539 usageTimeSummary += 540 "\n" + buildUsageTimeInfo(backgroundUsageTimeInMs, true); 541 } 542 } 543 preference.setSummary(usageTimeSummary); 544 } 545 buildUsageTimeInfo(long usageTimeInMs, boolean isBackground)546 private String buildUsageTimeInfo(long usageTimeInMs, boolean isBackground) { 547 if (usageTimeInMs < DateUtils.MINUTE_IN_MILLIS) { 548 return mPrefContext.getString( 549 isBackground 550 ? R.string.battery_usage_background_less_than_one_minute 551 : R.string.battery_usage_total_less_than_one_minute); 552 } 553 final CharSequence timeSequence = 554 StringUtil.formatElapsedTime(mPrefContext, usageTimeInMs, 555 /*withSeconds=*/ false, /*collapseTimeUnit=*/ false); 556 final int resourceId = 557 isBackground 558 ? R.string.battery_usage_for_background_time 559 : R.string.battery_usage_for_total_time; 560 return mPrefContext.getString(resourceId, timeSequence); 561 } 562 563 @VisibleForTesting isValidToShowSummary(String packageName)564 boolean isValidToShowSummary(String packageName) { 565 return !contains(packageName, mNotAllowShowSummaryPackages); 566 } 567 568 @VisibleForTesting isValidToShowEntry(String packageName)569 boolean isValidToShowEntry(String packageName) { 570 return !contains(packageName, mNotAllowShowEntryPackages); 571 } 572 573 @VisibleForTesting setTimestampLabel()574 void setTimestampLabel() { 575 if (mBatteryChartView == null || mBatteryHistoryKeys == null) { 576 return; 577 } 578 final long latestTimestamp = 579 mBatteryHistoryKeys[mBatteryHistoryKeys.length - 1]; 580 mBatteryChartView.setLatestTimestamp(latestTimestamp); 581 } 582 addFooterPreferenceIfNeeded(boolean containAppItems)583 private void addFooterPreferenceIfNeeded(boolean containAppItems) { 584 if (mIsFooterPrefAdded || mFooterPreference == null) { 585 return; 586 } 587 mIsFooterPrefAdded = true; 588 mFooterPreference.setTitle(mPrefContext.getString( 589 containAppItems 590 ? R.string.battery_usage_screen_footer 591 : R.string.battery_usage_screen_footer_empty)); 592 mHandler.post(() -> mPreferenceScreen.addPreference(mFooterPreference)); 593 } 594 contains(String target, CharSequence[] packageNames)595 private static boolean contains(String target, CharSequence[] packageNames) { 596 if (target != null && packageNames != null) { 597 for (CharSequence packageName : packageNames) { 598 if (TextUtils.equals(target, packageName)) { 599 return true; 600 } 601 } 602 } 603 return false; 604 } 605 606 @VisibleForTesting validateUsageTime(BatteryDiffEntry entry)607 static boolean validateUsageTime(BatteryDiffEntry entry) { 608 final long foregroundUsageTimeInMs = entry.mForegroundUsageTimeInMs; 609 final long backgroundUsageTimeInMs = entry.mBackgroundUsageTimeInMs; 610 final long totalUsageTimeInMs = foregroundUsageTimeInMs + backgroundUsageTimeInMs; 611 if (foregroundUsageTimeInMs > VALID_USAGE_TIME_DURATION 612 || backgroundUsageTimeInMs > VALID_USAGE_TIME_DURATION 613 || totalUsageTimeInMs > VALID_USAGE_TIME_DURATION) { 614 Log.e(TAG, "validateUsageTime() fail for\n" + entry); 615 return false; 616 } 617 return true; 618 } 619 getBatteryLast24HrUsageData(Context context)620 public static List<BatteryDiffEntry> getBatteryLast24HrUsageData(Context context) { 621 final long start = System.currentTimeMillis(); 622 final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap = 623 FeatureFactory.getFactory(context) 624 .getPowerUsageFeatureProvider(context) 625 .getBatteryHistory(context); 626 if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) { 627 return null; 628 } 629 Log.d(TAG, String.format("getBatteryLast24HrData() size=%d time=&d/ms", 630 batteryHistoryMap.size(), (System.currentTimeMillis() - start))); 631 final Map<Integer, List<BatteryDiffEntry>> batteryIndexedMap = 632 ConvertUtils.getIndexedUsageMap( 633 context, 634 /*timeSlotSize=*/ CHART_LEVEL_ARRAY_SIZE - 1, 635 getBatteryHistoryKeys(batteryHistoryMap), 636 batteryHistoryMap, 637 /*purgeLowPercentageAndFakeData=*/ true); 638 return batteryIndexedMap.get(BatteryChartView.SELECTED_INDEX_ALL); 639 } 640 getBatteryHistoryKeys( final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap)641 private static long[] getBatteryHistoryKeys( 642 final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) { 643 final List<Long> batteryHistoryKeyList = 644 new ArrayList<>(batteryHistoryMap.keySet()); 645 Collections.sort(batteryHistoryKeyList); 646 final long[] batteryHistoryKeys = new long[CHART_KEY_ARRAY_SIZE]; 647 for (int index = 0; index < CHART_KEY_ARRAY_SIZE; index++) { 648 batteryHistoryKeys[index] = batteryHistoryKeyList.get(index); 649 } 650 return batteryHistoryKeys; 651 } 652 653 // Loads all items icon and label in the background. 654 private final class LoadAllItemsInfoTask 655 extends AsyncTask<Void, Void, Map<Integer, List<BatteryDiffEntry>>> { 656 657 private long[] mBatteryHistoryKeysCache; 658 private Map<Long, Map<String, BatteryHistEntry>> mBatteryHistoryMap; 659 LoadAllItemsInfoTask( Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap)660 private LoadAllItemsInfoTask( 661 Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) { 662 this.mBatteryHistoryMap = batteryHistoryMap; 663 this.mBatteryHistoryKeysCache = mBatteryHistoryKeys; 664 } 665 666 @Override doInBackground(Void... voids)667 protected Map<Integer, List<BatteryDiffEntry>> doInBackground(Void... voids) { 668 if (mPrefContext == null || mBatteryHistoryKeysCache == null) { 669 return null; 670 } 671 final long startTime = System.currentTimeMillis(); 672 final Map<Integer, List<BatteryDiffEntry>> indexedUsageMap = 673 ConvertUtils.getIndexedUsageMap( 674 mPrefContext, /*timeSlotSize=*/ CHART_LEVEL_ARRAY_SIZE - 1, 675 mBatteryHistoryKeysCache, mBatteryHistoryMap, 676 /*purgeLowPercentageAndFakeData=*/ true); 677 // Pre-loads each BatteryDiffEntry relative icon and label for all slots. 678 for (List<BatteryDiffEntry> entries : indexedUsageMap.values()) { 679 entries.forEach(entry -> entry.loadLabelAndIcon()); 680 } 681 Log.d(TAG, String.format("execute LoadAllItemsInfoTask in %d/ms", 682 (System.currentTimeMillis() - startTime))); 683 return indexedUsageMap; 684 } 685 686 @Override onPostExecute( Map<Integer, List<BatteryDiffEntry>> indexedUsageMap)687 protected void onPostExecute( 688 Map<Integer, List<BatteryDiffEntry>> indexedUsageMap) { 689 mBatteryHistoryMap = null; 690 mBatteryHistoryKeysCache = null; 691 if (indexedUsageMap == null) { 692 return; 693 } 694 // Posts results back to main thread to refresh UI. 695 mHandler.post(() -> { 696 mBatteryIndexedMap = indexedUsageMap; 697 forceRefreshUi(); 698 }); 699 } 700 } 701 } 702