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