/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settings.fuelgauge.batteryusage; import static com.android.settingslib.fuelgauge.BatteryStatus.BATTERY_LEVEL_UNKNOWN; import android.text.format.DateUtils; import android.util.ArrayMap; import android.util.Pair; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.core.util.Preconditions; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; /** Wraps the battery timestamp and level data used for battery usage chart. */ public final class BatteryLevelData { private static final long MIN_SIZE = 2; private static final long TIME_SLOT = DateUtils.HOUR_IN_MILLIS * 2; // For testing only. @VisibleForTesting @Nullable static Calendar sTestCalendar; /** A container for the battery timestamp and level data. */ public static final class PeriodBatteryLevelData { // The length of mTimestamps and mLevels must be the same. mLevels[index] might be null when // there is no level data for the corresponding timestamp. private final List mTimestamps; private final List mLevels; private final boolean mIsStartTimestamp; public PeriodBatteryLevelData( @NonNull Map batteryLevelMap, @NonNull List timestamps, boolean isStartTimestamp) { mTimestamps = timestamps; mLevels = new ArrayList<>(timestamps.size()); mIsStartTimestamp = isStartTimestamp; for (Long timestamp : timestamps) { mLevels.add( batteryLevelMap.containsKey(timestamp) ? batteryLevelMap.get(timestamp) : BATTERY_LEVEL_UNKNOWN); } } public List getTimestamps() { return mTimestamps; } public List getLevels() { return mLevels; } public boolean isStartTimestamp() { return mIsStartTimestamp; } @Override public String toString() { return String.format( Locale.ENGLISH, "timestamps: %s; levels: %s", Objects.toString(mTimestamps), Objects.toString(mLevels)); } private int getIndexByTimestamps(long startTimestamp, long endTimestamp) { for (int index = 0; index < mTimestamps.size() - 1; index++) { if (mTimestamps.get(index) <= startTimestamp && endTimestamp <= mTimestamps.get(index + 1)) { return index; } } return BatteryChartViewModel.SELECTED_INDEX_INVALID; } } /** * There could be 2 cases for the daily battery levels:
* 1) length is 2: The usage data is within 1 day. Only contains start and end data, such as * data of 2022-01-01 06:00 and 2022-01-01 16:00.
* 2) length > 2: The usage data is more than 1 days. The data should be the start, end and 0am * data of every day between the start and end, such as data of 2022-01-01 06:00, 2022-01-02 * 00:00, 2022-01-03 00:00 and 2022-01-03 08:00. */ private final PeriodBatteryLevelData mDailyBatteryLevels; // The size of hourly data must be the size of daily data - 1. private final List mHourlyBatteryLevelsPerDay; public BatteryLevelData(@NonNull Map batteryLevelMap) { final int mapSize = batteryLevelMap.size(); Preconditions.checkArgument(mapSize >= MIN_SIZE, "batteryLevelMap size:" + mapSize); final List timestampList = new ArrayList<>(batteryLevelMap.keySet()); Collections.sort(timestampList); final long minTimestamp = timestampList.get(0); final long sixDaysAgoTimestamp = DatabaseUtils.getTimestampSixDaysAgo(sTestCalendar != null ? sTestCalendar : null); final boolean isStartTimestamp = minTimestamp > sixDaysAgoTimestamp; final List dailyTimestamps = getDailyTimestamps(timestampList); final List> hourlyTimestamps = getHourlyTimestamps(dailyTimestamps); mDailyBatteryLevels = new PeriodBatteryLevelData(batteryLevelMap, dailyTimestamps, isStartTimestamp); mHourlyBatteryLevelsPerDay = new ArrayList<>(hourlyTimestamps.size()); for (int i = 0; i < hourlyTimestamps.size(); i++) { final List hourlyTimestampsPerDay = hourlyTimestamps.get(i); mHourlyBatteryLevelsPerDay.add( new PeriodBatteryLevelData( batteryLevelMap, hourlyTimestampsPerDay, isStartTimestamp && i == 0)); } } /** Gets daily and hourly index between start and end timestamps. */ public Pair getIndexByTimestamps(long startTimestamp, long endTimestamp) { final int dailyHighlightIndex = mDailyBatteryLevels.getIndexByTimestamps(startTimestamp, endTimestamp); final int hourlyHighlightIndex = (dailyHighlightIndex == BatteryChartViewModel.SELECTED_INDEX_INVALID) ? BatteryChartViewModel.SELECTED_INDEX_INVALID : mHourlyBatteryLevelsPerDay .get(dailyHighlightIndex) .getIndexByTimestamps(startTimestamp, endTimestamp); return Pair.create(dailyHighlightIndex, hourlyHighlightIndex); } public PeriodBatteryLevelData getDailyBatteryLevels() { return mDailyBatteryLevels; } public List getHourlyBatteryLevelsPerDay() { return mHourlyBatteryLevelsPerDay; } @Override public String toString() { return String.format( Locale.ENGLISH, "dailyBatteryLevels: %s; hourlyBatteryLevelsPerDay: %s", Objects.toString(mDailyBatteryLevels), Objects.toString(mHourlyBatteryLevelsPerDay)); } @Nullable static BatteryLevelData combine( @Nullable BatteryLevelData existingBatteryLevelData, List batteryLevelRecordEvents) { final Map batteryLevelMap = new ArrayMap<>(batteryLevelRecordEvents.size()); for (BatteryEvent event : batteryLevelRecordEvents) { batteryLevelMap.put(event.getTimestamp(), event.getBatteryLevel()); } if (existingBatteryLevelData != null) { List multiDaysData = existingBatteryLevelData.getHourlyBatteryLevelsPerDay(); for (int dayIndex = 0; dayIndex < multiDaysData.size(); dayIndex++) { PeriodBatteryLevelData oneDayData = multiDaysData.get(dayIndex); for (int hourIndex = 0; hourIndex < oneDayData.getLevels().size(); hourIndex++) { batteryLevelMap.put( oneDayData.getTimestamps().get(hourIndex), oneDayData.getLevels().get(hourIndex)); } } } return batteryLevelMap.size() < MIN_SIZE ? null : new BatteryLevelData(batteryLevelMap); } /** * Computes expected daily timestamp slots. * *

