diff --git a/res/values/strings.xml b/res/values/strings.xml index 79faf9b013e..033211790d9 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -4773,6 +4773,21 @@ Low battery capacity Battery can\'t provide good battery life + + Phone used heavily + + Tablet used heavily + + Device used heavily + + About %1$s used since last full charge + + Your phone was used heavily and this consumed a lot of battery. Your battery is behaving normally.\n\n Your phone was used for about %1$s since last full charge.\n\n Total usage: + + Your tablet was used heavily and this consumed a lot of battery. Your battery is behaving normally.\n\n Your tablet was used for about %1$s since last full charge.\n\n Total usage: + + Your device was used heavily and this consumed a lot of battery. Your battery is behaving normally.\n\n Your device was used for about %1$s since last full charge.\n\n Total usage: + Smart battery manager diff --git a/src/com/android/settings/fuelgauge/BatteryUtils.java b/src/com/android/settings/fuelgauge/BatteryUtils.java index 68677fab2de..0952f1f095d 100644 --- a/src/com/android/settings/fuelgauge/BatteryUtils.java +++ b/src/com/android/settings/fuelgauge/BatteryUtils.java @@ -345,6 +345,17 @@ public class BatteryUtils { } + /** + * Calculate the screen usage time since last full charge. + * @param batteryStatsHelper utility class that contains the screen usage data + * @return time in millis + */ + public long calculateScreenUsageTime(BatteryStatsHelper batteryStatsHelper) { + final BatterySipper sipper = findBatterySipperByType( + batteryStatsHelper.getUsageList(), BatterySipper.DrainType.SCREEN); + return sipper != null ? sipper.usageTimeMs : 0; + } + public static void logRuntime(String tag, String message, long startTime) { Log.d(tag, message + ": " + (System.currentTimeMillis() - startTime) + "ms"); } @@ -432,6 +443,20 @@ public class BatteryUtils { return batteryInfo; } + /** + * Find the {@link BatterySipper} with the corresponding {@link BatterySipper.DrainType} + */ + public BatterySipper findBatterySipperByType(List usageList, + BatterySipper.DrainType type) { + for (int i = 0, size = usageList.size(); i < size; i++) { + final BatterySipper sipper = usageList.get(i); + if (sipper.drainType == type) { + return sipper; + } + } + return null; + } + private boolean isDataCorrupted() { return mPackageManager == null || mAppOpsManager == null; } diff --git a/src/com/android/settings/fuelgauge/PowerUsageSummary.java b/src/com/android/settings/fuelgauge/PowerUsageSummary.java index 0315f032e66..507043f1557 100644 --- a/src/com/android/settings/fuelgauge/PowerUsageSummary.java +++ b/src/com/android/settings/fuelgauge/PowerUsageSummary.java @@ -369,8 +369,9 @@ public class PowerUsageSummary extends PowerUsageBase implements OnLongClickList restartBatteryInfoLoader(); final long lastFullChargeTime = mBatteryUtils.calculateLastFullChargeTime(mStatsHelper, System.currentTimeMillis()); - updateScreenPreference(); updateLastFullChargePreference(lastFullChargeTime); + mScreenUsagePref.setSubtitle(Utils.formatElapsedTime(getContext(), + mBatteryUtils.calculateScreenUsageTime(mStatsHelper), false)); final CharSequence timeSequence = Utils.formatRelativeTime(context, lastFullChargeTime, false); @@ -393,26 +394,6 @@ public class PowerUsageSummary extends PowerUsageBase implements OnLongClickList return new AnomalyDetectionPolicy(getContext()); } - @VisibleForTesting - BatterySipper findBatterySipperByType(List usageList, DrainType type) { - for (int i = 0, size = usageList.size(); i < size; i++) { - final BatterySipper sipper = usageList.get(i); - if (sipper.drainType == type) { - return sipper; - } - } - return null; - } - - @VisibleForTesting - void updateScreenPreference() { - final BatterySipper sipper = findBatterySipperByType( - mStatsHelper.getUsageList(), DrainType.SCREEN); - final long usageTimeMs = sipper != null ? sipper.usageTimeMs : 0; - - mScreenUsagePref.setSubtitle(Utils.formatElapsedTime(getContext(), usageTimeMs, false)); - } - @VisibleForTesting void updateLastFullChargePreference(long timeMs) { final CharSequence timeSequence = Utils.formatRelativeTime(getContext(), timeMs, false); diff --git a/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java b/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java index 9c3f48c0a6b..a1db57a409a 100644 --- a/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java +++ b/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java @@ -23,6 +23,7 @@ import com.android.internal.os.BatteryStatsHelper; import com.android.settings.fuelgauge.BatteryInfo; import com.android.settings.fuelgauge.BatteryUtils; import com.android.settings.fuelgauge.batterytip.detectors.BatteryTipDetector; +import com.android.settings.fuelgauge.batterytip.detectors.HighUsageDetector; import com.android.settings.fuelgauge.batterytip.detectors.LowBatteryDetector; import com.android.settings.fuelgauge.batterytip.detectors.SummaryDetector; import com.android.settings.fuelgauge.batterytip.tips.BatteryTip; @@ -65,6 +66,8 @@ public class BatteryTipLoader extends AsyncLoader> { mVisibleTips = 0; addBatteryTipFromDetector(tips, new LowBatteryDetector(policy, batteryInfo)); + addBatteryTipFromDetector(tips, + new HighUsageDetector(getContext(), policy, mBatteryStatsHelper)); // Add summary detector at last since it need other detectors to update the mVisibleTips addBatteryTipFromDetector(tips, new SummaryDetector(policy, mVisibleTips)); diff --git a/src/com/android/settings/fuelgauge/batterytip/detectors/HighUsageDetector.java b/src/com/android/settings/fuelgauge/batterytip/detectors/HighUsageDetector.java new file mode 100644 index 00000000000..5c2ecad1e01 --- /dev/null +++ b/src/com/android/settings/fuelgauge/batterytip/detectors/HighUsageDetector.java @@ -0,0 +1,84 @@ +/* + * 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.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.Utils; +import com.android.settings.fuelgauge.BatteryUtils; +import com.android.settings.fuelgauge.batterytip.BatteryTipPolicy; +import com.android.settings.fuelgauge.batterytip.tips.BatteryTip; +import com.android.settings.fuelgauge.batterytip.tips.HighUsageTip; +import com.android.settings.fuelgauge.batterytip.tips.SummaryTip; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Detector whether to show summary tip. This detector should be executed as the last + * {@link BatteryTipDetector} since it need the most up-to-date {@code visibleTips} + */ +public class HighUsageDetector implements BatteryTipDetector { + private BatteryTipPolicy mPolicy; + private BatteryStatsHelper mBatteryStatsHelper; + private List mHighUsageAppList; + private Context mContext; + @VisibleForTesting + BatteryUtils mBatteryUtils; + + public HighUsageDetector(Context context, BatteryTipPolicy policy, + BatteryStatsHelper batteryStatsHelper) { + mContext = context; + mPolicy = policy; + mBatteryStatsHelper = batteryStatsHelper; + mHighUsageAppList = new ArrayList<>(); + mBatteryUtils = BatteryUtils.getInstance(context); + } + + @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 HighUsageTip.HighUsageApp( + mBatteryUtils.getPackageName(batterySipper.getUid()), + foregroundTimeMs)); + } + } + + mHighUsageAppList = mHighUsageAppList.subList(0, + Math.min(mPolicy.highUsageAppCount, mHighUsageAppList.size())); + Collections.sort(mHighUsageAppList, Collections.reverseOrder()); + } + + return new HighUsageTip(Utils.formatElapsedTime(mContext, screenUsageTimeMs, false), + mHighUsageAppList); + } +} diff --git a/src/com/android/settings/fuelgauge/batterytip/tips/HighUsageTip.java b/src/com/android/settings/fuelgauge/batterytip/tips/HighUsageTip.java new file mode 100644 index 00000000000..38f2a26c943 --- /dev/null +++ b/src/com/android/settings/fuelgauge/batterytip/tips/HighUsageTip.java @@ -0,0 +1,90 @@ +/* + * 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.tips; + +import android.app.Dialog; +import android.content.Context; + +import com.android.settings.R; + +import java.util.List; + +/** + * Tip to show general summary about battery life + */ +public class HighUsageTip extends BatteryTip { + + private final CharSequence mScreenTimeText; + private final List mHighUsageAppList; + + public HighUsageTip(CharSequence screenTimeText, List appList) { + mShowDialog = true; + mScreenTimeText = screenTimeText; + mType = TipType.HIGH_DEVICE_USAGE; + mHighUsageAppList = appList; + mState = appList.isEmpty() ? StateType.INVISIBLE : StateType.NEW; + } + + @Override + public CharSequence getTitle(Context context) { + return context.getString(R.string.battery_tip_high_usage_title); + } + + @Override + public CharSequence getSummary(Context context) { + return context.getString(R.string.battery_tip_high_usage_summary, mScreenTimeText); + } + + @Override + public int getIconId() { + return R.drawable.ic_perm_device_information_red_24dp; + } + + @Override + public void updateState(BatteryTip tip) { + mState = tip.mState; + } + + @Override + public void action() { + // do nothing + } + + @Override + public Dialog buildDialog() { + //TODO(b/70570352): build the real dialog + return null; + } + + /** + * Class representing app with high screen usage + */ + public static class HighUsageApp implements Comparable { + public final String packageName; + public final long screenOnTimeMs; + + public HighUsageApp(String packageName, long screenOnTimeMs) { + this.packageName = packageName; + this.screenOnTimeMs = screenOnTimeMs; + } + + @Override + public int compareTo(HighUsageApp o) { + return Long.compare(screenOnTimeMs, o.screenOnTimeMs); + } + } +} diff --git a/tests/robotests/src/com/android/settings/fuelgauge/BatteryUtilsTest.java b/tests/robotests/src/com/android/settings/fuelgauge/BatteryUtilsTest.java index 1393d5718b0..844aca4c4a5 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/BatteryUtilsTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/BatteryUtilsTest.java @@ -20,7 +20,9 @@ import static android.os.BatteryStats.Uid.PROCESS_STATE_FOREGROUND; import static android.os.BatteryStats.Uid.PROCESS_STATE_FOREGROUND_SERVICE; import static android.os.BatteryStats.Uid.PROCESS_STATE_TOP; import static android.os.BatteryStats.Uid.PROCESS_STATE_TOP_SLEEPING; + import static com.google.common.truth.Truth.assertThat; + import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyLong; @@ -141,6 +143,7 @@ public class BatteryUtilsTest { private BatteryUtils mBatteryUtils; private FakeFeatureFactory mFeatureFactory; private PowerUsageFeatureProvider mProvider; + private List mUsageList; @Before public void setUp() { @@ -194,6 +197,12 @@ public class BatteryUtilsTest { mBatteryUtils.mPowerUsageFeatureProvider = mProvider; doReturn(0L).when(mBatteryUtils).getForegroundServiceTotalTimeUs( any(BatteryStats.Uid.class), anyLong()); + + mUsageList = new ArrayList<>(); + mUsageList.add(mNormalBatterySipper); + mUsageList.add(mScreenBatterySipper); + mUsageList.add(mCellBatterySipper); + doReturn(mUsageList).when(mBatteryStatsHelper).getUsageList(); } @Test @@ -468,4 +477,28 @@ public class BatteryUtilsTest { verify(mBatteryStatsHelper).refreshStats(BatteryStats.STATS_SINCE_CHARGED, mUserManager.getUserProfiles()); } + + @Test + public void testFindBatterySipperByType_findTypeScreen() { + BatterySipper sipper = mBatteryUtils.findBatterySipperByType(mUsageList, + BatterySipper.DrainType.SCREEN); + + assertThat(sipper).isSameAs(mScreenBatterySipper); + } + + @Test + public void testFindBatterySipperByType_findTypeApp() { + BatterySipper sipper = mBatteryUtils.findBatterySipperByType(mUsageList, + BatterySipper.DrainType.APP); + + assertThat(sipper).isSameAs(mNormalBatterySipper); + } + + @Test + public void testCalculateScreenUsageTime_returnCorrectTime() { + mScreenBatterySipper.usageTimeMs = TIME_EXPECTED_FOREGROUND; + + assertThat(mBatteryUtils.calculateScreenUsageTime(mBatteryStatsHelper)).isEqualTo( + TIME_EXPECTED_FOREGROUND); + } } diff --git a/tests/robotests/src/com/android/settings/fuelgauge/PowerUsageSummaryTest.java b/tests/robotests/src/com/android/settings/fuelgauge/PowerUsageSummaryTest.java index 272890939aa..6fecf3ce928 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/PowerUsageSummaryTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/PowerUsageSummaryTest.java @@ -247,34 +247,6 @@ public class PowerUsageSummaryTest { assertThat(mFragment.mShowAllApps).isEqualTo(!isShowApps); } - @Test - public void testFindBatterySipperByType_findTypeScreen() { - BatterySipper sipper = mFragment.findBatterySipperByType(mUsageList, - BatterySipper.DrainType.SCREEN); - - assertThat(sipper).isSameAs(mScreenBatterySipper); - } - - @Test - public void testFindBatterySipperByType_findTypeApp() { - BatterySipper sipper = mFragment.findBatterySipperByType(mUsageList, - BatterySipper.DrainType.APP); - - assertThat(sipper).isSameAs(mNormalBatterySipper); - } - - @Test - public void testUpdateScreenPreference_showCorrectSummary() { - doReturn(mScreenBatterySipper).when(mFragment).findBatterySipperByType(any(), any()); - doReturn(mRealContext).when(mFragment).getContext(); - final CharSequence expectedSummary = Utils.formatElapsedTime(mRealContext, USAGE_TIME_MS, - false); - - mFragment.updateScreenPreference(); - - assertThat(mScreenUsagePref.getSubtitle()).isEqualTo(expectedSummary); - } - @Test public void testUpdateLastFullChargePreference_showCorrectSummary() { doReturn(mRealContext).when(mFragment).getContext(); @@ -284,16 +256,6 @@ public class PowerUsageSummaryTest { assertThat(mLastFullChargePref.getSubtitle()).isEqualTo("2 hr. ago"); } - @Test - public void testUpdatePreference_usageListEmpty_shouldNotCrash() { - when(mBatteryHelper.getUsageList()).thenReturn(new ArrayList()); - doReturn(STUB_STRING).when(mFragment).getString(anyInt(), any()); - doReturn(mRealContext).when(mFragment).getContext(); - - // Should not crash when update - mFragment.updateScreenPreference(); - } - @Test public void testNonIndexableKeys_MatchPreferenceKeys() { final Context context = RuntimeEnvironment.application; 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 new file mode 100644 index 00000000000..2a719916fb0 --- /dev/null +++ b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/detectors/HighUsageDetectorTest.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2017 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.detectors; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +import android.content.Context; +import android.os.BatteryStats; +import android.text.format.DateUtils; + +import com.android.internal.os.BatterySipper; +import com.android.internal.os.BatteryStatsHelper; +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.testutils.SettingsRobolectricTestRunner; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.util.ReflectionHelpers; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class HighUsageDetectorTest { + private Context mContext; + @Mock + private BatteryStatsHelper mBatteryStatsHelper; + @Mock + private BatteryUtils mBatteryUtils; + @Mock + private BatterySipper mBatterySipper; + + private BatteryTipPolicy mPolicy; + private HighUsageDetector mHighUsageDetector; + private List mUsageList; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mContext = RuntimeEnvironment.application; + mPolicy = spy(new BatteryTipPolicy(mContext)); + mHighUsageDetector = new HighUsageDetector(mContext, mPolicy, mBatteryStatsHelper); + mHighUsageDetector.mBatteryUtils = mBatteryUtils; + + mUsageList = new ArrayList<>(); + mUsageList.add(mBatterySipper); + } + + @Test + public void testDetect_disabledByPolicy_tipInvisible() { + ReflectionHelpers.setField(mPolicy, "highUsageEnabled", false); + + assertThat(mHighUsageDetector.detect().isVisible()).isFalse(); + } + + @Test + public void testDetect_containsHighUsageApp_tipVisible() { + doReturn(2 * DateUtils.HOUR_IN_MILLIS).when(mBatteryUtils).calculateScreenUsageTime( + mBatteryStatsHelper); + doReturn(mUsageList).when(mBatteryStatsHelper).getUsageList(); + doReturn(DateUtils.HOUR_IN_MILLIS).when(mBatteryUtils).getProcessTimeMs( + BatteryUtils.StatusType.FOREGROUND, mBatterySipper.uidObj, + BatteryStats.STATS_SINCE_CHARGED); + + assertThat(mHighUsageDetector.detect().isVisible()).isTrue(); + } +}