• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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