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);
+    }
+}