Add wakeup alarm anomaly detector
Wakeup alarm count frequent is calculated by a / b, where
1. a: the total wakeup alarm count since the last full charge
2. b: total time running since last full charge
(include sleeping time)
This cl also has the following changes:
1. Move bunch of methods to BatteryUtils
2. Create type WAKEUP_ALARM
3. Add and update tests
Upcoming cl will make sure we get the threshold from
AnomalyDetectionPolicy
Bug: 36921529
Test: RunSettingsRoboTests
Change-Id: I4f7b85606df68b6057f6c7d3f3be7f9a9a747f1d
diff --git a/src/com/android/settings/fuelgauge/BatteryUtils.java b/src/com/android/settings/fuelgauge/BatteryUtils.java
index 3888ece..327d3a4 100644
--- a/src/com/android/settings/fuelgauge/BatteryUtils.java
+++ b/src/com/android/settings/fuelgauge/BatteryUtils.java
@@ -27,6 +27,8 @@
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 @@
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 63d703b..512dc17 100644
--- a/src/com/android/settings/fuelgauge/PowerUsageSummary.java
+++ b/src/com/android/settings/fuelgauge/PowerUsageSummary.java
@@ -499,7 +499,8 @@
BatteryInfo batteryInfo = getBatteryInfo(elapsedRealtimeUs, batteryBroadcast);
updateHeaderPreference(batteryInfo);
- final long runningTime = calculateRunningTimeBasedOnStatsType();
+ final long runningTime = mBatteryUtils.calculateRunningTimeBasedOnStatsType(mStatsHelper,
+ mStatsType);
updateScreenPreference();
updateLastFullChargePreference(runningTime);
@@ -656,14 +657,6 @@
}
@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();
if (context == null) {
diff --git a/src/com/android/settings/fuelgauge/anomaly/Anomaly.java b/src/com/android/settings/fuelgauge/anomaly/Anomaly.java
index 8aff861..90fc852 100644
--- a/src/com/android/settings/fuelgauge/anomaly/Anomaly.java
+++ b/src/com/android/settings/fuelgauge/anomaly/Anomaly.java
@@ -34,9 +34,11 @@
*/
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 @@
}
@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 61293c6..00ffebd 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 @@
// 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 @@
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 0000000..82c009e
--- /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<Anomaly> detectAnomalies(BatteryStatsHelper batteryStatsHelper) {
+ final List<BatterySipper> batterySippers = batteryStatsHelper.getUsageList();
+ final List<Anomaly> 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<String, ? extends BatteryStats.Uid.Pkg> packageStats
+ = uid.getPackageStats();
+ for (int ipkg = packageStats.size() - 1; ipkg >= 0; ipkg--) {
+ final BatteryStats.Uid.Pkg ps = packageStats.valueAt(ipkg);
+ final ArrayMap<String, ? extends BatteryStats.Counter> 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 03759ec..395d36d 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.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 @@
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 @@
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 @@
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 @@
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 685e921..56dfbcd 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/PowerUsageSummaryTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/PowerUsageSummaryTest.java
@@ -442,12 +442,6 @@
}
@Test
- public void testCalculateRunningTimeBasedOnStatsType() {
- assertThat(mFragment.calculateRunningTimeBasedOnStatsType()).isEqualTo(
- TIME_SINCE_LAST_FULL_CHARGE_MS);
- }
-
- @Test
public void testNonIndexableKeys_MatchPreferenceKeys() {
final Context context = RuntimeEnvironment.application;
final List<String> niks = PowerUsageSummary.SEARCH_INDEX_DATA_PROVIDER
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 0000000..826694d
--- /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<BatterySipper> 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<Anomaly> mAnomalies = mWakeupAlarmAnomalyDetector.detectAnomalies(mBatteryStatsHelper);
+
+ assertThat(mAnomalies).containsExactly(anomaly);
+ }
+
+ @Test
+ public void testGetWakeupAlarmCountFromUid_countCorrect() {
+ final ArrayMap<String, BatteryStats.Uid.Pkg> packageStats = new ArrayMap<>();
+ final ArrayMap<String, BatteryStats.Counter> 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);
+ }
+}