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