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