Add high usage battery tip

1. Add both model and detector
2. Move the screen usage method to BatteryUtils
so we could reuse it.
3. Add and update the tests

Bug: 70570352
Test: RunSettingsRoboTests

Change-Id: I6a7248d9d48ee8cb6fc2c18c8c225210d49b6bc9
This commit is contained in:
jackqdyulei
2018-01-02 14:19:06 -08:00
parent 5ff1df89fa
commit ca102facf0
9 changed files with 346 additions and 59 deletions

View File

@@ -4773,6 +4773,21 @@
<string name="battery_tip_low_battery_title">Low battery capacity</string> <string name="battery_tip_low_battery_title">Low battery capacity</string>
<!-- Summary for the low battery tip [CHAR LIMIT=NONE] --> <!-- Summary for the low battery tip [CHAR LIMIT=NONE] -->
<string name="battery_tip_low_battery_summary">Battery can\'t provide good battery life</string> <string name="battery_tip_low_battery_summary">Battery can\'t provide good battery life</string>
<!-- Title for the battery high usage tip [CHAR LIMIT=NONE] -->
<string name="battery_tip_high_usage_title" product="default">Phone used heavily</string>
<!-- Title for the battery high usage tip [CHAR LIMIT=NONE] -->
<string name="battery_tip_high_usage_title" product="tablet">Tablet used heavily</string>
<!-- Title for the battery high usage tip [CHAR LIMIT=NONE] -->
<string name="battery_tip_high_usage_title" product="device">Device used heavily</string>
<!-- Summary for the battery high usage tip, which presents how many hours the device been used since last full charge [CHAR LIMIT=NONE] -->
<string name="battery_tip_high_usage_summary">About <xliff:g id="hour">%1$s</xliff:g> used since last full charge</string>
<!-- Message for battery tip dialog to show the status about the battery [CHAR LIMIT=NONE] -->
<string name="battery_tip_dialog_message" product="default">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 <xliff:g id="hour">%1$s</xliff:g> since last full charge.\n\n Total usage:</string>
<!-- Message for battery tip dialog to show the status about the battery [CHAR LIMIT=NONE] -->
<string name="battery_tip_dialog_message" product="tablet">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 <xliff:g id="hour">%1$s</xliff:g> since last full charge.\n\n Total usage:</string>
<!-- Message for battery tip dialog to show the status about the battery [CHAR LIMIT=NONE] -->
<string name="battery_tip_dialog_message" product="device">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 <xliff:g id="hour">%1$s</xliff:g> since last full charge.\n\n Total usage:</string>
<!-- Title for the smart battery manager preference [CHAR LIMIT=NONE] --> <!-- Title for the smart battery manager preference [CHAR LIMIT=NONE] -->
<string name="smart_battery_manager_title">Smart battery manager</string> <string name="smart_battery_manager_title">Smart battery manager</string>
<!-- Title for the smart battery toggle [CHAR LIMIT=NONE] --> <!-- Title for the smart battery toggle [CHAR LIMIT=NONE] -->

View File

@@ -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) { public static void logRuntime(String tag, String message, long startTime) {
Log.d(tag, message + ": " + (System.currentTimeMillis() - startTime) + "ms"); Log.d(tag, message + ": " + (System.currentTimeMillis() - startTime) + "ms");
} }
@@ -432,6 +443,20 @@ public class BatteryUtils {
return batteryInfo; return batteryInfo;
} }
/**
* Find the {@link BatterySipper} with the corresponding {@link BatterySipper.DrainType}
*/
public BatterySipper findBatterySipperByType(List<BatterySipper> 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() { private boolean isDataCorrupted() {
return mPackageManager == null || mAppOpsManager == null; return mPackageManager == null || mAppOpsManager == null;
} }

View File

@@ -369,8 +369,9 @@ public class PowerUsageSummary extends PowerUsageBase implements OnLongClickList
restartBatteryInfoLoader(); restartBatteryInfoLoader();
final long lastFullChargeTime = mBatteryUtils.calculateLastFullChargeTime(mStatsHelper, final long lastFullChargeTime = mBatteryUtils.calculateLastFullChargeTime(mStatsHelper,
System.currentTimeMillis()); System.currentTimeMillis());
updateScreenPreference();
updateLastFullChargePreference(lastFullChargeTime); updateLastFullChargePreference(lastFullChargeTime);
mScreenUsagePref.setSubtitle(Utils.formatElapsedTime(getContext(),
mBatteryUtils.calculateScreenUsageTime(mStatsHelper), false));
final CharSequence timeSequence = Utils.formatRelativeTime(context, lastFullChargeTime, final CharSequence timeSequence = Utils.formatRelativeTime(context, lastFullChargeTime,
false); false);
@@ -393,26 +394,6 @@ public class PowerUsageSummary extends PowerUsageBase implements OnLongClickList
return new AnomalyDetectionPolicy(getContext()); return new AnomalyDetectionPolicy(getContext());
} }
@VisibleForTesting
BatterySipper findBatterySipperByType(List<BatterySipper> 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 @VisibleForTesting
void updateLastFullChargePreference(long timeMs) { void updateLastFullChargePreference(long timeMs) {
final CharSequence timeSequence = Utils.formatRelativeTime(getContext(), timeMs, false); final CharSequence timeSequence = Utils.formatRelativeTime(getContext(), timeMs, false);

View File

@@ -23,6 +23,7 @@ import com.android.internal.os.BatteryStatsHelper;
import com.android.settings.fuelgauge.BatteryInfo; import com.android.settings.fuelgauge.BatteryInfo;
import com.android.settings.fuelgauge.BatteryUtils; import com.android.settings.fuelgauge.BatteryUtils;
import com.android.settings.fuelgauge.batterytip.detectors.BatteryTipDetector; 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.LowBatteryDetector;
import com.android.settings.fuelgauge.batterytip.detectors.SummaryDetector; import com.android.settings.fuelgauge.batterytip.detectors.SummaryDetector;
import com.android.settings.fuelgauge.batterytip.tips.BatteryTip; import com.android.settings.fuelgauge.batterytip.tips.BatteryTip;
@@ -65,6 +66,8 @@ public class BatteryTipLoader extends AsyncLoader<List<BatteryTip>> {
mVisibleTips = 0; mVisibleTips = 0;
addBatteryTipFromDetector(tips, new LowBatteryDetector(policy, batteryInfo)); 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 // Add summary detector at last since it need other detectors to update the mVisibleTips
addBatteryTipFromDetector(tips, new SummaryDetector(policy, mVisibleTips)); addBatteryTipFromDetector(tips, new SummaryDetector(policy, mVisibleTips));

View File

@@ -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<HighUsageTip.HighUsageApp> 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<BatterySipper> 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);
}
}

View File

@@ -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<HighUsageApp> mHighUsageAppList;
public HighUsageTip(CharSequence screenTimeText, List<HighUsageApp> 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<HighUsageApp> {
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);
}
}
}