The valid result should be composed of 3 parts:
* 1) start timestamp
* 2) every 00:00 timestamp (default timezone) between the start and end
* 3) end timestamp Otherwise, returns an empty list. */ @VisibleForTesting static List getDailyTimestamps(final List timestampList) { Preconditions.checkArgument( timestampList.size() >= MIN_SIZE, "timestampList size:" + timestampList.size()); final List dailyTimestampList = new ArrayList<>(); final long startTimestamp = timestampList.get(0); final long endTimestamp = timestampList.get(timestampList.size() - 1); for (long timestamp = startTimestamp; timestamp < endTimestamp; timestamp = TimestampUtils.getNextDayTimestamp(timestamp)) { dailyTimestampList.add(timestamp); } dailyTimestampList.add(endTimestamp); return dailyTimestampList; } private static List> getHourlyTimestamps(final List dailyTimestamps) { final List> hourlyTimestamps = new ArrayList<>(); for (int dailyIndex = 0; dailyIndex < dailyTimestamps.size() - 1; dailyIndex++) { final List hourlyTimestampsPerDay = new ArrayList<>(); final long startTime = dailyTimestamps.get(dailyIndex); final long endTime = dailyTimestamps.get(dailyIndex + 1); hourlyTimestampsPerDay.add(startTime); for (long timestamp = TimestampUtils.getNextEvenHourTimestamp(startTime); timestamp < endTime; timestamp += TIME_SLOT) { hourlyTimestampsPerDay.add(timestamp); } hourlyTimestampsPerDay.add(endTime); hourlyTimestamps.add(hourlyTimestampsPerDay); } return hourlyTimestamps; } }