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