• 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.settingslib.fuelgauge.BatteryStatus.BATTERY_LEVEL_UNKNOWN;
20 
21 import android.text.format.DateUtils;
22 import android.util.ArrayMap;
23 import android.util.Pair;
24 
25 import androidx.annotation.NonNull;
26 import androidx.annotation.Nullable;
27 import androidx.annotation.VisibleForTesting;
28 import androidx.core.util.Preconditions;
29 
30 import java.util.ArrayList;
31 import java.util.Collections;
32 import java.util.List;
33 import java.util.Locale;
34 import java.util.Map;
35 import java.util.Objects;
36 
37 /** Wraps the battery timestamp and level data used for battery usage chart. */
38 public final class BatteryLevelData {
39     private static final long MIN_SIZE = 2;
40     private static final long TIME_SLOT = DateUtils.HOUR_IN_MILLIS * 2;
41 
42     /** A container for the battery timestamp and level data. */
43     public static final class PeriodBatteryLevelData {
44         // The length of mTimestamps and mLevels must be the same. mLevels[index] might be null when
45         // there is no level data for the corresponding timestamp.
46         private final List<Long> mTimestamps;
47         private final List<Integer> mLevels;
48 
PeriodBatteryLevelData( @onNull Map<Long, Integer> batteryLevelMap, @NonNull List<Long> timestamps)49         public PeriodBatteryLevelData(
50                 @NonNull Map<Long, Integer> batteryLevelMap,
51                 @NonNull List<Long> timestamps) {
52             mTimestamps = timestamps;
53             mLevels = new ArrayList<>(timestamps.size());
54             for (Long timestamp : timestamps) {
55                 mLevels.add(batteryLevelMap.containsKey(timestamp)
56                         ? batteryLevelMap.get(timestamp) : BATTERY_LEVEL_UNKNOWN);
57             }
58         }
59 
getTimestamps()60         public List<Long> getTimestamps() {
61             return mTimestamps;
62         }
63 
getLevels()64         public List<Integer> getLevels() {
65             return mLevels;
66         }
67 
68         @Override
toString()69         public String toString() {
70             return String.format(Locale.ENGLISH, "timestamps: %s; levels: %s",
71                     Objects.toString(mTimestamps), Objects.toString(mLevels));
72         }
73 
getIndexByTimestamps(long startTimestamp, long endTimestamp)74         private int getIndexByTimestamps(long startTimestamp, long endTimestamp) {
75             for (int index = 0; index < mTimestamps.size() - 1; index++) {
76                 if (mTimestamps.get(index) <= startTimestamp
77                         && endTimestamp <= mTimestamps.get(index + 1)) {
78                     return index;
79                 }
80             }
81             return BatteryChartViewModel.SELECTED_INDEX_INVALID;
82         }
83     }
84 
85     /**
86      * There could be 2 cases for the daily battery levels:
87      * 1) length is 2: The usage data is within 1 day. Only contains start and end data, such as
88      *    data of 2022-01-01 06:00 and 2022-01-01 16:00.
89      * 2) length > 2: The usage data is more than 1 days. The data should be the start, end and 0am
90      *    data of every day between the start and end, such as data of 2022-01-01 06:00,
91      *    2022-01-02 00:00, 2022-01-03 00:00 and 2022-01-03 08:00.
92      */
93     private final PeriodBatteryLevelData mDailyBatteryLevels;
94     // The size of hourly data must be the size of daily data - 1.
95     private final List<PeriodBatteryLevelData> mHourlyBatteryLevelsPerDay;
96 
BatteryLevelData(@onNull Map<Long, Integer> batteryLevelMap)97     public BatteryLevelData(@NonNull Map<Long, Integer> batteryLevelMap) {
98         final int mapSize = batteryLevelMap.size();
99         Preconditions.checkArgument(mapSize >= MIN_SIZE, "batteryLevelMap size:" + mapSize);
100 
101         final List<Long> timestampList = new ArrayList<>(batteryLevelMap.keySet());
102         Collections.sort(timestampList);
103         final List<Long> dailyTimestamps = getDailyTimestamps(timestampList);
104         final List<List<Long>> hourlyTimestamps = getHourlyTimestamps(dailyTimestamps);
105 
106         mDailyBatteryLevels = new PeriodBatteryLevelData(batteryLevelMap, dailyTimestamps);
107         mHourlyBatteryLevelsPerDay = new ArrayList<>(hourlyTimestamps.size());
108         for (List<Long> hourlyTimestampsPerDay : hourlyTimestamps) {
109             mHourlyBatteryLevelsPerDay.add(
110                     new PeriodBatteryLevelData(batteryLevelMap, hourlyTimestampsPerDay));
111         }
112     }
113 
114     /** Gets daily and hourly index between start and end timestamps. */
getIndexByTimestamps(long startTimestamp, long endTimestamp)115     public Pair<Integer, Integer> getIndexByTimestamps(long startTimestamp, long endTimestamp) {
116         final int dailyHighlightIndex =
117                 mDailyBatteryLevels.getIndexByTimestamps(startTimestamp, endTimestamp);
118         final int hourlyHighlightIndex =
119                 (dailyHighlightIndex == BatteryChartViewModel.SELECTED_INDEX_INVALID)
120                         ? BatteryChartViewModel.SELECTED_INDEX_INVALID
121                         : mHourlyBatteryLevelsPerDay.get(dailyHighlightIndex)
122                         .getIndexByTimestamps(startTimestamp, endTimestamp);
123         return Pair.create(dailyHighlightIndex, hourlyHighlightIndex);
124     }
125 
getDailyBatteryLevels()126     public PeriodBatteryLevelData getDailyBatteryLevels() {
127         return mDailyBatteryLevels;
128     }
129 
getHourlyBatteryLevelsPerDay()130     public List<PeriodBatteryLevelData> getHourlyBatteryLevelsPerDay() {
131         return mHourlyBatteryLevelsPerDay;
132     }
133 
134     @Override
toString()135     public String toString() {
136         return String.format(Locale.ENGLISH,
137                 "dailyBatteryLevels: %s; hourlyBatteryLevelsPerDay: %s",
138                 Objects.toString(mDailyBatteryLevels),
139                 Objects.toString(mHourlyBatteryLevelsPerDay));
140     }
141 
142     @Nullable
combine(@ullable BatteryLevelData existingBatteryLevelData, List<BatteryEvent> batteryLevelRecordEvents)143     static BatteryLevelData combine(@Nullable BatteryLevelData existingBatteryLevelData,
144             List<BatteryEvent> batteryLevelRecordEvents) {
145         final Map<Long, Integer> batteryLevelMap = new ArrayMap<>(batteryLevelRecordEvents.size());
146         for (BatteryEvent event : batteryLevelRecordEvents) {
147             batteryLevelMap.put(event.getTimestamp(), event.getBatteryLevel());
148         }
149         if (existingBatteryLevelData != null) {
150             List<PeriodBatteryLevelData> multiDaysData =
151                     existingBatteryLevelData.getHourlyBatteryLevelsPerDay();
152             for (int dayIndex = 0; dayIndex < multiDaysData.size(); dayIndex++) {
153                 PeriodBatteryLevelData oneDayData = multiDaysData.get(dayIndex);
154                 for (int hourIndex = 0; hourIndex < oneDayData.getLevels().size(); hourIndex++) {
155                     batteryLevelMap.put(oneDayData.getTimestamps().get(hourIndex),
156                             oneDayData.getLevels().get(hourIndex));
157                 }
158             }
159         }
160         return batteryLevelMap.size() < MIN_SIZE ? null : new BatteryLevelData(batteryLevelMap);
161     }
162 
163     /**
164      * Computes expected daily timestamp slots.
165      *
166      * The valid result should be composed of 3 parts:
167      * 1) start timestamp
168      * 2) every 00:00 timestamp (default timezone) between the start and end
169      * 3) end timestamp
170      * Otherwise, returns an empty list.
171      */
172     @VisibleForTesting
getDailyTimestamps(final List<Long> timestampList)173     static List<Long> getDailyTimestamps(final List<Long> timestampList) {
174         Preconditions.checkArgument(
175                 timestampList.size() >= MIN_SIZE, "timestampList size:" + timestampList.size());
176         final List<Long> dailyTimestampList = new ArrayList<>();
177         final long startTimestamp = timestampList.get(0);
178         final long endTimestamp = timestampList.get(timestampList.size() - 1);
179         for (long timestamp = startTimestamp; timestamp < endTimestamp;
180                 timestamp = TimestampUtils.getNextDayTimestamp(timestamp)) {
181             dailyTimestampList.add(timestamp);
182         }
183         dailyTimestampList.add(endTimestamp);
184         return dailyTimestampList;
185     }
186 
getHourlyTimestamps(final List<Long> dailyTimestamps)187     private static List<List<Long>> getHourlyTimestamps(final List<Long> dailyTimestamps) {
188         final List<List<Long>> hourlyTimestamps = new ArrayList<>();
189         for (int dailyIndex = 0; dailyIndex < dailyTimestamps.size() - 1; dailyIndex++) {
190             final List<Long> hourlyTimestampsPerDay = new ArrayList<>();
191             final long startTime = dailyTimestamps.get(dailyIndex);
192             final long endTime = dailyTimestamps.get(dailyIndex + 1);
193 
194             hourlyTimestampsPerDay.add(startTime);
195             for (long timestamp = TimestampUtils.getNextEvenHourTimestamp(startTime);
196                     timestamp < endTime; timestamp += TIME_SLOT) {
197                 hourlyTimestampsPerDay.add(timestamp);
198             }
199             hourlyTimestampsPerDay.add(endTime);
200 
201             hourlyTimestamps.add(hourlyTimestampsPerDay);
202         }
203         return hourlyTimestamps;
204     }
205 }
206 
207