• 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 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