View File

@@ -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_FOREGROUND_SERVICE;
import static android.os.BatteryStats.Uid.PROCESS_STATE_TOP; import static android.os.BatteryStats.Uid.PROCESS_STATE_TOP;
import static android.os.BatteryStats.Uid.PROCESS_STATE_TOP_SLEEPING; import static android.os.BatteryStats.Uid.PROCESS_STATE_TOP_SLEEPING;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Matchers.any; import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyLong; import static org.mockito.Matchers.anyLong;
@@ -141,6 +143,7 @@ public class BatteryUtilsTest {
private BatteryUtils mBatteryUtils; private BatteryUtils mBatteryUtils;
private FakeFeatureFactory mFeatureFactory; private FakeFeatureFactory mFeatureFactory;
private PowerUsageFeatureProvider mProvider; private PowerUsageFeatureProvider mProvider;
private List<BatterySipper> mUsageList;
@Before @Before
public void setUp() { public void setUp() {
@@ -194,6 +197,12 @@ public class BatteryUtilsTest {
mBatteryUtils.mPowerUsageFeatureProvider = mProvider; mBatteryUtils.mPowerUsageFeatureProvider = mProvider;
doReturn(0L).when(mBatteryUtils).getForegroundServiceTotalTimeUs( doReturn(0L).when(mBatteryUtils).getForegroundServiceTotalTimeUs(
any(BatteryStats.Uid.class), anyLong()); any(BatteryStats.Uid.class), anyLong());
mUsageList = new ArrayList<>();
mUsageList.add(mNormalBatterySipper);
mUsageList.add(mScreenBatterySipper);
mUsageList.add(mCellBatterySipper);
doReturn(mUsageList).when(mBatteryStatsHelper).getUsageList();
} }
@Test @Test
@@ -468,4 +477,28 @@ public class BatteryUtilsTest {
verify(mBatteryStatsHelper).refreshStats(BatteryStats.STATS_SINCE_CHARGED, verify(mBatteryStatsHelper).refreshStats(BatteryStats.STATS_SINCE_CHARGED,
mUserManager.getUserProfiles()); 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);
}
} }

View File

@@ -247,34 +247,6 @@ public class PowerUsageSummaryTest {
assertThat(mFragment.mShowAllApps).isEqualTo(!isShowApps); 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 @Test
public void testUpdateLastFullChargePreference_showCorrectSummary() { public void testUpdateLastFullChargePreference_showCorrectSummary() {
doReturn(mRealContext).when(mFragment).getContext(); doReturn(mRealContext).when(mFragment).getContext();
@@ -284,16 +256,6 @@ public class PowerUsageSummaryTest {
assertThat(mLastFullChargePref.getSubtitle()).isEqualTo("2 hr. ago"); assertThat(mLastFullChargePref.getSubtitle()).isEqualTo("2 hr. ago");
} }
@Test
public void testUpdatePreference_usageListEmpty_shouldNotCrash() {
when(mBatteryHelper.getUsageList()).thenReturn(new ArrayList<BatterySipper>());
doReturn(STUB_STRING).when(mFragment).getString(anyInt(), any());
doReturn(mRealContext).when(mFragment).getContext();
// Should not crash when update
mFragment.updateScreenPreference();
}
@Test @Test
public void testNonIndexableKeys_MatchPreferenceKeys() { public void testNonIndexableKeys_MatchPreferenceKeys() {
final Context context = RuntimeEnvironment.application; final Context context = RuntimeEnvironment.application;

View File

@@ -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<BatterySipper> 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();
}
}