From 4aa3358c4c38e345dc8c464b7bc299d545b22b82 Mon Sep 17 00:00:00 2001 From: jackqdyulei Date: Tue, 25 Apr 2017 18:22:45 -0700 Subject: [PATCH] Add wakelock anomaly detector This cl detects whether apps hold wakelock for long time. For now we use the following attribute: 1. Longest total duration time among all the wakelocks for one app. Following cl will: 1. Get threshold from server side. 2. Add more attributes to make the detection more robust. Bug: 36925184 Test: RunSettingsRoboTests Change-Id: I1946faf69c363f6aa823d0005d6e03bc9082c085 --- .../settings/fuelgauge/anomaly/Anomaly.java | 35 ++++- .../fuelgauge/anomaly/AnomalyLoader.java | 7 +- .../anomaly/AnomalyPreferenceController.java | 8 +- .../checker/WakeLockAnomalyDetector.java | 83 ++++++++++- .../checker/WakeLockAnomalyDetectorTest.java | 140 ++++++++++++++++++ 5 files changed, 263 insertions(+), 10 deletions(-) create mode 100644 tests/robotests/src/com/android/settings/fuelgauge/anomaly/checker/WakeLockAnomalyDetectorTest.java diff --git a/src/com/android/settings/fuelgauge/anomaly/Anomaly.java b/src/com/android/settings/fuelgauge/anomaly/Anomaly.java index a10d3f4047e..6eef1c018a6 100644 --- a/src/com/android/settings/fuelgauge/anomaly/Anomaly.java +++ b/src/com/android/settings/fuelgauge/anomaly/Anomaly.java @@ -19,9 +19,11 @@ package com.android.settings.fuelgauge.anomaly; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.IntDef; +import android.text.TextUtils; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Objects; /** * Data that represents an app has been detected as anomaly. It contains @@ -53,7 +55,7 @@ public class Anomaly implements Parcelable { /** * Display name of this anomaly, usually it is the app name */ - public final String displayName; + public final CharSequence displayName; public final String packageName; private Anomaly(Builder builder) { @@ -67,7 +69,7 @@ public class Anomaly implements Parcelable { private Anomaly(Parcel in) { type = in.readInt(); uid = in.readInt(); - displayName = in.readString(); + displayName = in.readCharSequence(); packageName = in.readString(); wakelockTimeMs = in.readLong(); } @@ -81,11 +83,34 @@ public class Anomaly implements Parcelable { public void writeToParcel(Parcel dest, int flags) { dest.writeInt(type); dest.writeInt(uid); - dest.writeString(displayName); + dest.writeCharSequence(displayName); dest.writeString(packageName); dest.writeLong(wakelockTimeMs); } + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Anomaly)) { + return false; + } + + Anomaly other = (Anomaly) obj; + + return type == other.type + && uid == other.uid + && wakelockTimeMs == other.wakelockTimeMs + && TextUtils.equals(displayName, other.displayName) + && TextUtils.equals(packageName, other.packageName); + } + + @Override + public int hashCode() { + return Objects.hash(type, uid, displayName, packageName, wakelockTimeMs); + } + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { public Anomaly createFromParcel(Parcel in) { return new Anomaly(in); @@ -100,7 +125,7 @@ public class Anomaly implements Parcelable { @AnomalyType private int mType; private int mUid; - private String mDisplayName; + private CharSequence mDisplayName; private String mPackageName; private long mWakeLockTimeMs; @@ -114,7 +139,7 @@ public class Anomaly implements Parcelable { return this; } - public Builder setDisplayName(String displayName) { + public Builder setDisplayName(CharSequence displayName) { mDisplayName = displayName; return this; } diff --git a/src/com/android/settings/fuelgauge/anomaly/AnomalyLoader.java b/src/com/android/settings/fuelgauge/anomaly/AnomalyLoader.java index e689256a8c3..f530035f819 100644 --- a/src/com/android/settings/fuelgauge/anomaly/AnomalyLoader.java +++ b/src/com/android/settings/fuelgauge/anomaly/AnomalyLoader.java @@ -39,13 +39,16 @@ public class AnomalyLoader extends AsyncLoader> { } @Override - protected void onDiscardResult(List result) {} + protected void onDiscardResult(List result) { + } @Override public List loadInBackground() { final List anomalies = new ArrayList<>(); - anomalies.addAll(new WakeLockAnomalyDetector().detectAnomalies(mBatteryStatsHelper)); + anomalies.addAll(new WakeLockAnomalyDetector(getContext()) + .detectAnomalies(mBatteryStatsHelper)); return anomalies; } + } diff --git a/src/com/android/settings/fuelgauge/anomaly/AnomalyPreferenceController.java b/src/com/android/settings/fuelgauge/anomaly/AnomalyPreferenceController.java index b499690d686..ff7809ee25e 100644 --- a/src/com/android/settings/fuelgauge/anomaly/AnomalyPreferenceController.java +++ b/src/com/android/settings/fuelgauge/anomaly/AnomalyPreferenceController.java @@ -20,6 +20,8 @@ import android.support.annotation.VisibleForTesting; import android.support.v14.preference.PreferenceFragment; import android.support.v7.preference.Preference; +import com.android.settings.R; + import java.util.List; /** @@ -66,8 +68,10 @@ public class AnomalyPreferenceController { public void updateAnomalyPreference(List anomalies) { mAnomalies = anomalies; - mAnomalyPreference.setVisible(true); - //TODO(b/36924669): update summary for anomaly preference + if (!mAnomalies.isEmpty()) { + mAnomalyPreference.setVisible(true); + //TODO(b/36924669): update summary for anomaly preference + } } public void hideAnomalyPreference() { diff --git a/src/com/android/settings/fuelgauge/anomaly/checker/WakeLockAnomalyDetector.java b/src/com/android/settings/fuelgauge/anomaly/checker/WakeLockAnomalyDetector.java index a9ebc662dfd..61293c66b89 100644 --- a/src/com/android/settings/fuelgauge/anomaly/checker/WakeLockAnomalyDetector.java +++ b/src/com/android/settings/fuelgauge/anomaly/checker/WakeLockAnomalyDetector.java @@ -16,7 +16,20 @@ package com.android.settings.fuelgauge.anomaly.checker; +import android.content.Context; +import android.content.pm.ApplicationInfo; +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 android.util.Log; + +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 java.util.ArrayList; @@ -26,11 +39,79 @@ import java.util.List; * Check whether apps holding wakelock too long */ public class WakeLockAnomalyDetector implements AnomalyDetector { + private static final String TAG = "WakeLockAnomalyChecker"; + // TODO: get threshold form server side + private static final long WAKE_LOCK_THRESHOLD_MS = 2 * DateUtils.MINUTE_IN_MILLIS; + private PackageManager mPackageManager; + private Context mContext; + @VisibleForTesting + BatteryUtils mBatteryUtils; + + public WakeLockAnomalyDetector(Context context) { + mContext = context; + mPackageManager = context.getPackageManager(); + mBatteryUtils = BatteryUtils.getInstance(context); + } @Override public List detectAnomalies(BatteryStatsHelper batteryStatsHelper) { - //TODO(b/36921529): check anomaly using the batteryStatsHelper + final List batterySippers = batteryStatsHelper.getUsageList(); final List anomalies = new ArrayList<>(); + final long rawRealtime = SystemClock.elapsedRealtime(); + + // Check the app one by one + 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) { + continue; + } + final ArrayMap wakelocks = + uid.getWakelockStats(); + long maxPartialWakeLockMs = 0; + + for (int iw = wakelocks.size() - 1; iw >= 0; iw--) { + final BatteryStats.Timer timer = wakelocks.valueAt(iw).getWakeTime( + BatteryStats.WAKE_TYPE_PARTIAL); + if (timer == null) { + continue; + } + maxPartialWakeLockMs = Math.max(maxPartialWakeLockMs, + getTotalDurationMs(timer, rawRealtime)); + } + + // Report it if wakelock time is too long and it is not a hidden batterysipper + // TODO: add more attributes to detect wakelock anomaly + if (maxPartialWakeLockMs > WAKE_LOCK_THRESHOLD_MS + && !mBatteryUtils.shouldHideSipper(sipper)) { + final String packageName = getPackageName(uid.getUid()); + final CharSequence displayName = Utils.getApplicationLabel(mContext, + packageName); + + Anomaly anomaly = new Anomaly.Builder() + .setUid(uid.getUid()) + .setType(Anomaly.AnomalyType.WAKE_LOCK) + .setDisplayName(displayName) + .setPackageName(packageName) + .build(); + anomalies.add(anomaly); + } + + } 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) { + return 0; + } + return timer.getTotalDurationMsLocked(rawRealtime); + } } diff --git a/tests/robotests/src/com/android/settings/fuelgauge/anomaly/checker/WakeLockAnomalyDetectorTest.java b/tests/robotests/src/com/android/settings/fuelgauge/anomaly/checker/WakeLockAnomalyDetectorTest.java new file mode 100644 index 00000000000..70107836343 --- /dev/null +++ b/tests/robotests/src/com/android/settings/fuelgauge/anomaly/checker/WakeLockAnomalyDetectorTest.java @@ -0,0 +1,140 @@ +/* + * 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 WakeLockAnomalyDetectorTest { + private static final long ANOMALY_WAKELOCK_TIME_MS = DateUtils.HOUR_IN_MILLIS; + private static final long NORMAL_WAKELOCK_TIME_MS = DateUtils.SECOND_IN_MILLIS; + private static final int ANOMALY_UID = 111; + private static final int NORMAL_UID = 222; + @Mock + private BatteryStatsHelper mBatteryStatsHelper; + @Mock + private BatterySipper mAnomalySipper; + @Mock + private BatteryStats.Timer mAnomalyTimer; + @Mock + private BatteryStats.Uid.Wakelock mAnomalyWakelock; + @Mock + private BatterySipper mNormalSipper; + @Mock + private BatteryStats.Timer mNormalTimer; + @Mock + private BatteryStats.Uid.Wakelock mNormalWakelock; + @Mock + private BatteryStats.Uid mAnomalyUid; + @Mock + private BatteryStats.Uid mNormalUid; + @Mock + private BatteryUtils mBatteryUtils; + @Mock + private PackageManager mPackageManager; + @Mock + private ApplicationInfo mApplicationInfo; + + private ArrayMap mAnomalyWakelocks; + private ArrayMap mNormalWakelocks; + private WakeLockAnomalyDetector mWakelockAnomalyDetector; + 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(mPackageManager).when(mContext).getPackageManager(); + doReturn(mApplicationInfo).when(mPackageManager).getApplicationInfo(anyString(), anyInt()); + + mAnomalySipper.uidObj = mAnomalyUid; + mAnomalyWakelocks = new ArrayMap<>(); + mAnomalyWakelocks.put("", mAnomalyWakelock); + doReturn(mAnomalyWakelocks).when(mAnomalyUid).getWakelockStats(); + doReturn(mAnomalyTimer).when(mAnomalyWakelock).getWakeTime(BatteryStats.WAKE_TYPE_PARTIAL); + doReturn(ANOMALY_UID).when(mAnomalyUid).getUid(); + + mNormalSipper.uidObj = mNormalUid; + mNormalWakelocks = new ArrayMap<>(); + mNormalWakelocks.put("", mNormalWakelock); + doReturn(mNormalTimer).when(mNormalWakelock).getWakeTime(BatteryStats.WAKE_TYPE_PARTIAL); + doReturn(mNormalWakelocks).when(mNormalUid).getWakelockStats(); + doReturn(NORMAL_UID).when(mNormalUid).getUid(); + + mUsageList = new ArrayList<>(); + mUsageList.add(mAnomalySipper); + mUsageList.add(mNormalSipper); + doReturn(mUsageList).when(mBatteryStatsHelper).getUsageList(); + + mWakelockAnomalyDetector = spy(new WakeLockAnomalyDetector(mContext)); + mWakelockAnomalyDetector.mBatteryUtils = mBatteryUtils; + doReturn(ANOMALY_WAKELOCK_TIME_MS).when(mWakelockAnomalyDetector).getTotalDurationMs( + eq(mAnomalyTimer), anyLong()); + doReturn(NORMAL_WAKELOCK_TIME_MS).when(mWakelockAnomalyDetector).getTotalDurationMs( + eq(mNormalTimer), anyLong()); + } + + @Test + public void testDetectAnomalies_containsAnomaly_detectIt() { + final Anomaly anomaly = new Anomaly.Builder() + .setUid(ANOMALY_UID) + .setType(Anomaly.AnomalyType.WAKE_LOCK) + .build(); + + List mAnomalies = mWakelockAnomalyDetector.detectAnomalies(mBatteryStatsHelper); + + assertThat(mAnomalies).containsExactly(anomaly); + } +}