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

Commit 873a4757 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add DataProcessor to process raw history map for UI."

parents d30f5d1a 287a40a9
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());
    }

}