diff --git a/src/com/android/settings/fuelgauge/BatteryInfo.java b/src/com/android/settings/fuelgauge/BatteryInfo.java index acd81446b57..c4c795b29b1 100644 --- a/src/com/android/settings/fuelgauge/BatteryInfo.java +++ b/src/com/android/settings/fuelgauge/BatteryInfo.java @@ -272,7 +272,7 @@ public class BatteryInfo { void onParsingDone(); } - private static void parse(BatteryStats stats, BatteryDataParser... parsers) { + public static void parse(BatteryStats stats, BatteryDataParser... parsers) { long startWalltime = 0; long endWalltime = 0; long historyStart = 0; diff --git a/src/com/android/settings/fuelgauge/batterytip/BatteryTipPolicy.java b/src/com/android/settings/fuelgauge/batterytip/BatteryTipPolicy.java index 6af859b4569..a580db1d311 100644 --- a/src/com/android/settings/fuelgauge/batterytip/BatteryTipPolicy.java +++ b/src/com/android/settings/fuelgauge/batterytip/BatteryTipPolicy.java @@ -19,9 +19,12 @@ package com.android.settings.fuelgauge.batterytip; import android.content.Context; import android.provider.Settings; import android.support.annotation.VisibleForTesting; +import android.text.format.DateUtils; import android.util.KeyValueListParser; import android.util.Log; +import java.time.Duration; + /** * Class to store the policy for battery tips, which comes from * {@link Settings.Global} @@ -34,6 +37,8 @@ public class BatteryTipPolicy { private static final String KEY_BATTERY_SAVER_TIP_ENABLED = "battery_saver_tip_enabled"; private static final String KEY_HIGH_USAGE_ENABLED = "high_usage_enabled"; private static final String KEY_HIGH_USAGE_APP_COUNT = "high_usage_app_count"; + private static final String KEY_HIGH_USAGE_PERIOD_MS = "high_usage_period_ms"; + private static final String KEY_HIGH_USAGE_BATTERY_DRAINING = "high_usage_battery_draining"; private static final String KEY_APP_RESTRICTION_ENABLED = "app_restriction_enabled"; private static final String KEY_REDUCED_BATTERY_ENABLED = "reduced_battery_enabled"; private static final String KEY_REDUCED_BATTERY_PERCENT = "reduced_battery_percent"; @@ -80,6 +85,24 @@ public class BatteryTipPolicy { */ public final int highUsageAppCount; + /** + * The size of the window(milliseconds) for checking if the device is being heavily used + * + * @see Settings.Global#BATTERY_TIP_CONSTANTS + * @see #KEY_HIGH_USAGE_PERIOD_MS + */ + public final long highUsagePeriodMs; + + /** + * The battery draining threshold to detect whether device is heavily used. + * If battery drains more than {@link #highUsageBatteryDraining} in last {@link + * #highUsagePeriodMs}, treat device as heavily used. + * + * @see Settings.Global#BATTERY_TIP_CONSTANTS + * @see #KEY_HIGH_USAGE_BATTERY_DRAINING + */ + public final int highUsageBatteryDraining; + /** * {@code true} if app restriction tip is enabled * @@ -143,6 +166,9 @@ public class BatteryTipPolicy { batterySaverTipEnabled = mParser.getBoolean(KEY_BATTERY_SAVER_TIP_ENABLED, true); highUsageEnabled = mParser.getBoolean(KEY_HIGH_USAGE_ENABLED, true); highUsageAppCount = mParser.getInt(KEY_HIGH_USAGE_APP_COUNT, 3); + highUsagePeriodMs = mParser.getLong(KEY_HIGH_USAGE_PERIOD_MS, + Duration.ofHours(2).toMillis()); + highUsageBatteryDraining = mParser.getInt(KEY_HIGH_USAGE_BATTERY_DRAINING, 25); appRestrictionEnabled = mParser.getBoolean(KEY_APP_RESTRICTION_ENABLED, true); reducedBatteryEnabled = mParser.getBoolean(KEY_REDUCED_BATTERY_ENABLED, false); reducedBatteryPercent = mParser.getInt(KEY_REDUCED_BATTERY_PERCENT, 50); diff --git a/src/com/android/settings/fuelgauge/batterytip/HighUsageDataParser.java b/src/com/android/settings/fuelgauge/batterytip/HighUsageDataParser.java new file mode 100644 index 00000000000..cc5aed679dd --- /dev/null +++ b/src/com/android/settings/fuelgauge/batterytip/HighUsageDataParser.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2018 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.batterytip; + +import android.os.BatteryStats; + +import com.android.settings.fuelgauge.BatteryInfo; + +/** + * DataParser used to go through battery data and detect whether battery is + * heavily used. + */ +public class HighUsageDataParser implements BatteryInfo.BatteryDataParser { + /** + * time period to check the battery usage + */ + private final long mTimePeriodMs; + /** + * treat device as heavily used if battery usage is more than {@code threshold}. 1 means 1% + * battery usage. + */ + private int mThreshold; + private long mEndTimeMs; + private byte mEndBatteryLevel; + private byte mLastPeriodBatteryLevel; + private int mBatteryDrain; + + public HighUsageDataParser(long timePeriodMs, int threshold) { + mTimePeriodMs = timePeriodMs; + mThreshold = threshold; + } + + @Override + public void onParsingStarted(long startTime, long endTime) { + mEndTimeMs = endTime; + } + + @Override + public void onDataPoint(long time, BatteryStats.HistoryItem record) { + if (record.currentTime <= mEndTimeMs - mTimePeriodMs) { + // Since onDataPoint is invoked sorted by time, so we could use this way to get the + // closet battery level 'mTimePeriodMs' time ago. + mLastPeriodBatteryLevel = record.batteryLevel; + } + mEndBatteryLevel = record.batteryLevel; + } + + @Override + public void onDataGap() { + // do nothing + } + + @Override + public void onParsingDone() { + mBatteryDrain = mLastPeriodBatteryLevel - mEndBatteryLevel; + } + + /** + * Return {@code true} if the battery drain in {@link #mTimePeriodMs} is too much + */ + public boolean isDeviceHeavilyUsed() { + return mBatteryDrain > mThreshold; + } +} + diff --git a/src/com/android/settings/fuelgauge/batterytip/detectors/HighUsageDetector.java b/src/com/android/settings/fuelgauge/batterytip/detectors/HighUsageDetector.java index 3c696671e95..ed3fa04c1e0 100644 --- a/src/com/android/settings/fuelgauge/batterytip/detectors/HighUsageDetector.java +++ b/src/com/android/settings/fuelgauge/batterytip/detectors/HighUsageDetector.java @@ -19,13 +19,14 @@ package com.android.settings.fuelgauge.batterytip.detectors; import android.content.Context; import android.os.BatteryStats; import android.support.annotation.VisibleForTesting; -import android.text.format.DateUtils; import com.android.internal.os.BatterySipper; import com.android.internal.os.BatteryStatsHelper; import com.android.settings.fuelgauge.BatteryUtils; import com.android.settings.fuelgauge.batterytip.BatteryTipPolicy; import com.android.settings.fuelgauge.batterytip.AppInfo; +import com.android.settings.fuelgauge.BatteryInfo; +import com.android.settings.fuelgauge.batterytip.HighUsageDataParser; import com.android.settings.fuelgauge.batterytip.tips.BatteryTip; import com.android.settings.fuelgauge.batterytip.tips.HighUsageTip; @@ -43,6 +44,8 @@ public class HighUsageDetector implements BatteryTipDetector { private List mHighUsageAppList; private Context mContext; @VisibleForTesting + HighUsageDataParser mDataParser; + @VisibleForTesting BatteryUtils mBatteryUtils; public HighUsageDetector(Context context, BatteryTipPolicy policy, @@ -52,32 +55,42 @@ public class HighUsageDetector implements BatteryTipDetector { mBatteryStatsHelper = batteryStatsHelper; mHighUsageAppList = new ArrayList<>(); mBatteryUtils = BatteryUtils.getInstance(context); + mDataParser = new HighUsageDataParser(mPolicy.highUsagePeriodMs, + mPolicy.highUsageBatteryDraining); } @Override public BatteryTip detect() { final long screenUsageTimeMs = mBatteryUtils.calculateScreenUsageTime(mBatteryStatsHelper); - //TODO(b/70570352): Change it to detect whether battery drops 25% in last 2 hours - if (mPolicy.highUsageEnabled && screenUsageTimeMs > DateUtils.HOUR_IN_MILLIS) { - final List batterySippers = mBatteryStatsHelper.getUsageList(); - for (int i = 0, size = batterySippers.size(); i < size; i++) { - final BatterySipper batterySipper = batterySippers.get(i); - if (!mBatteryUtils.shouldHideSipper(batterySipper)) { - final long foregroundTimeMs = mBatteryUtils.getProcessTimeMs( - BatteryUtils.StatusType.FOREGROUND, batterySipper.uidObj, - BatteryStats.STATS_SINCE_CHARGED); - mHighUsageAppList.add(new AppInfo.Builder() - .setPackageName(mBatteryUtils.getPackageName(batterySipper.getUid())) - .setScreenOnTimeMs(foregroundTimeMs) - .build()); + if (mPolicy.highUsageEnabled) { + parseBatteryData(); + if (mDataParser.isDeviceHeavilyUsed()) { + final List batterySippers = mBatteryStatsHelper.getUsageList(); + for (int i = 0, size = batterySippers.size(); i < size; i++) { + final BatterySipper batterySipper = batterySippers.get(i); + if (!mBatteryUtils.shouldHideSipper(batterySipper)) { + final long foregroundTimeMs = mBatteryUtils.getProcessTimeMs( + BatteryUtils.StatusType.FOREGROUND, batterySipper.uidObj, + BatteryStats.STATS_SINCE_CHARGED); + mHighUsageAppList.add(new AppInfo.Builder() + .setPackageName( + mBatteryUtils.getPackageName(batterySipper.getUid())) + .setScreenOnTimeMs(foregroundTimeMs) + .build()); + } } - } - mHighUsageAppList = mHighUsageAppList.subList(0, - Math.min(mPolicy.highUsageAppCount, mHighUsageAppList.size())); - Collections.sort(mHighUsageAppList, Collections.reverseOrder()); + mHighUsageAppList = mHighUsageAppList.subList(0, + Math.min(mPolicy.highUsageAppCount, mHighUsageAppList.size())); + Collections.sort(mHighUsageAppList, Collections.reverseOrder()); + } } return new HighUsageTip(screenUsageTimeMs, mHighUsageAppList); } + + @VisibleForTesting + void parseBatteryData() { + BatteryInfo.parse(mBatteryStatsHelper.getStats(), mDataParser); + } } diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoaderTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoaderTest.java index 53c9766ad55..83b32258009 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoaderTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoaderTest.java @@ -37,6 +37,7 @@ import com.android.settings.testutils.SettingsRobolectricTestRunner; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Answers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RuntimeEnvironment; @@ -54,7 +55,7 @@ public class BatteryTipLoaderTest { BatteryTip.TipType.BATTERY_SAVER, BatteryTip.TipType.LOW_BATTERY, BatteryTip.TipType.SUMMARY}; - @Mock + @Mock(answer = Answers.RETURNS_DEEP_STUBS) private BatteryStatsHelper mBatteryStatsHelper; @Mock private PowerManager mPowerManager; diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/BatteryTipPolicyTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/BatteryTipPolicyTest.java index bb9a37bd50c..78c86f8d3e6 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/BatteryTipPolicyTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/BatteryTipPolicyTest.java @@ -24,6 +24,7 @@ import static org.mockito.Mockito.spy; import android.content.Context; import android.provider.Settings; +import android.text.format.DateUtils; import com.android.settings.TestConfig; import com.android.settings.testutils.SettingsRobolectricTestRunner; @@ -42,6 +43,8 @@ public class BatteryTipPolicyTest { + ",battery_saver_tip_enabled=false" + ",high_usage_enabled=true" + ",high_usage_app_count=5" + + ",high_usage_period_ms=2000" + + ",high_usage_battery_draining=30" + ",app_restriction_enabled=true" + ",reduced_battery_enabled=true" + ",reduced_battery_percent=30" @@ -66,6 +69,8 @@ public class BatteryTipPolicyTest { assertThat(batteryTipPolicy.batterySaverTipEnabled).isFalse(); assertThat(batteryTipPolicy.highUsageEnabled).isTrue(); assertThat(batteryTipPolicy.highUsageAppCount).isEqualTo(5); + assertThat(batteryTipPolicy.highUsagePeriodMs).isEqualTo(2000); + assertThat(batteryTipPolicy.highUsageBatteryDraining).isEqualTo(30); assertThat(batteryTipPolicy.appRestrictionEnabled).isTrue(); assertThat(batteryTipPolicy.reducedBatteryEnabled).isTrue(); assertThat(batteryTipPolicy.reducedBatteryPercent).isEqualTo(30); @@ -85,6 +90,8 @@ public class BatteryTipPolicyTest { assertThat(batteryTipPolicy.batterySaverTipEnabled).isTrue(); assertThat(batteryTipPolicy.highUsageEnabled).isTrue(); assertThat(batteryTipPolicy.highUsageAppCount).isEqualTo(3); + assertThat(batteryTipPolicy.highUsagePeriodMs).isEqualTo(2 * DateUtils.HOUR_IN_MILLIS); + assertThat(batteryTipPolicy.highUsageBatteryDraining).isEqualTo(25); assertThat(batteryTipPolicy.appRestrictionEnabled).isTrue(); assertThat(batteryTipPolicy.reducedBatteryEnabled).isFalse(); assertThat(batteryTipPolicy.reducedBatteryPercent).isEqualTo(50); diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/HighUsageDataParserTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/HighUsageDataParserTest.java new file mode 100644 index 00000000000..5bdae0ccc2d --- /dev/null +++ b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/HighUsageDataParserTest.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2018 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.batterytip; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.BatteryStats; +import android.text.format.DateUtils; + +import com.android.settings.TestConfig; +import com.android.settings.testutils.SettingsRobolectricTestRunner; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.robolectric.annotation.Config; + +import java.time.Duration; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class HighUsageDataParserTest { + private static final long PERIOD_ONE_MINUTE_MS = Duration.ofMinutes(1).toMillis(); + private static final long END_TIME_MS = 2 * PERIOD_ONE_MINUTE_MS; + private static final int THRESHOLD_LOW = 10; + private static final int THRESHOLD_HIGH = 20; + private HighUsageDataParser mDataParser; + private BatteryStats.HistoryItem mFirstItem; + private BatteryStats.HistoryItem mSecondItem; + private BatteryStats.HistoryItem mThirdItem; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mFirstItem = new BatteryStats.HistoryItem(); + mFirstItem.batteryLevel = 100; + mFirstItem.currentTime = 0; + mSecondItem = new BatteryStats.HistoryItem(); + mSecondItem.batteryLevel = 95; + mSecondItem.currentTime = PERIOD_ONE_MINUTE_MS; + mThirdItem = new BatteryStats.HistoryItem(); + mThirdItem.batteryLevel = 80; + mThirdItem.currentTime = END_TIME_MS; + } + + @Test + public void testDataParser_thresholdLow_isHeavilyUsed() { + mDataParser = new HighUsageDataParser(PERIOD_ONE_MINUTE_MS, THRESHOLD_LOW); + parseData(); + + assertThat(mDataParser.isDeviceHeavilyUsed()).isTrue(); + } + + @Test + public void testDataParser_thresholdHigh_notHeavilyUsed() { + mDataParser = new HighUsageDataParser(PERIOD_ONE_MINUTE_MS, THRESHOLD_HIGH); + parseData(); + + assertThat(mDataParser.isDeviceHeavilyUsed()).isFalse(); + } + + private void parseData() { + mDataParser.onParsingStarted(0, END_TIME_MS); + mDataParser.onDataPoint(0, mFirstItem); + mDataParser.onDataPoint(PERIOD_ONE_MINUTE_MS, mSecondItem); + mDataParser.onDataPoint(END_TIME_MS, mThirdItem); + + mDataParser.onParsingDone(); + } +} diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/detectors/HighUsageDetectorTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/detectors/HighUsageDetectorTest.java index 2a719916fb0..8df7c56d9ef 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/detectors/HighUsageDetectorTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/detectors/HighUsageDetectorTest.java @@ -18,6 +18,8 @@ package com.android.settings.fuelgauge.batterytip.detectors; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; @@ -31,6 +33,7 @@ import com.android.settings.TestConfig; import com.android.settings.fuelgauge.BatteryInfo; import com.android.settings.fuelgauge.BatteryUtils; import com.android.settings.fuelgauge.batterytip.BatteryTipPolicy; +import com.android.settings.fuelgauge.batterytip.HighUsageDataParser; import com.android.settings.testutils.SettingsRobolectricTestRunner; import org.junit.Before; @@ -55,6 +58,8 @@ public class HighUsageDetectorTest { private BatteryUtils mBatteryUtils; @Mock private BatterySipper mBatterySipper; + @Mock + private HighUsageDataParser mDataParser; private BatteryTipPolicy mPolicy; private HighUsageDetector mHighUsageDetector; @@ -66,8 +71,10 @@ public class HighUsageDetectorTest { mContext = RuntimeEnvironment.application; mPolicy = spy(new BatteryTipPolicy(mContext)); - mHighUsageDetector = new HighUsageDetector(mContext, mPolicy, mBatteryStatsHelper); + mHighUsageDetector = spy(new HighUsageDetector(mContext, mPolicy, mBatteryStatsHelper)); mHighUsageDetector.mBatteryUtils = mBatteryUtils; + mHighUsageDetector.mDataParser = mDataParser; + doNothing().when(mHighUsageDetector).parseBatteryData(); mUsageList = new ArrayList<>(); mUsageList.add(mBatterySipper); @@ -82,8 +89,7 @@ public class HighUsageDetectorTest { @Test public void testDetect_containsHighUsageApp_tipVisible() { - doReturn(2 * DateUtils.HOUR_IN_MILLIS).when(mBatteryUtils).calculateScreenUsageTime( - mBatteryStatsHelper); + doReturn(true).when(mDataParser).isDeviceHeavilyUsed(); doReturn(mUsageList).when(mBatteryStatsHelper).getUsageList(); doReturn(DateUtils.HOUR_IN_MILLIS).when(mBatteryUtils).getProcessTimeMs( BatteryUtils.StatusType.FOREGROUND, mBatterySipper.uidObj,