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