Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 287a40a9 authored by Kuan Wang's avatar Kuan Wang
Browse files

Add DataProcessor to process raw history map for UI.

- Move interpolation from DatabaseUtil
- Add the logic to wrap processed history map to BatteryLevelData
- Change ImmutableList to List in BatteryLevelData because
  ImmutableList.of doesn't support null element.

Bug: 236101687
Test: make RunSettingsRoboTests
Change-Id: I0a842f24e5481faf65f667c08a4fdc11fe2f235c
parent 755d6911
Loading
Loading
Loading
Loading
+34 −26
Original line number Diff line number Diff line
@@ -16,58 +16,66 @@

package com.android.settings.fuelgauge.batteryusage;

import android.util.Log;

import com.google.common.collect.ImmutableList;
import androidx.annotation.NonNull;
import androidx.core.util.Preconditions;

import java.util.List;

/** Wraps the battery timestamp and level data used for battery usage chart. */
public final class BatteryLevelData {

    /** A container for the battery timestamp and level data. */
    public static final class PeriodBatteryLevelData {
        private static final String TAG = "PeriodBatteryLevelData";

        private final ImmutableList<Long> mTimestamps;
        private final ImmutableList<Integer> mLevels;
        // 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<Long> mTimestamps;
        private final List<Integer> mLevels;

        public PeriodBatteryLevelData(List<Long> timestamps, List<Integer> levels) {
            if (timestamps.size() != levels.size()) {
                Log.e(TAG, "Different sizes of timestamps and levels. Timestamp: "
                        + timestamps.size() + ", Level: " + levels.size());
                mTimestamps = ImmutableList.of();
                mLevels = ImmutableList.of();
                return;
            }
            mTimestamps = ImmutableList.copyOf(timestamps);
            mLevels = ImmutableList.copyOf(levels);
        public PeriodBatteryLevelData(
                @NonNull List<Long> timestamps, @NonNull List<Integer> levels) {
            Preconditions.checkArgument(timestamps.size() == levels.size(),
                    /* errorMessage= */ "Timestamp: " + timestamps.size() + ", Level: "
                            + levels.size());
            mTimestamps = timestamps;
            mLevels = levels;
        }

        public ImmutableList<Long> getTimestamps() {
        public List<Long> getTimestamps() {
            return mTimestamps;
        }

        public ImmutableList<Integer> getLevels() {
        public List<Integer> getLevels() {
            return mLevels;
        }
    }

    /**
     * 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;
    private final ImmutableList<PeriodBatteryLevelData> mHourlyBatteryLevelsPerDay;
    // The size of hourly data must be the size of daily data - 1.
    private final List<PeriodBatteryLevelData> mHourlyBatteryLevelsPerDay;

    public BatteryLevelData(
            PeriodBatteryLevelData dailyBatteryLevels,
            List<PeriodBatteryLevelData> hourlyBatteryLevelsPerDay) {
            @NonNull PeriodBatteryLevelData dailyBatteryLevels,
            @NonNull List<PeriodBatteryLevelData> hourlyBatteryLevelsPerDay) {
        final long dailySize = dailyBatteryLevels.getTimestamps().size();
        final long hourlySize = hourlyBatteryLevelsPerDay.size();
        Preconditions.checkArgument(hourlySize == dailySize - 1,
                /* errorMessage= */ "DailySize: " + dailySize + ", HourlySize: " + hourlySize);
        mDailyBatteryLevels = dailyBatteryLevels;
        mHourlyBatteryLevelsPerDay = ImmutableList.copyOf(hourlyBatteryLevelsPerDay);
        mHourlyBatteryLevelsPerDay = hourlyBatteryLevelsPerDay;
    }

    public PeriodBatteryLevelData getDailyBatteryLevels() {
        return mDailyBatteryLevels;
    }

