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 static com.android.settings.fuelgauge.batteryusage.BatteryChartViewModel.SELECTED_INDEX_ALL; 20 import static com.android.settings.fuelgauge.batteryusage.BatteryChartViewModel.SELECTED_INDEX_INVALID; 21 22 import android.animation.Animator; 23 import android.animation.AnimatorListenerAdapter; 24 import android.app.settings.SettingsEnums; 25 import android.content.Context; 26 import android.os.Bundle; 27 import android.os.Handler; 28 import android.os.Looper; 29 import android.text.format.DateFormat; 30 import android.text.format.DateUtils; 31 import android.util.Log; 32 import android.view.View; 33 import android.widget.TextView; 34 35 import androidx.annotation.NonNull; 36 import androidx.annotation.VisibleForTesting; 37 import androidx.preference.PreferenceScreen; 38 39 import com.android.settings.R; 40 import com.android.settings.SettingsActivity; 41 import com.android.settings.Utils; 42 import com.android.settings.core.PreferenceControllerMixin; 43 import com.android.settings.overlay.FeatureFactory; 44 import com.android.settingslib.core.AbstractPreferenceController; 45 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; 46 import com.android.settingslib.core.lifecycle.Lifecycle; 47 import com.android.settingslib.core.lifecycle.LifecycleObserver; 48 import com.android.settingslib.core.lifecycle.events.OnCreate; 49 import com.android.settingslib.core.lifecycle.events.OnDestroy; 50 import com.android.settingslib.core.lifecycle.events.OnResume; 51 import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState; 52 53 import com.google.common.base.Objects; 54 55 import java.util.ArrayList; 56 import java.util.Calendar; 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, 63 LifecycleObserver, 64 OnCreate, 65 OnDestroy, 66 OnSaveInstanceState, 67 OnResume { 68 private static final String TAG = "BatteryChartPreferenceController"; 69 private static final String PREFERENCE_KEY = "battery_chart"; 70 71 private static final long FADE_IN_ANIMATION_DURATION = 400L; 72 private static final long FADE_OUT_ANIMATION_DURATION = 200L; 73 74 // Keys for bundle instance to restore configurations. 75 private static final String KEY_DAILY_CHART_INDEX = "daily_chart_index"; 76 private static final String KEY_HOURLY_CHART_INDEX = "hourly_chart_index"; 77 78 /** A callback listener for the selected index is updated. */ 79 interface OnSelectedIndexUpdatedListener { 80 /** The callback function for the selected index is updated. */ onSelectedIndexUpdated()81 void onSelectedIndexUpdated(); 82 } 83 84 @VisibleForTesting Context mPrefContext; 85 @VisibleForTesting TextView mChartSummaryTextView; 86 @VisibleForTesting BatteryChartView mDailyChartView; 87 @VisibleForTesting BatteryChartView mHourlyChartView; 88 @VisibleForTesting int mDailyChartIndex = SELECTED_INDEX_ALL; 89 @VisibleForTesting int mHourlyChartIndex = SELECTED_INDEX_ALL; 90 @VisibleForTesting int mDailyHighlightSlotIndex = SELECTED_INDEX_INVALID; 91 @VisibleForTesting int mHourlyHighlightSlotIndex = SELECTED_INDEX_INVALID; 92 93 private boolean mIs24HourFormat; 94 private View mBatteryChartViewGroup; 95 private BatteryChartViewModel mDailyViewModel; 96 private List<BatteryChartViewModel> mHourlyViewModels; 97 private OnSelectedIndexUpdatedListener mOnSelectedIndexUpdatedListener; 98 99 private final SettingsActivity mActivity; 100 private final MetricsFeatureProvider mMetricsFeatureProvider; 101 private final Handler mHandler = new Handler(Looper.getMainLooper()); 102 private final AnimatorListenerAdapter mHourlyChartFadeInAdapter = 103 createHourlyChartAnimatorListenerAdapter(/* visible= */ true); 104 private final AnimatorListenerAdapter mHourlyChartFadeOutAdapter = 105 createHourlyChartAnimatorListenerAdapter(/* visible= */ false); 106 107 @VisibleForTesting 108 final DailyChartLabelTextGenerator mDailyChartLabelTextGenerator = 109 new DailyChartLabelTextGenerator(); 110 111 @VisibleForTesting 112 final HourlyChartLabelTextGenerator mHourlyChartLabelTextGenerator = 113 new HourlyChartLabelTextGenerator(); 114 BatteryChartPreferenceController( Context context, Lifecycle lifecycle, SettingsActivity activity)115 public BatteryChartPreferenceController( 116 Context context, Lifecycle lifecycle, SettingsActivity activity) { 117 super(context); 118 mActivity = activity; 119 mIs24HourFormat = DateFormat.is24HourFormat(context); 120 mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider(); 121 if (lifecycle != null) { 122 lifecycle.addObserver(this); 123 } 124 } 125 126 @Override onCreate(Bundle savedInstanceState)127 public void onCreate(Bundle savedInstanceState) { 128 if (savedInstanceState == null) { 129 return; 130 } 131 mDailyChartIndex = savedInstanceState.getInt(KEY_DAILY_CHART_INDEX, mDailyChartIndex); 132 mHourlyChartIndex = savedInstanceState.getInt(KEY_HOURLY_CHART_INDEX, mHourlyChartIndex); 133 Log.d( 134 TAG, 135 String.format( 136 "onCreate() dailyIndex=%d hourlyIndex=%d", 137 mDailyChartIndex, mHourlyChartIndex)); 138 } 139 140 @Override onResume()141 public void onResume() { 142 mIs24HourFormat = DateFormat.is24HourFormat(mContext); 143 mMetricsFeatureProvider.action(mPrefContext, SettingsEnums.OPEN_BATTERY_USAGE); 144 } 145 146 @Override onSaveInstanceState(Bundle savedInstance)147 public void onSaveInstanceState(Bundle savedInstance) { 148 if (savedInstance == null) { 149 return; 150 } 151 savedInstance.putInt(KEY_DAILY_CHART_INDEX, mDailyChartIndex); 152 savedInstance.putInt(KEY_HOURLY_CHART_INDEX, mHourlyChartIndex); 153 Log.d( 154 TAG, 155 String.format( 156 "onSaveInstanceState() dailyIndex=%d hourlyIndex=%d", 157 mDailyChartIndex, mHourlyChartIndex)); 158 } 159 160 @Override onDestroy()161 public void onDestroy() { 162 if (mActivity == null || mActivity.isChangingConfigurations()) { 163 BatteryDiffEntry.clearCache(); 164 } 165 mHandler.removeCallbacksAndMessages(/* token= */ null); 166 } 167 168 @Override displayPreference(PreferenceScreen screen)169 public void displayPreference(PreferenceScreen screen) { 170 super.displayPreference(screen); 171 mPrefContext = screen.getContext(); 172 } 173 174 @Override isAvailable()175 public boolean isAvailable() { 176 return true; 177 } 178 179 @Override getPreferenceKey()180 public String getPreferenceKey() { 181 return PREFERENCE_KEY; 182 } 183 getDailyChartIndex()184 int getDailyChartIndex() { 185 return mDailyChartIndex; 186 } 187 getHourlyChartIndex()188 int getHourlyChartIndex() { 189 return mHourlyChartIndex; 190 } 191 setOnSelectedIndexUpdatedListener(OnSelectedIndexUpdatedListener listener)192 void setOnSelectedIndexUpdatedListener(OnSelectedIndexUpdatedListener listener) { 193 mOnSelectedIndexUpdatedListener = listener; 194 } 195 onBatteryLevelDataUpdate(final BatteryLevelData batteryLevelData)196 void onBatteryLevelDataUpdate(final BatteryLevelData batteryLevelData) { 197 Log.d(TAG, "onBatteryLevelDataUpdate: " + batteryLevelData); 198 mMetricsFeatureProvider.action( 199 mPrefContext, 200 SettingsEnums.ACTION_BATTERY_HISTORY_LOADED, 201 getTotalHours(batteryLevelData)); 202 203 if (batteryLevelData == null) { 204 mDailyChartIndex = SELECTED_INDEX_ALL; 205 mHourlyChartIndex = SELECTED_INDEX_ALL; 206 mDailyViewModel = null; 207 mHourlyViewModels = null; 208 refreshUi(); 209 return; 210 } 211 mDailyViewModel = 212 new BatteryChartViewModel( 213 batteryLevelData.getDailyBatteryLevels().getLevels(), 214 batteryLevelData.getDailyBatteryLevels().getTimestamps(), 215 BatteryChartViewModel.AxisLabelPosition.CENTER_OF_TRAPEZOIDS, 216 mDailyChartLabelTextGenerator); 217 mHourlyViewModels = new ArrayList<>(); 218 for (BatteryLevelData.PeriodBatteryLevelData hourlyBatteryLevelsPerDay : 219 batteryLevelData.getHourlyBatteryLevelsPerDay()) { 220 mHourlyViewModels.add( 221 new BatteryChartViewModel( 222 hourlyBatteryLevelsPerDay.getLevels(), 223 hourlyBatteryLevelsPerDay.getTimestamps(), 224 BatteryChartViewModel.AxisLabelPosition.BETWEEN_TRAPEZOIDS, 225 mHourlyChartLabelTextGenerator.updateSpecialCaseContext( 226 batteryLevelData))); 227 } 228 refreshUi(); 229 } 230 isHighlightSlotFocused()231 boolean isHighlightSlotFocused() { 232 return (mDailyHighlightSlotIndex != SELECTED_INDEX_INVALID 233 && mDailyHighlightSlotIndex == mDailyChartIndex 234 && mHourlyHighlightSlotIndex != SELECTED_INDEX_INVALID 235 && mHourlyHighlightSlotIndex == mHourlyChartIndex); 236 } 237 onHighlightSlotIndexUpdate(int dailyHighlightSlotIndex, int hourlyHighlightSlotIndex)238 void onHighlightSlotIndexUpdate(int dailyHighlightSlotIndex, int hourlyHighlightSlotIndex) { 239 mDailyHighlightSlotIndex = dailyHighlightSlotIndex; 240 mHourlyHighlightSlotIndex = hourlyHighlightSlotIndex; 241 refreshUi(); 242 if (mOnSelectedIndexUpdatedListener != null) { 243 mOnSelectedIndexUpdatedListener.onSelectedIndexUpdated(); 244 } 245 } 246 selectHighlightSlotIndex()247 void selectHighlightSlotIndex() { 248 if (mDailyHighlightSlotIndex == SELECTED_INDEX_INVALID 249 || mHourlyHighlightSlotIndex == SELECTED_INDEX_INVALID) { 250 return; 251 } 252 if (mDailyHighlightSlotIndex == mDailyChartIndex 253 && mHourlyHighlightSlotIndex == mHourlyChartIndex) { 254 return; 255 } 256 mDailyChartIndex = mDailyHighlightSlotIndex; 257 mHourlyChartIndex = mHourlyHighlightSlotIndex; 258 Log.d( 259 TAG, 260 String.format( 261 "onDailyChartSelect:%d, onHourlyChartSelect:%d", 262 mDailyChartIndex, mHourlyChartIndex)); 263 refreshUi(); 264 // The highlight slot must be selected. 265 mHandler.post( 266 () -> 267 mDailyChartView.setAccessibilityPaneTitle( 268 getAccessibilityAnnounceMessage(/* isSlotSelected= */ true))); 269 if (mOnSelectedIndexUpdatedListener != null) { 270 mOnSelectedIndexUpdatedListener.onSelectedIndexUpdated(); 271 } 272 } 273 setBatteryChartView( @onNull final BatteryChartView dailyChartView, @NonNull final BatteryChartView hourlyChartView)274 void setBatteryChartView( 275 @NonNull final BatteryChartView dailyChartView, 276 @NonNull final BatteryChartView hourlyChartView) { 277 final View parentView = (View) dailyChartView.getParent(); 278 if (parentView != null && parentView.getId() == R.id.battery_chart_group) { 279 mBatteryChartViewGroup = (View) dailyChartView.getParent(); 280 } 281 if (mDailyChartView != dailyChartView || mHourlyChartView != hourlyChartView) { 282 mHandler.post(() -> setBatteryChartViewInner(dailyChartView, hourlyChartView)); 283 } 284 if (mBatteryChartViewGroup != null) { 285 final View grandparentView = (View) mBatteryChartViewGroup.getParent(); 286 mChartSummaryTextView = 287 grandparentView != null 288 ? grandparentView.findViewById(R.id.chart_summary) 289 : null; 290 } 291 } 292 setBatteryChartViewInner( @onNull final BatteryChartView dailyChartView, @NonNull final BatteryChartView hourlyChartView)293 private void setBatteryChartViewInner( 294 @NonNull final BatteryChartView dailyChartView, 295 @NonNull final BatteryChartView hourlyChartView) { 296 mDailyChartView = dailyChartView; 297 mDailyChartView.setOnSelectListener( 298 trapezoidIndex -> { 299 if (mDailyChartIndex == trapezoidIndex) { 300 return; 301 } 302 Log.d(TAG, "onDailyChartSelect:" + trapezoidIndex); 303 mDailyChartIndex = trapezoidIndex; 304 mHourlyChartIndex = SELECTED_INDEX_ALL; 305 refreshUi(); 306 mHandler.post( 307 () -> 308 mDailyChartView.setAccessibilityPaneTitle( 309 getAccessibilityAnnounceMessage( 310 mDailyChartIndex != SELECTED_INDEX_ALL))); 311 mMetricsFeatureProvider.action( 312 mPrefContext, 313 trapezoidIndex == SELECTED_INDEX_ALL 314 ? SettingsEnums.ACTION_BATTERY_USAGE_DAILY_SHOW_ALL 315 : SettingsEnums.ACTION_BATTERY_USAGE_DAILY_TIME_SLOT, 316 mDailyChartIndex); 317 if (mOnSelectedIndexUpdatedListener != null) { 318 mOnSelectedIndexUpdatedListener.onSelectedIndexUpdated(); 319 } 320 }); 321 mHourlyChartView = hourlyChartView; 322 mHourlyChartView.setOnSelectListener( 323 trapezoidIndex -> { 324 if (mDailyChartIndex == SELECTED_INDEX_ALL) { 325 // This will happen when a daily slot and an hour slot are clicked together. 326 return; 327 } 328 if (mHourlyChartIndex == trapezoidIndex) { 329 return; 330 } 331 Log.d(TAG, "onHourlyChartSelect:" + trapezoidIndex); 332 mHourlyChartIndex = trapezoidIndex; 333 refreshUi(); 334 mHandler.post( 335 () -> 336 mHourlyChartView.setAccessibilityPaneTitle( 337 getAccessibilityAnnounceMessage( 338 mHourlyChartIndex != SELECTED_INDEX_ALL))); 339 mMetricsFeatureProvider.action( 340 mPrefContext, 341 trapezoidIndex == SELECTED_INDEX_ALL 342 ? SettingsEnums.ACTION_BATTERY_USAGE_SHOW_ALL 343 : SettingsEnums.ACTION_BATTERY_USAGE_TIME_SLOT, 344 mHourlyChartIndex); 345 if (mOnSelectedIndexUpdatedListener != null) { 346 mOnSelectedIndexUpdatedListener.onSelectedIndexUpdated(); 347 } 348 }); 349 refreshUi(); 350 } 351 352 // Show empty hourly chart view only if there is no valid battery usage data. showEmptyChart()353 void showEmptyChart() { 354 if (mDailyChartView == null || mHourlyChartView == null) { 355 // Chart views are not initialized. 356 return; 357 } 358 setChartSummaryVisible(true); 359 mDailyChartView.setVisibility(View.GONE); 360 mHourlyChartView.setVisibility(View.VISIBLE); 361 mHourlyChartView.setViewModel(null); 362 } 363 364 @VisibleForTesting refreshUi()365 void refreshUi() { 366 if (mDailyChartView == null || mHourlyChartView == null) { 367 // Chart views are not initialized. 368 return; 369 } 370 371 if (mDailyViewModel == null || mHourlyViewModels == null) { 372 setChartSummaryVisible(false); 373 mDailyChartView.setVisibility(View.GONE); 374 mHourlyChartView.setVisibility(View.GONE); 375 mDailyChartView.setViewModel(null); 376 mHourlyChartView.setViewModel(null); 377 return; 378 } 379 380 setChartSummaryVisible(true); 381 // Gets valid battery level data. 382 if (isBatteryLevelDataInOneDay()) { 383 // Only 1 day data, hide the daily chart view. 384 mDailyChartView.setVisibility(View.GONE); 385 mDailyChartIndex = 0; 386 } else { 387 mDailyChartView.setVisibility(View.VISIBLE); 388 if (mDailyChartIndex >= mDailyViewModel.size()) { 389 mDailyChartIndex = SELECTED_INDEX_ALL; 390 } 391 mDailyViewModel.setSelectedIndex(mDailyChartIndex); 392 mDailyViewModel.setHighlightSlotIndex(mDailyHighlightSlotIndex); 393 mDailyChartView.setViewModel(mDailyViewModel); 394 } 395 396 if (mDailyChartIndex == SELECTED_INDEX_ALL) { 397 // Multiple days are selected, hide the hourly chart view. 398 animateBatteryHourlyChartView(/* visible= */ false); 399 } else { 400 animateBatteryHourlyChartView(/* visible= */ true); 401 final BatteryChartViewModel hourlyViewModel = mHourlyViewModels.get(mDailyChartIndex); 402 if (mHourlyChartIndex >= hourlyViewModel.size()) { 403 mHourlyChartIndex = SELECTED_INDEX_ALL; 404 } 405 hourlyViewModel.setSelectedIndex(mHourlyChartIndex); 406 hourlyViewModel.setHighlightSlotIndex( 407 (mDailyChartIndex == mDailyHighlightSlotIndex) 408 ? mHourlyHighlightSlotIndex 409 : SELECTED_INDEX_INVALID); 410 mHourlyChartView.setViewModel(hourlyViewModel); 411 } 412 } 413 getSlotInformation(boolean isAccessibilityText)414 String getSlotInformation(boolean isAccessibilityText) { 415 if (mDailyViewModel == null || mHourlyViewModels == null) { 416 // No data 417 return null; 418 } 419 if (isAllSelected()) { 420 return null; 421 } 422 423 final String selectedDayText = 424 isAccessibilityText 425 ? mDailyViewModel.getContentDescription(mDailyChartIndex) 426 : mDailyViewModel.getFullText(mDailyChartIndex); 427 if (mHourlyChartIndex == SELECTED_INDEX_ALL) { 428 return selectedDayText; 429 } 430 431 final String selectedHourText = 432 isAccessibilityText 433 ? mHourlyViewModels 434 .get(mDailyChartIndex) 435 .getContentDescription(mHourlyChartIndex) 436 : mHourlyViewModels.get(mDailyChartIndex).getFullText(mHourlyChartIndex); 437 if (isBatteryLevelDataInOneDay()) { 438 return selectedHourText; 439 } 440 441 return mContext.getString( 442 R.string.battery_usage_day_and_hour, selectedDayText, selectedHourText); 443 } 444 445 @VisibleForTesting getBatteryLevelPercentageInfo()446 String getBatteryLevelPercentageInfo() { 447 if (mDailyViewModel == null || mHourlyViewModels == null) { 448 // No data 449 return ""; 450 } 451 452 if (mDailyChartIndex == SELECTED_INDEX_ALL || mHourlyChartIndex == SELECTED_INDEX_ALL) { 453 return mDailyViewModel.getSlotBatteryLevelText(mDailyChartIndex); 454 } 455 456 return mHourlyViewModels.get(mDailyChartIndex).getSlotBatteryLevelText(mHourlyChartIndex); 457 } 458 getAccessibilityAnnounceMessage(final boolean isSlotSelected)459 private String getAccessibilityAnnounceMessage(final boolean isSlotSelected) { 460 final String selectedInformation = 461 mPrefContext.getString( 462 isSlotSelected 463 ? R.string.battery_chart_slot_status_selected 464 : R.string.battery_chart_slot_status_unselected); 465 final String slotInformation = getSlotInformation(/* isAccessibilityText= */ true); 466 final String slotInformationMessage = 467 slotInformation == null 468 ? mPrefContext.getString( 469 R.string.battery_usage_breakdown_title_since_last_full_charge) 470 : mPrefContext.getString( 471 R.string.battery_usage_breakdown_title_for_slot, slotInformation); 472 final String batteryLevelPercentageMessage = getBatteryLevelPercentageInfo(); 473 474 return mPrefContext.getString( 475 R.string.battery_usage_status_time_info_and_battery_level, 476 selectedInformation, 477 slotInformationMessage, 478 batteryLevelPercentageMessage); 479 } 480 animateBatteryChartViewGroup()481 private void animateBatteryChartViewGroup() { 482 if (mBatteryChartViewGroup != null && mBatteryChartViewGroup.getAlpha() == 0) { 483 mBatteryChartViewGroup 484 .animate() 485 .alpha(1f) 486 .setDuration(FADE_IN_ANIMATION_DURATION) 487 .start(); 488 } 489 } 490 animateBatteryHourlyChartView(final boolean visible)491 private void animateBatteryHourlyChartView(final boolean visible) { 492 if (mHourlyChartView == null 493 || (mHourlyChartView.getVisibility() == View.VISIBLE) == visible) { 494 return; 495 } 496 497 if (visible) { 498 mHourlyChartView.setVisibility(View.VISIBLE); 499 mHourlyChartView 500 .animate() 501 .alpha(1f) 502 .setDuration(FADE_IN_ANIMATION_DURATION) 503 .setListener(mHourlyChartFadeInAdapter) 504 .start(); 505 } else { 506 mHourlyChartView 507 .animate() 508 .alpha(0f) 509 .setDuration(FADE_OUT_ANIMATION_DURATION) 510 .setListener(mHourlyChartFadeOutAdapter) 511 .start(); 512 } 513 } 514 setChartSummaryVisible(final boolean visible)515 private void setChartSummaryVisible(final boolean visible) { 516 if (mChartSummaryTextView != null) { 517 mChartSummaryTextView.setVisibility(visible ? View.VISIBLE : View.GONE); 518 } 519 } 520 createHourlyChartAnimatorListenerAdapter( final boolean visible)521 private AnimatorListenerAdapter createHourlyChartAnimatorListenerAdapter( 522 final boolean visible) { 523 final int visibility = visible ? View.VISIBLE : View.GONE; 524 525 return new AnimatorListenerAdapter() { 526 @Override 527 public void onAnimationEnd(Animator animation) { 528 super.onAnimationEnd(animation); 529 if (mHourlyChartView != null) { 530 mHourlyChartView.setVisibility(visibility); 531 } 532 } 533 534 @Override 535 public void onAnimationCancel(Animator animation) { 536 super.onAnimationCancel(animation); 537 if (mHourlyChartView != null) { 538 mHourlyChartView.setVisibility(visibility); 539 } 540 } 541 }; 542 } 543 544 private boolean isBatteryLevelDataInOneDay() { 545 return mHourlyViewModels != null && mHourlyViewModels.size() == 1; 546 } 547 548 private boolean isAllSelected() { 549 return (isBatteryLevelDataInOneDay() || mDailyChartIndex == SELECTED_INDEX_ALL) 550 && mHourlyChartIndex == SELECTED_INDEX_ALL; 551 } 552 553 @VisibleForTesting 554 static int getTotalHours(final BatteryLevelData batteryLevelData) { 555 if (batteryLevelData == null) { 556 return 0; 557 } 558 List<Long> dailyTimestamps = batteryLevelData.getDailyBatteryLevels().getTimestamps(); 559 return (int) 560 ((dailyTimestamps.get(dailyTimestamps.size() - 1) - dailyTimestamps.get(0)) 561 / DateUtils.HOUR_IN_MILLIS); 562 } 563 564 /** Used for {@link AppBatteryPreferenceController}. */ 565 public static List<BatteryDiffEntry> getAppBatteryUsageData(Context context) { 566 final long start = System.currentTimeMillis(); 567 final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap = 568 DatabaseUtils.getHistoryMapSinceLastFullCharge(context, Calendar.getInstance()); 569 if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) { 570 return null; 571 } 572 Log.d( 573 TAG, 574 String.format( 575 "getBatterySinceLastFullChargeUsageData() size=%d time=%d/ms", 576 batteryHistoryMap.size(), (System.currentTimeMillis() - start))); 577 final Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageData = 578 DataProcessor.getBatteryUsageData( 579 context, 580 new UserIdsSeries(context, /* isNonUIRequest= */ false), 581 batteryHistoryMap); 582 if (batteryUsageData == null) { 583 return null; 584 } 585 BatteryDiffData allBatteryDiffData = 586 batteryUsageData.get(SELECTED_INDEX_ALL).get(SELECTED_INDEX_ALL); 587 return allBatteryDiffData == null ? null : allBatteryDiffData.getAppDiffEntryList(); 588 } 589 590 private static <T> T getLast(List<T> list) { 591 if (list == null || list.isEmpty()) { 592 return null; 593 } 594 return list.get(list.size() - 1); 595 } 596 597 /** Used for {@link AppBatteryPreferenceController}. */ 598 public static BatteryDiffEntry getAppBatteryUsageData( 599 Context context, String packageName, int userId) { 600 if (packageName == null) { 601 return null; 602 } 603 final List<BatteryDiffEntry> entries = getAppBatteryUsageData(context); 604 if (entries == null) { 605 return null; 606 } 607 for (BatteryDiffEntry entry : entries) { 608 if (!entry.isSystemEntry() 609 && entry.mUserId == userId 610 && packageName.equals(entry.getPackageName())) { 611 return entry; 612 } 613 } 614 return null; 615 } 616 617 private abstract class BaseLabelTextGenerator 618 implements BatteryChartViewModel.LabelTextGenerator { 619 @Override 620 public String generateContentDescription(List<Long> timestamps, int index) { 621 return generateFullText(timestamps, index); 622 } 623 624 @Override 625 public String generateSlotBatteryLevelText(List<Integer> levels, int index) { 626 final int fromBatteryLevelIndex = index == SELECTED_INDEX_ALL ? 0 : index; 627 final int toBatteryLevelIndex = 628 index == SELECTED_INDEX_ALL ? levels.size() - 1 : index + 1; 629 return mPrefContext.getString( 630 R.string.battery_level_percentage, 631 generateBatteryLevelText(levels.get(fromBatteryLevelIndex)), 632 generateBatteryLevelText(levels.get(toBatteryLevelIndex))); 633 } 634 635 @VisibleForTesting 636 private static String generateBatteryLevelText(Integer level) { 637 return Utils.formatPercentage(level); 638 } 639 } 640 641 private final class DailyChartLabelTextGenerator extends BaseLabelTextGenerator 642 implements BatteryChartViewModel.LabelTextGenerator { 643 @Override 644 public String generateText(List<Long> timestamps, int index) { 645 return ConvertUtils.utcToLocalTimeDayOfWeek( 646 mContext, timestamps.get(index), /* isAbbreviation= */ true); 647 } 648 649 @Override 650 public String generateFullText(List<Long> timestamps, int index) { 651 return ConvertUtils.utcToLocalTimeDayOfWeek( 652 mContext, timestamps.get(index), /* isAbbreviation= */ false); 653 } 654 } 655 656 private final class HourlyChartLabelTextGenerator extends BaseLabelTextGenerator 657 implements BatteryChartViewModel.LabelTextGenerator { 658 private boolean mIsStartTimestamp; 659 private long mFistTimestamp; 660 private long mLatestTimestamp; 661 662 @Override 663 public String generateText(List<Long> timestamps, int index) { 664 if (Objects.equal(timestamps.get(index), mLatestTimestamp)) { 665 // Replaces the latest timestamp text to "now". 666 return mContext.getString(R.string.battery_usage_chart_label_now); 667 } 668 long timestamp = timestamps.get(index); 669 boolean showMinute = false; 670 if (Objects.equal(timestamp, mFistTimestamp)) { 671 if (mIsStartTimestamp) { 672 showMinute = true; 673 } else { 674 // starts from 7 days ago 675 timestamp = TimestampUtils.getLastEvenHourTimestamp(timestamp); 676 } 677 } 678 return ConvertUtils.utcToLocalTimeHour( 679 mContext, timestamp, mIs24HourFormat, showMinute); 680 } 681 682 @Override 683 public String generateFullText(List<Long> timestamps, int index) { 684 return index == timestamps.size() - 1 685 ? generateText(timestamps, index) 686 : mContext.getString( 687 R.string.battery_usage_timestamps_hyphen, 688 generateText(timestamps, index), 689 generateText(timestamps, index + 1)); 690 } 691 692 @Override 693 public String generateContentDescription(List<Long> timestamps, int index) { 694 return index == timestamps.size() - 1 695 ? generateText(timestamps, index) 696 : mContext.getString( 697 R.string.battery_usage_timestamps_content_description, 698 generateText(timestamps, index), 699 generateText(timestamps, index + 1)); 700 } 701 702 HourlyChartLabelTextGenerator updateSpecialCaseContext( 703 @NonNull final BatteryLevelData batteryLevelData) { 704 BatteryLevelData.PeriodBatteryLevelData firstDayLevelData = 705 batteryLevelData.getHourlyBatteryLevelsPerDay().get(0); 706 this.mIsStartTimestamp = firstDayLevelData.isStartTimestamp(); 707 this.mFistTimestamp = firstDayLevelData.getTimestamps().get(0); 708 this.mLatestTimestamp = 709 getLast( 710 getLast(batteryLevelData.getHourlyBatteryLevelsPerDay()) 711 .getTimestamps()); 712 return this; 713 } 714 } 715 } 716