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