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