diff --git a/src/com/android/settings/fuelgauge/BatteryUtils.java b/src/com/android/settings/fuelgauge/BatteryUtils.java index 3888ece953e..327d3a4a340 100644 --- a/src/com/android/settings/fuelgauge/BatteryUtils.java +++ b/src/com/android/settings/fuelgauge/BatteryUtils.java @@ -27,6 +27,8 @@ import android.util.Log; import android.util.SparseLongArray; import com.android.internal.os.BatterySipper; +import com.android.internal.os.BatteryStatsHelper; +import com.android.internal.util.ArrayUtils; import com.android.settings.overlay.FeatureFactory; import java.lang.annotation.Retention; @@ -213,6 +215,38 @@ public class BatteryUtils { return (powerUsageMah / (totalPowerMah - hiddenPowerMah)) * dischargeAmount; } + /** + * Calculate the whole running time in the state {@code statsType} + * + * @param batteryStatsHelper utility class that contains the data + * @param statsType state that we want to calculate the time for + * @return the running time in millis + */ + public long calculateRunningTimeBasedOnStatsType(BatteryStatsHelper batteryStatsHelper, + int statsType) { + final long elapsedRealtimeUs = convertMsToUs(SystemClock.elapsedRealtime()); + // Return the battery time (millisecond) on status mStatsType + return convertUsToMs( + batteryStatsHelper.getStats().computeBatteryRealtime(elapsedRealtimeUs, statsType)); + + } + + /** + * Find the package name for a {@link android.os.BatteryStats.Uid} + * + * @param uid id to get the package name + * @return the package name. If there are multiple packages related to + * given id, return the first one. Or return null if there are no known + * packages with the given id + * + * @see PackageManager#getPackagesForUid(int) + */ + public String getPackageName(int uid) { + final String[] packageNames = mPackageManager.getPackagesForUid(uid); + + return ArrayUtils.isEmpty(packageNames) ? null : packageNames[0]; + } + private long convertUsToMs(long timeUs) { return timeUs / 1000; } diff --git a/src/com/android/settings/fuelgauge/PowerUsageSummary.java b/src/com/android/settings/fuelgauge/PowerUsageSummary.java index 63d703b9cbd..512dc179fa3 100644 --- a/src/com/android/settings/fuelgauge/PowerUsageSummary.java +++ b/src/com/android/settings/fuelgauge/PowerUsageSummary.java @@ -499,7 +499,8 @@ public class PowerUsageSummary extends PowerUsageBase implements BatteryInfo batteryInfo = getBatteryInfo(elapsedRealtimeUs, batteryBroadcast); updateHeaderPreference(batteryInfo); - final long runningTime = calculateRunningTimeBasedOnStatsType(); + final long runningTime = mBatteryUtils.calculateRunningTimeBasedOnStatsType(mStatsHelper, + mStatsType); updateScreenPreference(); updateLastFullChargePreference(runningTime); @@ -655,14 +656,6 @@ public class PowerUsageSummary extends PowerUsageBase implements timeSequence)); } - @VisibleForTesting - long calculateRunningTimeBasedOnStatsType() { - final long elapsedRealtimeUs = mBatteryUtils.convertMsToUs(SystemClock.elapsedRealtime()); - // Return the battery time (millisecond) on status mStatsType - return mStatsHelper.getStats().computeBatteryRealtime(elapsedRealtimeUs, - mStatsType /* STATS_SINCE_CHARGED */) / 1000; - } - @VisibleForTesting void updateHeaderPreference(BatteryInfo info) { final Context context = getContext(); diff --git a/src/com/android/settings/fuelgauge/anomaly/Anomaly.java b/src/com/android/settings/fuelgauge/anomaly/Anomaly.java index 8aff86144ea..90fc852daf3 100644 --- a/src/com/android/settings/fuelgauge/anomaly/Anomaly.java +++ b/src/com/android/settings/fuelgauge/anomaly/Anomaly.java @@ -34,9 +34,11 @@ import java.util.Objects; */ public class Anomaly implements Parcelable { @Retention(RetentionPolicy.SOURCE) - @IntDef({AnomalyType.WAKE_LOCK}) + @IntDef({AnomalyType.WAKE_LOCK, + AnomalyType.WAKEUP_ALARM}) public @interface AnomalyType { int WAKE_LOCK = 0; + int WAKEUP_ALARM = 1; } @Retention(RetentionPolicy.SOURCE) @@ -46,7 +48,9 @@ public class Anomaly implements Parcelable { } @AnomalyType - public static final int[] ANOMALY_TYPE_LIST = {AnomalyType.WAKE_LOCK}; + public static final int[] ANOMALY_TYPE_LIST = + {AnomalyType.WAKE_LOCK, + AnomalyType.WAKEUP_ALARM}; /** * Type of this this anomaly diff --git a/src/com/android/settings/fuelgauge/anomaly/checker/WakeLockAnomalyDetector.java b/src/com/android/settings/fuelgauge/anomaly/checker/WakeLockAnomalyDetector.java index 61293c66b89..00ffebd8c6c 100644 --- a/src/com/android/settings/fuelgauge/anomaly/checker/WakeLockAnomalyDetector.java +++ b/src/com/android/settings/fuelgauge/anomaly/checker/WakeLockAnomalyDetector.java @@ -84,7 +84,7 @@ public class WakeLockAnomalyDetector implements AnomalyDetector { // TODO: add more attributes to detect wakelock anomaly if (maxPartialWakeLockMs > WAKE_LOCK_THRESHOLD_MS && !mBatteryUtils.shouldHideSipper(sipper)) { - final String packageName = getPackageName(uid.getUid()); + final String packageName = mBatteryUtils.getPackageName(uid.getUid()); final CharSequence displayName = Utils.getApplicationLabel(mContext, packageName); @@ -101,12 +101,6 @@ public class WakeLockAnomalyDetector implements AnomalyDetector { return anomalies; } - private String getPackageName(int uid) { - final String[] packageNames = mPackageManager.getPackagesForUid(uid); - - return packageNames == null ? null : packageNames[0]; - } - @VisibleForTesting long getTotalDurationMs(BatteryStats.Timer timer, long rawRealtime) { if (timer == null) { diff --git a/src/com/android/settings/fuelgauge/anomaly/checker/WakeupAlarmAnomalyDetector.java b/src/com/android/settings/fuelgauge/anomaly/checker/WakeupAlarmAnomalyDetector.java new file mode 100644 index 00000000000..82c009ed296 --- /dev/null +++ b/src/com/android/settings/fuelgauge/anomaly/checker/WakeupAlarmAnomalyDetector.java @@ -0,0 +1,107 @@ +/* + * 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.anomaly.checker; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.BatteryStats; +import android.os.SystemClock; +import android.support.annotation.VisibleForTesting; +import android.text.format.DateUtils; +import android.util.ArrayMap; + +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.anomaly.Anomaly; +import com.android.settings.fuelgauge.anomaly.AnomalyDetectionPolicy; + +import java.util.ArrayList; +import java.util.List; + +/** + * Check whether apps has too many wakeup alarms + */ +public class WakeupAlarmAnomalyDetector implements AnomalyDetector { + private static final String TAG = "WakeupAlarmAnomalyDetector"; + //TODO: add this threshold into AnomalyDetectionPolicy + private static final int WAKEUP_ALARM_THRESHOLD = 60; + private Context mContext; + @VisibleForTesting + BatteryUtils mBatteryUtils; + + public WakeupAlarmAnomalyDetector(Context context) { + mContext = context; + mBatteryUtils = BatteryUtils.getInstance(context); + } + + @Override + public List detectAnomalies(BatteryStatsHelper batteryStatsHelper) { + final List batterySippers = batteryStatsHelper.getUsageList(); + final List anomalies = new ArrayList<>(); + final long totalRunningHours = mBatteryUtils.calculateRunningTimeBasedOnStatsType( + batteryStatsHelper, BatteryStats.STATS_SINCE_CHARGED) / DateUtils.HOUR_IN_MILLIS; + + if (totalRunningHours != 0) { + for (int i = 0, size = batterySippers.size(); i < size; i++) { + final BatterySipper sipper = batterySippers.get(i); + final BatteryStats.Uid uid = sipper.uidObj; + if (uid == null || mBatteryUtils.shouldHideSipper(sipper)) { + continue; + } + + final int wakeups = getWakeupAlarmCountFromUid(uid); + if ((wakeups / totalRunningHours) > WAKEUP_ALARM_THRESHOLD) { + final String packageName = mBatteryUtils.getPackageName(uid.getUid()); + final CharSequence displayName = Utils.getApplicationLabel(mContext, + packageName); + + Anomaly anomaly = new Anomaly.Builder() + .setUid(uid.getUid()) + .setType(Anomaly.AnomalyType.WAKEUP_ALARM) + .setDisplayName(displayName) + .setPackageName(packageName) + .build(); + anomalies.add(anomaly); + } + } + } + + return anomalies; + } + + @VisibleForTesting + int getWakeupAlarmCountFromUid(BatteryStats.Uid uid) { + int wakeups = 0; + final ArrayMap packageStats + = uid.getPackageStats(); + for (int ipkg = packageStats.size() - 1; ipkg >= 0; ipkg--) { + final BatteryStats.Uid.Pkg ps = packageStats.valueAt(ipkg); + final ArrayMap alarms = + ps.getWakeupAlarmStats(); + for (int iwa = alarms.size() - 1; iwa >= 0; iwa--) { + int count = alarms.valueAt(iwa).getCountLocked(BatteryStats.STATS_SINCE_CHARGED); + wakeups += count; + } + + } + + return wakeups; + } + +} diff --git a/tests/robotests/src/com/android/settings/fuelgauge/BatteryUtilsTest.java b/tests/robotests/src/com/android/settings/fuelgauge/BatteryUtilsTest.java index 03759ec6fdc..395d36d1b32 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/BatteryUtilsTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/BatteryUtilsTest.java @@ -21,6 +21,7 @@ import android.os.Process; import android.text.format.DateUtils; import com.android.internal.os.BatterySipper; +import com.android.internal.os.BatteryStatsHelper; import com.android.settings.SettingsRobolectricTestRunner; import com.android.settings.TestConfig; import com.android.settings.testutils.FakeFeatureFactory; @@ -68,6 +69,9 @@ public class BatteryUtilsTest { private static final long TIME_STATE_BACKGROUND = 6000 * UNIT; private static final long TIME_FOREGROUND_ACTIVITY_ZERO = 0; private static final long TIME_FOREGROUND_ACTIVITY = 100 * DateUtils.MINUTE_IN_MILLIS; + private static final long TIME_SINCE_LAST_FULL_CHARGE_MS = 120 * 60 * 1000; + private static final long TIME_SINCE_LAST_FULL_CHARGE_US = + TIME_SINCE_LAST_FULL_CHARGE_MS * 1000; private static final int UID = 123; private static final long TIME_EXPECTED_FOREGROUND = 1500; @@ -100,6 +104,8 @@ public class BatteryUtilsTest { private BatterySipper mCellBatterySipper; @Mock(answer = Answers.RETURNS_DEEP_STUBS) private Context mContext; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private BatteryStatsHelper mBatteryStatsHelper; private BatteryUtils mBatteryUtils; private FakeFeatureFactory mFeatureFactory; private PowerUsageFeatureProvider mProvider; @@ -122,6 +128,8 @@ public class BatteryUtilsTest { anyLong(), anyInt()); doReturn(TIME_STATE_BACKGROUND).when(mUid).getProcessStateTime(eq(PROCESS_STATE_BACKGROUND), anyLong(), anyInt()); + when(mBatteryStatsHelper.getStats().computeBatteryRealtime(anyLong(), anyInt())).thenReturn( + TIME_SINCE_LAST_FULL_CHARGE_US); mNormalBatterySipper.drainType = BatterySipper.DrainType.APP; mNormalBatterySipper.totalPowerMah = TOTAL_BATTERY_USAGE; @@ -278,6 +286,12 @@ public class BatteryUtilsTest { BATTERY_APP_USAGE + BATTERY_SCREEN_USAGE); } + @Test + public void testCalculateRunningTimeBasedOnStatsType() { + assertThat(mBatteryUtils.calculateRunningTimeBasedOnStatsType(mBatteryStatsHelper, + BatteryStats.STATS_SINCE_CHARGED)).isEqualTo(TIME_SINCE_LAST_FULL_CHARGE_MS); + } + private BatterySipper createTestSmearBatterySipper(long activityTime, double totalPowerMah, int uidCode, boolean isUidNull) { final BatterySipper sipper = mock(BatterySipper.class); diff --git a/tests/robotests/src/com/android/settings/fuelgauge/PowerUsageSummaryTest.java b/tests/robotests/src/com/android/settings/fuelgauge/PowerUsageSummaryTest.java index 685e9212f77..56dfbcdd04e 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/PowerUsageSummaryTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/PowerUsageSummaryTest.java @@ -441,12 +441,6 @@ public class PowerUsageSummaryTest { assertThat(percent).isWithin(PRECISION).of(POWER_USAGE_PERCENTAGE); } - @Test - public void testCalculateRunningTimeBasedOnStatsType() { - assertThat(mFragment.calculateRunningTimeBasedOnStatsType()).isEqualTo( - TIME_SINCE_LAST_FULL_CHARGE_MS); - } - @Test public void testNonIndexableKeys_MatchPreferenceKeys() { final Context context = RuntimeEnvironment.application; diff --git a/tests/robotests/src/com/android/settings/fuelgauge/anomaly/checker/WakeupAlarmAnomalyDetectorTest.java b/tests/robotests/src/com/android/settings/fuelgauge/anomaly/checker/WakeupAlarmAnomalyDetectorTest.java new file mode 100644 index 00000000000..826694d6242 --- /dev/null +++ b/tests/robotests/src/com/android/settings/fuelgauge/anomaly/checker/WakeupAlarmAnomalyDetectorTest.java @@ -0,0 +1,139 @@ +/* + * 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.anomaly.checker; + +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; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.BatteryStats; +import android.text.format.DateUtils; +import android.util.ArrayMap; + +import com.android.internal.os.BatterySipper; +import com.android.internal.os.BatteryStatsHelper; +import com.android.settings.SettingsRobolectricTestRunner; +import com.android.settings.TestConfig; +import com.android.settings.fuelgauge.BatteryUtils; +import com.android.settings.fuelgauge.anomaly.Anomaly; + +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 java.util.ArrayList; +import java.util.List; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class WakeupAlarmAnomalyDetectorTest { + private static final int ANOMALY_UID = 111; + private static final int NORMAL_UID = 222; + private static final long RUNNING_TIME_MS = 2 * DateUtils.HOUR_IN_MILLIS; + private static final int ANOMALY_WAKEUP_COUNT = 500; + private static final int NORMAL_WAKEUP_COUNT = 50; + @Mock + private BatteryStatsHelper mBatteryStatsHelper; + @Mock + private BatterySipper mAnomalySipper; + @Mock + private BatterySipper mNormalSipper; + @Mock + private BatteryStats.Uid mAnomalyUid; + @Mock + private BatteryStats.Uid mNormalUid; + @Mock + private BatteryUtils mBatteryUtils; + @Mock + private ApplicationInfo mApplicationInfo; + @Mock + private BatteryStats.Uid.Pkg mPkg; + @Mock + private BatteryStats.Counter mCounter; + + private WakeupAlarmAnomalyDetector mWakeupAlarmAnomalyDetector; + private Context mContext; + private List mUsageList; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mContext = spy(RuntimeEnvironment.application); + + doReturn(false).when(mBatteryUtils).shouldHideSipper(any()); + doReturn(RUNNING_TIME_MS).when(mBatteryUtils).calculateRunningTimeBasedOnStatsType(any(), + anyInt()); + + mAnomalySipper.uidObj = mAnomalyUid; + doReturn(ANOMALY_UID).when(mAnomalyUid).getUid(); + mNormalSipper.uidObj = mNormalUid; + doReturn(NORMAL_UID).when(mNormalUid).getUid(); + + mUsageList = new ArrayList<>(); + mUsageList.add(mAnomalySipper); + mUsageList.add(mNormalSipper); + doReturn(mUsageList).when(mBatteryStatsHelper).getUsageList(); + + mWakeupAlarmAnomalyDetector = spy(new WakeupAlarmAnomalyDetector(mContext)); + mWakeupAlarmAnomalyDetector.mBatteryUtils = mBatteryUtils; + } + + @Test + public void testDetectAnomalies_containsAnomaly_detectIt() { + doReturn(ANOMALY_WAKEUP_COUNT).when(mWakeupAlarmAnomalyDetector).getWakeupAlarmCountFromUid( + mAnomalyUid); + doReturn(NORMAL_WAKEUP_COUNT).when(mWakeupAlarmAnomalyDetector).getWakeupAlarmCountFromUid( + mNormalUid); + final Anomaly anomaly = new Anomaly.Builder() + .setUid(ANOMALY_UID) + .setType(Anomaly.AnomalyType.WAKEUP_ALARM) + .build(); + + List mAnomalies = mWakeupAlarmAnomalyDetector.detectAnomalies(mBatteryStatsHelper); + + assertThat(mAnomalies).containsExactly(anomaly); + } + + @Test + public void testGetWakeupAlarmCountFromUid_countCorrect() { + final ArrayMap packageStats = new ArrayMap<>(); + final ArrayMap alarms = new ArrayMap<>(); + doReturn(alarms).when(mPkg).getWakeupAlarmStats(); + doReturn(NORMAL_WAKEUP_COUNT).when(mCounter).getCountLocked(anyInt()); + doReturn(packageStats).when(mAnomalyUid).getPackageStats(); + packageStats.put("", mPkg); + alarms.put("1", mCounter); + alarms.put("2", mCounter); + + assertThat(mWakeupAlarmAnomalyDetector.getWakeupAlarmCountFromUid(mAnomalyUid)).isEqualTo( + 2 * NORMAL_WAKEUP_COUNT); + } +}