    public ImmutableList<PeriodBatteryLevelData> getHourlyBatteryLevelsPerDay() {
    public List<PeriodBatteryLevelData> getHourlyBatteryLevelsPerDay() {
        return mHourlyBatteryLevelsPerDay;
    }
}
 No newline at end of file
+468 −0

File added.

Preview size limit exceeded, changes collapsed.

+454 −0
Original line number Diff line number Diff line
/*
 * 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.google.common.truth.Truth.assertThat;

import static org.mockito.Mockito.spy;

import android.content.ContentValues;
import android.content.Context;
import android.text.format.DateUtils;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;

@RunWith(RobolectricTestRunner.class)
public class DataProcessorTest {
    private static final String FAKE_ENTRY_KEY = "fake_entry_key";

    private Context mContext;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        TimeZone.setDefault(TimeZone.getTimeZone("GMT+8"));

        mContext = spy(RuntimeEnvironment.application);
    }

    @Test
    public void getBatteryLevelData_emptyHistoryMap_returnNull() {
        assertThat(DataProcessor.getBatteryLevelData(mContext, null)).isNull();
        assertThat(DataProcessor.getBatteryLevelData(mContext, new HashMap<>())).isNull();
    }

    @Test
    public void getBatteryLevelData_notEnoughData_returnNull() {
        // The timestamps are within 1 hour.
        final long[] timestamps = {1000000L, 2000000L, 3000000L};
        final int[] levels = {100, 99, 98};
        final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap =
                createHistoryMap(timestamps, levels);

        assertThat(DataProcessor.getBatteryLevelData(mContext, batteryHistoryMap)).isNull();
    }

    @Test
    public void getBatteryLevelData_returnExpectedResult() {
        // Timezone GMT+8: 2022-01-01 00:00:00, 2022-01-01 01:00:00, 2022-01-01 02:00:00
        final long[] timestamps = {1640966400000L, 1640970000000L, 1640973600000L};
        final int[] levels = {100, 99, 98};
        final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap =
                createHistoryMap(timestamps, levels);

        final BatteryLevelData resultData =
                DataProcessor.getBatteryLevelData(mContext, batteryHistoryMap);

        final List<Long> expectedDailyTimestamps = List.of(timestamps[0], timestamps[2]);
        final List<Integer> expectedDailyLevels = List.of(levels[0], levels[2]);
        final List<List<Long>> expectedHourlyTimestamps = List.of(expectedDailyTimestamps);
        final List<List<Integer>> expectedHourlyLevels = List.of(expectedDailyLevels);
        verifyExpectedBatteryLevelData(
                resultData,
                expectedDailyTimestamps,
                expectedDailyLevels,
                expectedHourlyTimestamps,
                expectedHourlyLevels);
    }

    @Test
    public void getHistoryMapWithExpectedTimestamps_emptyHistoryMap_returnEmptyMap() {
        assertThat(DataProcessor
                .getHistoryMapWithExpectedTimestamps(mContext, new HashMap<>()))
                .isEmpty();
    }

    @Test
    public void getHistoryMapWithExpectedTimestamps_returnExpectedMap() {
        // Timezone GMT+8
        final long[] timestamps = {
                1640966700000L, // 2022-01-01 00:05:00
                1640970180000L, // 2022-01-01 01:03:00
                1640973840000L, // 2022-01-01 02:04:00
                1640978100000L, // 2022-01-01 03:15:00
                1640981400000L  // 2022-01-01 04:10:00
        };
        final int[] levels = {100, 94, 90, 82, 50};
        final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap =
                createHistoryMap(timestamps, levels);

        final Map<Long, Map<String, BatteryHistEntry>> resultMap =
                DataProcessor.getHistoryMapWithExpectedTimestamps(mContext, batteryHistoryMap);

        // Timezone GMT+8
        final long[] expectedTimestamps = {
                1640966400000L, // 2022-01-01 00:00:00
                1640970000000L, // 2022-01-01 01:00:00
                1640973600000L, // 2022-01-01 02:00:00
                1640977200000L, // 2022-01-01 03:00:00
                1640980800000L  // 2022-01-01 04:00:00
        };
        final int[] expectedLevels = {100, 94, 90, 84, 56};
        assertThat(resultMap).hasSize(expectedLevels.length);
        for (int index = 0; index < expectedLevels.length; index++) {
            assertThat(resultMap.get(expectedTimestamps[index]).get(FAKE_ENTRY_KEY).mBatteryLevel)
                    .isEqualTo(expectedLevels[index]);
        }
    }

    @Test
    public void getLevelDataThroughProcessedHistoryMap_notEnoughData_returnNull() {
        final long[] timestamps = {100L};
        final int[] levels = {100};
        final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap =
                createHistoryMap(timestamps, levels);

        assertThat(
                DataProcessor.getLevelDataThroughProcessedHistoryMap(mContext, batteryHistoryMap))
                .isNull();
    }

    @Test
    public void getLevelDataThroughProcessedHistoryMap_OneDayData_returnExpectedResult() {
        // Timezone GMT+8
        final long[] timestamps = {
                1640966400000L, // 2022-01-01 00:00:00
                1640970000000L, // 2022-01-01 01:00:00
                1640973600000L, // 2022-01-01 02:00:00
                1640977200000L, // 2022-01-01 03:00:00
                1640980800000L  // 2022-01-01 04:00:00
        };
        final int[] levels = {100, 94, 90, 82, 50};
        final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap =
                createHistoryMap(timestamps, levels);

        final BatteryLevelData resultData =
                DataProcessor.getLevelDataThroughProcessedHistoryMap(mContext, batteryHistoryMap);

        final List<Long> expectedDailyTimestamps = List.of(timestamps[0], timestamps[4]);
        final List<Integer> expectedDailyLevels = List.of(levels[0], levels[4]);
        final List<List<Long>> expectedHourlyTimestamps = List.of(
                List.of(timestamps[0], timestamps[2], timestamps[4])
        );
        final List<List<Integer>> expectedHourlyLevels = List.of(
                List.of(levels[0], levels[2], levels[4])
        );
        verifyExpectedBatteryLevelData(
                resultData,
                expectedDailyTimestamps,
                expectedDailyLevels,
                expectedHourlyTimestamps,
                expectedHourlyLevels);
    }

    @Test
    public void getLevelDataThroughProcessedHistoryMap_MultipleDaysData_returnExpectedResult() {
        // Timezone GMT+8
        final long[] timestamps = {
                1641038400000L, // 2022-01-01 20:00:00
                1641060000000L, // 2022-01-02 02:00:00
                1641067200000L, // 2022-01-02 04:00:00
                1641081600000L, // 2022-01-02 08:00:00
        };
        final int[] levels = {100, 94, 90, 82};
        final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap =
                createHistoryMap(timestamps, levels);

        final BatteryLevelData resultData =
                DataProcessor.getLevelDataThroughProcessedHistoryMap(mContext, batteryHistoryMap);

        final List<Long> expectedDailyTimestamps = List.of(
                1641038400000L, // 2022-01-01 20:00:00
                1641052800000L, // 2022-01-02 00:00:00
                1641081600000L  // 2022-01-02 08:00:00
        );
        final List<Integer> expectedDailyLevels = new ArrayList<>();
        expectedDailyLevels.add(100);
        expectedDailyLevels.add(null);
        expectedDailyLevels.add(82);
        final List<List<Long>> expectedHourlyTimestamps = List.of(
                List.of(
                        1641038400000L, // 2022-01-01 20:00:00
                        1641045600000L, // 2022-01-01 22:00:00
                        1641052800000L  // 2022-01-02 00:00:00
                ),
                List.of(
                        1641052800000L, // 2022-01-02 00:00:00
                        1641060000000L, // 2022-01-02 02:00:00
                        1641067200000L, // 2022-01-02 04:00:00
                        1641074400000L, // 2022-01-02 06:00:00
                        1641081600000L  // 2022-01-02 08:00:00
                )
        );
        final List<Integer> expectedHourlyLevels1 = new ArrayList<>();
        expectedHourlyLevels1.add(100);
        expectedHourlyLevels1.add(null);
        expectedHourlyLevels1.add(null);
        final List<Integer> expectedHourlyLevels2 = new ArrayList<>();
        expectedHourlyLevels2.add(null);
        expectedHourlyLevels2.add(94);
        expectedHourlyLevels2.add(90);
        expectedHourlyLevels2.add(null);
        expectedHourlyLevels2.add(82);
        final List<List<Integer>> expectedHourlyLevels = List.of(
                expectedHourlyLevels1,
                expectedHourlyLevels2
        );
        verifyExpectedBatteryLevelData(
                resultData,
                expectedDailyTimestamps,
                expectedDailyLevels,
                expectedHourlyTimestamps,
                expectedHourlyLevels);
    }

    @Test
    public void getTimestampSlots_emptyRawList_returnEmptyList() {
        final List<Long> resultList =
                DataProcessor.getTimestampSlots(new ArrayList<>());
        assertThat(resultList).isEmpty();
    }

    @Test
    public void getTimestampSlots_startWithEvenHour_returnExpectedResult() {
        final Calendar startCalendar = Calendar.getInstance();
        startCalendar.set(2022, 6, 5, 6, 30, 50); // 2022-07-05 06:30:50
        final Calendar endCalendar = Calendar.getInstance();
        endCalendar.set(2022, 6, 5, 22, 30, 50); // 2022-07-05 22:30:50

        final Calendar expectedStartCalendar = Calendar.getInstance();
        expectedStartCalendar.set(2022, 6, 5, 6, 0, 0); // 2022-07-05 06:00:00
        final Calendar expectedEndCalendar = Calendar.getInstance();
        expectedEndCalendar.set(2022, 6, 5, 22, 0, 0); // 2022-07-05 22:00:00
        verifyExpectedTimestampSlots(
                startCalendar, endCalendar, expectedStartCalendar, expectedEndCalendar);
    }

    @Test
    public void getTimestampSlots_startWithOddHour_returnExpectedResult() {
        final Calendar startCalendar = Calendar.getInstance();
        startCalendar.set(2022, 6, 5, 5, 0, 50); // 2022-07-05 05:00:50
        final Calendar endCalendar = Calendar.getInstance();
        endCalendar.set(2022, 6, 6, 21, 00, 50); // 2022-07-06 21:00:50

        final Calendar expectedStartCalendar = Calendar.getInstance();
        expectedStartCalendar.set(2022, 6, 5, 6, 00, 00); // 2022-07-05 06:00:00
        final Calendar expectedEndCalendar = Calendar.getInstance();
        expectedEndCalendar.set(2022, 6, 6, 20, 00, 00); // 2022-07-06 20:00:00
        verifyExpectedTimestampSlots(
                startCalendar, endCalendar, expectedStartCalendar, expectedEndCalendar);
    }

    @Test
    public void getDailyTimestamps_notEnoughData_returnEmptyList() {
        assertThat(DataProcessor.getDailyTimestamps(new ArrayList<>())).isEmpty();
        assertThat(DataProcessor.getDailyTimestamps(List.of(100L))).isEmpty();
    }

    @Test
    public void getDailyTimestamps_OneDayData_returnExpectedList() {
        // Timezone GMT+8
        final List<Long> timestamps = List.of(
                1640966400000L, // 2022-01-01 00:00:00
                1640970000000L, // 2022-01-01 01:00:00
                1640973600000L, // 2022-01-01 02:00:00
                1640977200000L, // 2022-01-01 03:00:00
                1640980800000L  // 2022-01-01 04:00:00
        );

        final List<Long> expectedTimestamps = List.of(
                1640966400000L, // 2022-01-01 00:00:00
                1640980800000L  // 2022-01-01 04:00:00
        );
        assertThat(DataProcessor.getDailyTimestamps(timestamps)).isEqualTo(expectedTimestamps);
    }

    @Test
    public void getDailyTimestamps_MultipleDaysData_returnExpectedList() {
        // Timezone GMT+8
        final List<Long> timestamps = List.of(
                1640988000000L, // 2022-01-01 06:00:00
                1641060000000L, // 2022-01-02 02:00:00
                1641160800000L, // 2022-01-03 06:00:00
                1641254400000L  // 2022-01-04 08:00:00
        );

        final List<Long> expectedTimestamps = List.of(
                1640988000000L, // 2022-01-01 06:00:00
                1641052800000L, // 2022-01-02 00:00:00
                1641139200000L, // 2022-01-03 00:00:00
                1641225600000L, // 2022-01-04 00:00:00
                1641254400000L  // 2022-01-04 08:00:00
        );
        assertThat(DataProcessor.getDailyTimestamps(timestamps)).isEqualTo(expectedTimestamps);
    }

    @Test
    public void isFromFullCharge_emptyData_returnFalse() {
        assertThat(DataProcessor.isFromFullCharge(null)).isFalse();
        assertThat(DataProcessor.isFromFullCharge(new HashMap<>())).isFalse();
    }

    @Test
    public void isFromFullCharge_notChargedData_returnFalse() {
        final Map<String, BatteryHistEntry> entryMap = new HashMap<>();
        final ContentValues values = new ContentValues();
        values.put("batteryLevel", 98);
        final BatteryHistEntry entry = new BatteryHistEntry(values);
        entryMap.put(FAKE_ENTRY_KEY, entry);

        assertThat(DataProcessor.isFromFullCharge(entryMap)).isFalse();
    }

    @Test
    public void isFromFullCharge_chargedData_returnTrue() {
        final Map<String, BatteryHistEntry> entryMap = new HashMap<>();
        final ContentValues values = new ContentValues();
        values.put("batteryLevel", 100);
        final BatteryHistEntry entry = new BatteryHistEntry(values);
        entryMap.put(FAKE_ENTRY_KEY, entry);

        assertThat(DataProcessor.isFromFullCharge(entryMap)).isTrue();
    }

    @Test
    public void findNearestTimestamp_returnExpectedResult() {
        long[] results = DataProcessor.findNearestTimestamp(
                Arrays.asList(10L, 20L, 30L, 40L), /*target=*/ 15L);
        assertThat(results).isEqualTo(new long[] {10L, 20L});

