diff --git a/src/com/android/settings/fuelgauge/BatteryDiffEntry.java b/src/com/android/settings/fuelgauge/BatteryDiffEntry.java new file mode 100644 index 00000000000..6c885a19e34 --- /dev/null +++ b/src/com/android/settings/fuelgauge/BatteryDiffEntry.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2021 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; + +import java.time.Duration; + +/** A container class to carry battery data in a specific time slot. */ +public final class BatteryDiffEntry { + private static final String TAG = "BatteryDiffEntry"; + + public long mForegroundUsageTimeInMs; + public long mBackgroundUsageTimeInMs; + public double mConsumePower; + // A BatteryHistEntry corresponding to this diff usage data. + public final BatteryHistEntry mBatteryHistEntry; + + private double mTotalConsumePower; + private double mPercentOfTotal; + + public BatteryDiffEntry( + long foregroundUsageTimeInMs, + long backgroundUsageTimeInMs, + double consumePower, + BatteryHistEntry batteryHistEntry) { + mForegroundUsageTimeInMs = foregroundUsageTimeInMs; + mBackgroundUsageTimeInMs = backgroundUsageTimeInMs; + mConsumePower = consumePower; + mBatteryHistEntry = batteryHistEntry; + } + + /** Sets the total consumed power in a specific time slot. */ + public void setTotalConsumePower(double totalConsumePower) { + mTotalConsumePower = totalConsumePower; + mPercentOfTotal = totalConsumePower == 0 + ? 0 : (mConsumePower / mTotalConsumePower) * 100.0; + } + + /** Gets the percentage of total consumed power. */ + public double getPercentOfTotal() { + return mPercentOfTotal; + } + + /** Clones a new instance. */ + public BatteryDiffEntry clone() { + return new BatteryDiffEntry( + this.mForegroundUsageTimeInMs, + this.mBackgroundUsageTimeInMs, + this.mConsumePower, + this.mBatteryHistEntry /*same instance*/); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder() + .append("BatteryDiffEntry{") + .append("\n\tname=" + mBatteryHistEntry.mAppLabel) + .append(String.format("\n\tconsume=%.2f%% %f/%f", + mPercentOfTotal, mConsumePower, mTotalConsumePower)) + .append(String.format("\n\tforeground:%d background:%d", + Duration.ofMillis(mForegroundUsageTimeInMs).getSeconds(), + Duration.ofMillis(mBackgroundUsageTimeInMs).getSeconds())) + .append(String.format("\n\tpackage:%s uid:%s", + mBatteryHistEntry.mPackageName, mBatteryHistEntry.mUid)); + return builder.toString(); + } +} diff --git a/src/com/android/settings/fuelgauge/BatteryHistEntry.java b/src/com/android/settings/fuelgauge/BatteryHistEntry.java index fb46140aba6..e8cbce5bfd5 100644 --- a/src/com/android/settings/fuelgauge/BatteryHistEntry.java +++ b/src/com/android/settings/fuelgauge/BatteryHistEntry.java @@ -112,6 +112,11 @@ public final class BatteryHistEntry { return mIsValidEntry; } + /** Gets an identifier to represent this {@link BatteryHistEntry}. */ + public String getKey() { + return mPackageName + "-" + mUserId; + } + @Override public String toString() { final String recordAtDateTime = ConvertUtils.utcToLocalTime(mTimestamp); @@ -135,7 +140,7 @@ public final class BatteryHistEntry { return values.getAsInteger(key); }; mIsValidEntry = false; - return -1; + return 0; } private int getInteger(Cursor cursor, String key) { @@ -144,7 +149,7 @@ public final class BatteryHistEntry { return cursor.getInt(columnIndex); } mIsValidEntry = false; - return -1; + return 0; } private long getLong(ContentValues values, String key) { @@ -152,7 +157,7 @@ public final class BatteryHistEntry { return values.getAsLong(key); } mIsValidEntry = false; - return -1L; + return 0L; } private long getLong(Cursor cursor, String key) { @@ -161,7 +166,7 @@ public final class BatteryHistEntry { return cursor.getLong(columnIndex); } mIsValidEntry = false; - return -1L; + return 0L; } private double getDouble(ContentValues values, String key) { diff --git a/src/com/android/settings/fuelgauge/ConvertUtils.java b/src/com/android/settings/fuelgauge/ConvertUtils.java index 4ddee3252ef..2e943d12c72 100644 --- a/src/com/android/settings/fuelgauge/ConvertUtils.java +++ b/src/com/android/settings/fuelgauge/ConvertUtils.java @@ -26,13 +26,22 @@ import android.util.Log; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; import java.util.Locale; +import java.util.Map; +import java.util.Set; import java.util.TimeZone; /** A utility class to convert data into another types. */ public final class ConvertUtils { private static final String TAG = "ConvertUtils"; + private static final Map EMPTY_BATTERY_MAP = new HashMap<>(); + private static final BatteryHistEntry EMPTY_BATTERY_HIST_ENTRY = + new BatteryHistEntry(new ContentValues()); /** Invalid system battery consumer drain type. */ public static final int INVALID_DRAIN_TYPE = -1; @@ -126,4 +135,170 @@ public final class ConvertUtils { } return sSimpleDateFormat.format(new Date(timestamp)); } + + /** Gets indexed battery usage data for each corresponding time slot. */ + public static Map> getIndexedUsageMap( + final int timeSlotSize, + final long[] batteryHistoryKeys, + final Map> batteryHistoryMap) { + final Map> resultMap = new HashMap<>(); + // Generates a temporary map to calculate diff usage data, which converts the inputted + // List into Map with the key comes from + // the BatteryHistEntry.getKey() method. + final Map> newBatteryHistoryMap = new HashMap<>(); + for (int index = 0; index < batteryHistoryKeys.length; index++) { + final Long timestamp = Long.valueOf(batteryHistoryKeys[index]); + final List entries = batteryHistoryMap.get(timestamp); + if (entries == null || entries.isEmpty()) { + continue; + } + final Map slotBatteryHistDataMap = new HashMap<>(); + for (BatteryHistEntry entry : entries) { + // Excludes auto-generated fake BatteryHistEntry data, + // which is used to record battery level and status purpose only. + if (!FAKE_PACKAGE_NAME.equals(entry.mPackageName)) { + slotBatteryHistDataMap.put(entry.getKey(), entry); + } + } + newBatteryHistoryMap.put(timestamp, slotBatteryHistDataMap); + } + + // Each time slot usage diff data = + // Math.abs(timestamp[i+2] data - timestamp[i+1] data) + + // Math.abs(timestamp[i+1] data - timestamp[i] data); + // since we want to aggregate every two hours data into a single time slot. + final int timestampStride = 2; + for (int index = 0; index < timeSlotSize; index++) { + final Long currentTimestamp = + Long.valueOf(batteryHistoryKeys[index * timestampStride]); + final Long nextTimestamp = + Long.valueOf(batteryHistoryKeys[index * timestampStride + 1]); + final Long nextTwoTimestamp = + Long.valueOf(batteryHistoryKeys[index * timestampStride + 2]); + + // Fetches BatteryHistEntry data from corresponding time slot. + final Map currentBatteryHistMap = + newBatteryHistoryMap.getOrDefault(currentTimestamp, EMPTY_BATTERY_MAP); + final Map nextBatteryHistMap = + newBatteryHistoryMap.getOrDefault(nextTimestamp, EMPTY_BATTERY_MAP); + final Map nextTwoBatteryHistMap = + newBatteryHistoryMap.getOrDefault(nextTwoTimestamp, EMPTY_BATTERY_MAP); + + // Collects all keys in these three time slot records as population. + final Set allBatteryHistEntryKeys = new HashSet<>(); + allBatteryHistEntryKeys.addAll(currentBatteryHistMap.keySet()); + allBatteryHistEntryKeys.addAll(nextBatteryHistMap.keySet()); + allBatteryHistEntryKeys.addAll(nextTwoBatteryHistMap.keySet()); + + double totalConsumePower = 0.0; + final List batteryDiffEntryList = new ArrayList<>(); + // Adds a specific time slot BatteryDiffEntry list into result map. + resultMap.put(Integer.valueOf(index), batteryDiffEntryList); + + // Calculates all packages diff usage data in a specific time slot. + for (String key : allBatteryHistEntryKeys) { + final BatteryHistEntry currentEntry = + currentBatteryHistMap.getOrDefault(key, EMPTY_BATTERY_HIST_ENTRY); + final BatteryHistEntry nextEntry = + nextBatteryHistMap.getOrDefault(key, EMPTY_BATTERY_HIST_ENTRY); + final BatteryHistEntry nextTwoEntry = + nextTwoBatteryHistMap.getOrDefault(key, EMPTY_BATTERY_HIST_ENTRY); + // Cumulative values is a specific time slot for a specific app. + final long foregroundUsageTimeInMs = + getDiffValue( + currentEntry.mForegroundUsageTimeInMs, + nextEntry.mForegroundUsageTimeInMs, + nextTwoEntry.mForegroundUsageTimeInMs); + final long backgroundUsageTimeInMs = + getDiffValue( + currentEntry.mBackgroundUsageTimeInMs, + nextEntry.mBackgroundUsageTimeInMs, + nextTwoEntry.mBackgroundUsageTimeInMs); + final double consumePower = + getDiffValue( + currentEntry.mConsumePower, + nextEntry.mConsumePower, + nextTwoEntry.mConsumePower); + totalConsumePower += consumePower; + + // Excludes entry since we don't have enough data to calculate. + if (foregroundUsageTimeInMs == 0 + && backgroundUsageTimeInMs == 0 + && consumePower == 0) { + continue; + } + final BatteryHistEntry selectedBatteryEntry = + selectBatteryHistEntry(currentEntry, nextEntry, nextTwoEntry); + if (selectedBatteryEntry == null) { + continue; + } + batteryDiffEntryList.add( + new BatteryDiffEntry( + foregroundUsageTimeInMs, + backgroundUsageTimeInMs, + consumePower, + selectedBatteryEntry)); + } + // Sets total consume power data into all BatteryDiffEntry in the same slot. + for (BatteryDiffEntry diffEntry : batteryDiffEntryList) { + diffEntry.setTotalConsumePower(totalConsumePower); + } + } + insert24HoursData(BatteryChartView.SELECTED_INDEX_ALL, resultMap); + return resultMap; + } + + private static void insert24HoursData( + final int desiredIndex, + final Map> indexedUsageMap) { + final Map resultMap = new HashMap<>(); + double totalConsumePower = 0.0; + // Loops for all BatteryDiffEntry and aggregate them together. + for (List entryList : indexedUsageMap.values()) { + for (BatteryDiffEntry entry : entryList) { + final String key = entry.mBatteryHistEntry.getKey(); + final BatteryDiffEntry oldBatteryDiffEntry = resultMap.get(key); + // Creates new BatteryDiffEntry if we don't have it. + if (oldBatteryDiffEntry == null) { + resultMap.put(key, entry.clone()); + } else { + // Sums up some fields data into the existing one. + oldBatteryDiffEntry.mForegroundUsageTimeInMs += + entry.mForegroundUsageTimeInMs; + oldBatteryDiffEntry.mBackgroundUsageTimeInMs += + entry.mBackgroundUsageTimeInMs; + oldBatteryDiffEntry.mConsumePower += entry.mConsumePower; + } + totalConsumePower += entry.mConsumePower; + } + } + final List resultList = new ArrayList<>(resultMap.values()); + // Sets total 24 hours consume power data into all BatteryDiffEntry. + for (BatteryDiffEntry entry : resultList) { + entry.setTotalConsumePower(totalConsumePower); + } + indexedUsageMap.put(Integer.valueOf(desiredIndex), resultList); + } + + private static long getDiffValue(long v1, long v2, long v3) { + return (v2 > v1 ? v2 - v1 : 0) + (v3 > v2 ? v3 - v2 : 0); + } + + private static double getDiffValue(double v1, double v2, double v3) { + return (v2 > v1 ? v2 - v1 : 0) + (v3 > v2 ? v3 - v2 : 0); + } + + private static BatteryHistEntry selectBatteryHistEntry( + BatteryHistEntry entry1, + BatteryHistEntry entry2, + BatteryHistEntry entry3) { + if (entry1 != null && entry1 != EMPTY_BATTERY_HIST_ENTRY) { + return entry1; + } else if (entry2 != null && entry2 != EMPTY_BATTERY_HIST_ENTRY) { + return entry2; + } else { + return entry3 != null && entry3 != EMPTY_BATTERY_HIST_ENTRY + ? entry3 : null; + } + } } diff --git a/tests/robotests/src/com/android/settings/fuelgauge/BatteryDiffEntryTest.java b/tests/robotests/src/com/android/settings/fuelgauge/BatteryDiffEntryTest.java new file mode 100644 index 00000000000..07e00fc9ed5 --- /dev/null +++ b/tests/robotests/src/com/android/settings/fuelgauge/BatteryDiffEntryTest.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2021 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; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public final class BatteryDiffEntryTest { + + @Test + public void testSetTotalConsumePower_returnExpectedResult() { + final BatteryDiffEntry entry = + new BatteryDiffEntry( + /*foregroundUsageTimeInMs=*/ 10001L, + /*backgroundUsageTimeInMs=*/ 20002L, + /*consumePower=*/ 22.0, + /*batteryHistEntry=*/ null); + entry.setTotalConsumePower(100.0); + + assertThat(entry.getPercentOfTotal()).isEqualTo(22.0); + } + + @Test + public void testSetTotalConsumePower_setZeroValue_returnsZeroValue() { + final BatteryDiffEntry entry = + new BatteryDiffEntry( + /*foregroundUsageTimeInMs=*/ 10001L, + /*backgroundUsageTimeInMs=*/ 20002L, + /*consumePower=*/ 22.0, + /*batteryHistEntry=*/ null); + entry.setTotalConsumePower(0); + + assertThat(entry.getPercentOfTotal()).isEqualTo(0); + } +} diff --git a/tests/robotests/src/com/android/settings/fuelgauge/BatteryHistEntryTest.java b/tests/robotests/src/com/android/settings/fuelgauge/BatteryHistEntryTest.java index b0fb3d14bd9..d97b0d22baf 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/BatteryHistEntryTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/BatteryHistEntryTest.java @@ -161,5 +161,7 @@ public final class BatteryHistEntryTest { .isEqualTo(BatteryManager.BATTERY_STATUS_FULL); assertThat(entry.mBatteryHealth) .isEqualTo(BatteryManager.BATTERY_HEALTH_COLD); + assertThat(entry.getKey()) + .isEqualTo("com.google.android.settings.battery-" + entry.mUserId); } } diff --git a/tests/robotests/src/com/android/settings/fuelgauge/ConvertUtilsTest.java b/tests/robotests/src/com/android/settings/fuelgauge/ConvertUtilsTest.java index 6a5ea617b18..681fce69086 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/ConvertUtilsTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/ConvertUtilsTest.java @@ -37,6 +37,11 @@ import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.TimeZone; @RunWith(RobolectricTestRunner.class) @@ -165,4 +170,84 @@ public final class ConvertUtilsTest { assertThat(ConvertUtils.getConsumerType(mockBatteryConsumer)) .isEqualTo(ConvertUtils.CONSUMER_TYPE_UNKNOWN); } + + @Test + public void testGetIndexedUsageMap_returnsExpectedResult() { + // Creates the fake testing data. + final int timeSlotSize = 2; + final long[] batteryHistoryKeys = new long[] {101L, 102L, 103L, 104L, 105L}; + final Map> batteryHistoryMap = new HashMap<>(); + batteryHistoryMap.put( + Long.valueOf(batteryHistoryKeys[0]), + Arrays.asList( + createBatteryHistEntry( + "package1", "label1", 5.0, 1L, 10L, 20L))); + batteryHistoryMap.put( + Long.valueOf(batteryHistoryKeys[1]), new ArrayList()); + batteryHistoryMap.put( + Long.valueOf(batteryHistoryKeys[2]), + Arrays.asList( + createBatteryHistEntry( + "package2", "label2", 10.0, 2L, 15L, 25L))); + batteryHistoryMap.put( + Long.valueOf(batteryHistoryKeys[3]), + Arrays.asList( + createBatteryHistEntry( + "package2", "label2", 15.0, 2L, 25L, 35L), + createBatteryHistEntry( + "package3", "label3", 5.0, 2L, 5L, 5L))); + batteryHistoryMap.put( + Long.valueOf(batteryHistoryKeys[4]), + Arrays.asList( + createBatteryHistEntry( + "package2", "label2", 30.0, 2L, 30L, 40L), + createBatteryHistEntry( + "package2", "label2", 75.0, 3L, 40L, 50L), + createBatteryHistEntry( + "package3", "label3", 5.0, 2L, 5L, 5L))); + + final Map> resultMap = + ConvertUtils.getIndexedUsageMap( + timeSlotSize, batteryHistoryKeys, batteryHistoryMap); + + assertThat(resultMap).hasSize(3); + // Verifies the first timestamp result. + List entryList = resultMap.get(Integer.valueOf(0)); + assertThat(entryList).hasSize(1); + assertBatteryDiffEntry(entryList.get(0), 100, 15L, 25L); + // Verifies the second timestamp result. + entryList = resultMap.get(Integer.valueOf(1)); + assertThat(entryList).hasSize(3); + assertBatteryDiffEntry(entryList.get(0), 5, 5L, 5L); + assertBatteryDiffEntry(entryList.get(1), 75, 40L, 50L); + assertBatteryDiffEntry(entryList.get(2), 20, 15L, 15L); + // Verifies the last 24 hours aggregate result. + entryList = resultMap.get(Integer.valueOf(-1)); + assertThat(entryList).hasSize(3); + assertBatteryDiffEntry(entryList.get(0), 4, 5L, 5L); + assertBatteryDiffEntry(entryList.get(1), 68, 40L, 50L); + assertBatteryDiffEntry(entryList.get(2), 27, 30L, 40L); + } + + private static BatteryHistEntry createBatteryHistEntry( + String packageName, String appLabel, double consumePower, + long userId, long foregroundUsageTimeInMs, long backgroundUsageTimeInMs) { + // Only insert required fields. + final ContentValues values = new ContentValues(); + values.put("packageName", packageName); + values.put("appLabel", appLabel); + values.put("userId", userId); + values.put("consumePower", consumePower); + values.put("foregroundUsageTimeInMs", foregroundUsageTimeInMs); + values.put("backgroundUsageTimeInMs", backgroundUsageTimeInMs); + return new BatteryHistEntry(values); + } + + private static void assertBatteryDiffEntry( + BatteryDiffEntry entry, int percentOfTotal, + long foregroundUsageTimeInMs, long backgroundUsageTimeInMs) { + assertThat((int) entry.getPercentOfTotal()).isEqualTo(percentOfTotal); + assertThat(entry.mForegroundUsageTimeInMs).isEqualTo(foregroundUsageTimeInMs); + assertThat(entry.mBackgroundUsageTimeInMs).isEqualTo(backgroundUsageTimeInMs); + } }