        results = DataProcessor.findNearestTimestamp(
                Arrays.asList(10L, 20L, 30L, 40L), /*target=*/ 10L);
        assertThat(results).isEqualTo(new long[] {10L, 10L});

        results = DataProcessor.findNearestTimestamp(
                Arrays.asList(10L, 20L, 30L, 40L), /*target=*/ 5L);
        assertThat(results).isEqualTo(new long[] {0L, 10L});

        results = DataProcessor.findNearestTimestamp(
                Arrays.asList(10L, 20L, 30L, 40L), /*target=*/ 50L);
        assertThat(results).isEqualTo(new long[] {40L, 0L});
    }

    @Test
    public void getTimestampOfNextDay_returnExpectedResult() {
        // 2021-02-28 06:00:00 => 2021-03-01 00:00:00
        assertThat(DataProcessor.getTimestampOfNextDay(1614463200000L))
                .isEqualTo(1614528000000L);
        // 2021-12-31 16:00:00 => 2022-01-01 00:00:00
        assertThat(DataProcessor.getTimestampOfNextDay(1640937600000L))
                .isEqualTo(1640966400000L);
    }

    private static Map<Long, Map<String, BatteryHistEntry>> createHistoryMap(
            final long[] timestamps, final int[] levels) {
        final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap = new HashMap<>();
        for (int index = 0; index < timestamps.length; index++) {
            final Map<String, BatteryHistEntry> entryMap = new HashMap<>();
            final ContentValues values = new ContentValues();
            values.put(BatteryHistEntry.KEY_BATTERY_LEVEL, levels[index]);
            final BatteryHistEntry entry = new BatteryHistEntry(values);
            entryMap.put(FAKE_ENTRY_KEY, entry);
            batteryHistoryMap.put(timestamps[index], entryMap);
        }
        return batteryHistoryMap;
    }

    private static void verifyExpectedBatteryLevelData(
            final BatteryLevelData resultData,
            final List<Long> expectedDailyTimestamps,
            final List<Integer> expectedDailyLevels,
            final List<List<Long>> expectedHourlyTimestamps,
            final List<List<Integer>> expectedHourlyLevels) {
        final BatteryLevelData.PeriodBatteryLevelData dailyResultData =
                resultData.getDailyBatteryLevels();
        final List<BatteryLevelData.PeriodBatteryLevelData> hourlyResultData =
                resultData.getHourlyBatteryLevelsPerDay();
        verifyExpectedDailyBatteryLevelData(
                dailyResultData, expectedDailyTimestamps, expectedDailyLevels);
        verifyExpectedHourlyBatteryLevelData(
                hourlyResultData, expectedHourlyTimestamps, expectedHourlyLevels);
    }

    private static void verifyExpectedDailyBatteryLevelData(
            final BatteryLevelData.PeriodBatteryLevelData dailyResultData,
            final List<Long> expectedDailyTimestamps,
            final List<Integer> expectedDailyLevels) {
        assertThat(dailyResultData.getTimestamps()).isEqualTo(expectedDailyTimestamps);
        assertThat(dailyResultData.getLevels()).isEqualTo(expectedDailyLevels);
    }

    private static void verifyExpectedHourlyBatteryLevelData(
            final List<BatteryLevelData.PeriodBatteryLevelData> hourlyResultData,
            final List<List<Long>> expectedHourlyTimestamps,
            final List<List<Integer>> expectedHourlyLevels) {
        final int expectedHourlySize = expectedHourlyTimestamps.size();
        assertThat(hourlyResultData).hasSize(expectedHourlySize);
        for (int dailyIndex = 0; dailyIndex < expectedHourlySize; dailyIndex++) {
            assertThat(hourlyResultData.get(dailyIndex).getTimestamps())
                    .isEqualTo(expectedHourlyTimestamps.get(dailyIndex));
            assertThat(hourlyResultData.get(dailyIndex).getLevels())
                    .isEqualTo(expectedHourlyLevels.get(dailyIndex));
        }
    }

    private static void verifyExpectedTimestampSlots(
            final Calendar start,
            final Calendar end,
            final Calendar expectedStart,
            final Calendar expectedEnd) {
        expectedStart.set(Calendar.MILLISECOND, 0);
        expectedEnd.set(Calendar.MILLISECOND, 0);
        final ArrayList<Long> timestampSlots = new ArrayList<>();
        timestampSlots.add(start.getTimeInMillis());
        timestampSlots.add(end.getTimeInMillis());
        final List<Long> resultList =
                DataProcessor.getTimestampSlots(timestampSlots);

        for (int index = 0; index < resultList.size(); index++) {
            final long expectedTimestamp =
                    expectedStart.getTimeInMillis() + index * DateUtils.HOUR_IN_MILLIS;
            assertThat(resultList.get(index)).isEqualTo(expectedTimestamp);
        }
        assertThat(resultList.get(resultList.size() - 1))
                .isEqualTo(expectedEnd.getTimeInMillis());
    }

}