Add auto restriction for excessive background
If it is excessive bg anomaly and auto restriction is on, then
restrict the anomaly in receiver and store it in database.
Also in this cl we move the anomaly logic to a JobService, so all
works are done in a background thread and won't interfere the main
thread.
Bug: 72385333
Test: RunSettingsRoboTests &&
Will add auto restriction test once robo framework is updated(b/73172999)
Change-Id: Id0ec5fb449ce26bf19a292bcbe63838d621cfd8e
diff --git a/Android.mk b/Android.mk
index e816dfd..61851cf 100644
--- a/Android.mk
+++ b/Android.mk
@@ -40,6 +40,7 @@
LOCAL_STATIC_JAVA_LIBRARIES := \
android-arch-lifecycle-runtime \
android-arch-lifecycle-extensions \
+ guava \
jsr305 \
settings-logtags \
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 9332ac8..d6a194a 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -3309,6 +3309,9 @@
<service android:name=".fuelgauge.batterytip.AnomalyCleanUpJobService"
android:permission="android.permission.BIND_JOB_SERVICE" />
+ <service android:name=".fuelgauge.batterytip.AnomalyDetectionJobService"
+ android:permission="android.permission.BIND_JOB_SERVICE" />
+
<!-- This is the longest AndroidManifest.xml ever. -->
</application>
</manifest>
diff --git a/res/values/ids.xml b/res/values/ids.xml
index d5c9291..76322ff 100644
--- a/res/values/ids.xml
+++ b/res/values/ids.xml
@@ -19,6 +19,7 @@
<resources>
<item type="id" name="preference_highlighted" />
<item type="id" name="job_anomaly_clean_up" />
+ <item type="id" name="job_anomaly_detection" />
<item type="id" name="lock_none" />
<item type="id" name="lock_pin" />
diff --git a/src/com/android/settings/fuelgauge/batterytip/AnomalyDetectionJobService.java b/src/com/android/settings/fuelgauge/batterytip/AnomalyDetectionJobService.java
new file mode 100644
index 0000000..19fc0d5
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/batterytip/AnomalyDetectionJobService.java
@@ -0,0 +1,169 @@
+/*
+ * 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;
+
+import static android.os.StatsDimensionsValue.INT_VALUE_TYPE;
+import static android.os.StatsDimensionsValue.TUPLE_VALUE_TYPE;
+
+import android.app.AppOpsManager;
+import android.app.StatsManager;
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.app.job.JobWorkItem;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.StatsDimensionsValue;
+import android.os.SystemPropertiesProto;
+import android.provider.Settings;
+import android.support.annotation.VisibleForTesting;
+import android.util.Log;
+
+import com.android.settings.R;
+import com.android.settings.fuelgauge.BatteryUtils;
+import com.android.settingslib.utils.ThreadUtils;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/** A JobService to store anomaly data to anomaly database */
+public class AnomalyDetectionJobService extends JobService {
+ private static final String TAG = "AnomalyDetectionService";
+ private static final int UID_NULL = 0;
+ private static final int STATSD_UID_FILED = 1;
+ private static final int ON = 1;
+
+ @VisibleForTesting
+ static final long MAX_DELAY_MS = TimeUnit.MINUTES.toMillis(30);
+
+ public static void scheduleAnomalyDetection(Context context, Intent intent) {
+ final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
+ final ComponentName component = new ComponentName(context,
+ AnomalyDetectionJobService.class);
+ final JobInfo.Builder jobBuilder =
+ new JobInfo.Builder(R.id.job_anomaly_detection, component)
+ .setOverrideDeadline(MAX_DELAY_MS);
+
+ if (jobScheduler.enqueue(jobBuilder.build(), new JobWorkItem(intent))
+ != JobScheduler.RESULT_SUCCESS) {
+ Log.i(TAG, "Anomaly detection job service enqueue failed.");
+ }
+ }
+
+ @Override
+ public boolean onStartJob(JobParameters params) {
+ ThreadUtils.postOnBackgroundThread(() -> {
+ final BatteryDatabaseManager batteryDatabaseManager =
+ BatteryDatabaseManager.getInstance(this);
+ final BatteryTipPolicy policy = new BatteryTipPolicy(this);
+ final BatteryUtils batteryUtils = BatteryUtils.getInstance(this);
+ final ContentResolver contentResolver = getContentResolver();
+
+ for (JobWorkItem item = params.dequeueWork(); item != null;
+ item = params.dequeueWork()) {
+ saveAnomalyToDatabase(batteryDatabaseManager, batteryUtils, policy, contentResolver,
+ item.getIntent().getExtras());
+ }
+ jobFinished(params, false /* wantsReschedule */);
+ });
+
+ return true;
+ }
+
+ @Override
+ public boolean onStopJob(JobParameters jobParameters) {
+ return false;
+ }
+
+ @VisibleForTesting
+ void saveAnomalyToDatabase(BatteryDatabaseManager databaseManager,
+ BatteryUtils batteryUtils, BatteryTipPolicy policy, ContentResolver contentResolver,
+ Bundle bundle) {
+ // The Example of intentDimsValue is: 35:{1:{1:{1:10013|}|}|}
+ final StatsDimensionsValue intentDimsValue =
+ bundle.getParcelable(StatsManager.EXTRA_STATS_DIMENSIONS_VALUE);
+ final long subscriptionId = bundle.getLong(StatsManager.EXTRA_STATS_SUBSCRIPTION_ID,
+ -1);
+ final long timeMs = bundle.getLong(AnomalyDetectionReceiver.KEY_ANOMALY_TIMESTAMP,
+ System.currentTimeMillis());
+ Log.i(TAG, "Extra stats value: " + intentDimsValue.toString());
+
+ try {
+ final int uid = extractUidFromStatsDimensionsValue(intentDimsValue);
+ final int anomalyType = StatsManagerConfig.getAnomalyTypeFromSubscriptionId(
+ subscriptionId);
+ final boolean smartBatteryOn = Settings.Global.getInt(contentResolver,
+ Settings.Global.APP_STANDBY_ENABLED, ON) == ON;
+ final String packageName = batteryUtils.getPackageName(uid);
+
+ if (anomalyType == StatsManagerConfig.AnomalyType.EXCESSIVE_BG) {
+ // TODO(b/72385333): check battery percentage draining in batterystats
+ if (batteryUtils.isLegacyApp(packageName)) {
+ Log.e(TAG, "Excessive detected uid=" + uid);
+ batteryUtils.setForceAppStandby(uid, packageName,
+ AppOpsManager.MODE_IGNORED);
+ databaseManager.insertAnomaly(packageName, anomalyType,
+ smartBatteryOn
+ ? AnomalyDatabaseHelper.State.AUTO_HANDLED
+ : AnomalyDatabaseHelper.State.NEW,
+ timeMs);
+ }
+ } else {
+ databaseManager.insertAnomaly(packageName, anomalyType,
+ AnomalyDatabaseHelper.State.NEW, timeMs);
+ }
+ } catch (NullPointerException | IndexOutOfBoundsException e) {
+ Log.e(TAG, "Parse stats dimensions value error.", e);
+ }
+ }
+
+ /**
+ * Extract the uid from {@link StatsDimensionsValue}
+ *
+ * The uid dimension has the format: 1:<int> inside the tuple list. Here are some examples:
+ * 1. Excessive bg anomaly: 27:{1:10089|}
+ * 2. Wakeup alarm anomaly: 35:{1:{1:{1:10013|}|}|}
+ * 3. Bluetooth anomaly: 3:{1:{1:{1:10140|}|}|}
+ */
+ @VisibleForTesting
+ final int extractUidFromStatsDimensionsValue(StatsDimensionsValue statsDimensionsValue) {
+ //TODO(b/73172999): Add robo test for this method
+ if (statsDimensionsValue == null) {
+ return UID_NULL;
+ }
+ if (statsDimensionsValue.isValueType(INT_VALUE_TYPE)
+ && statsDimensionsValue.getField() == STATSD_UID_FILED) {
+ // Find out the real uid
+ return statsDimensionsValue.getIntValue();
+ }
+ if (statsDimensionsValue.isValueType(TUPLE_VALUE_TYPE)) {
+ final List<StatsDimensionsValue> values = statsDimensionsValue.getTupleValueList();
+ for (int i = 0, size = values.size(); i < size; i++) {
+ int uid = extractUidFromStatsDimensionsValue(values.get(i));
+ if (uid != UID_NULL) {
+ return uid;
+ }
+ }
+ }
+
+ return UID_NULL;
+ }
+}
diff --git a/src/com/android/settings/fuelgauge/batterytip/AnomalyDetectionReceiver.java b/src/com/android/settings/fuelgauge/batterytip/AnomalyDetectionReceiver.java
index 88f399f..0a24b00 100644
--- a/src/com/android/settings/fuelgauge/batterytip/AnomalyDetectionReceiver.java
+++ b/src/com/android/settings/fuelgauge/batterytip/AnomalyDetectionReceiver.java
@@ -20,59 +20,30 @@
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
-import android.os.StatsDimensionsValue;
-import android.support.annotation.VisibleForTesting;
+import android.os.Bundle;
import android.util.Log;
-import com.android.settings.fuelgauge.BatteryUtils;
-
-import java.util.List;
-
/**
* Receive the anomaly info from {@link StatsManager}
*/
public class AnomalyDetectionReceiver extends BroadcastReceiver {
private static final String TAG = "SettingsAnomalyReceiver";
+ public static final String KEY_ANOMALY_TIMESTAMP = "key_anomaly_timestamp";
+
@Override
public void onReceive(Context context, Intent intent) {
- final BatteryDatabaseManager databaseManager = BatteryDatabaseManager.getInstance(context);
- final BatteryUtils batteryUtils = BatteryUtils.getInstance(context);
final long configUid = intent.getLongExtra(StatsManager.EXTRA_STATS_CONFIG_UID, -1);
final long configKey = intent.getLongExtra(StatsManager.EXTRA_STATS_CONFIG_KEY, -1);
final long subscriptionId = intent.getLongExtra(StatsManager.EXTRA_STATS_SUBSCRIPTION_ID,
-1);
-
Log.i(TAG, "Anomaly intent received. configUid = " + configUid + " configKey = "
+ configKey + " subscriptionId = " + subscriptionId);
- saveAnomalyToDatabase(databaseManager, batteryUtils, intent);
+ final Bundle bundle = intent.getExtras();
+ bundle.putLong(KEY_ANOMALY_TIMESTAMP, System.currentTimeMillis());
+
+ AnomalyDetectionJobService.scheduleAnomalyDetection(context, intent);
AnomalyCleanUpJobService.scheduleCleanUp(context);
}
-
- @VisibleForTesting
- void saveAnomalyToDatabase(BatteryDatabaseManager databaseManager, BatteryUtils batteryUtils
- , Intent intent) {
- // The Example of intentDimsValue is: 35:{1:{1:{1:10013|}|}|}
- StatsDimensionsValue intentDimsValue =
- intent.getParcelableExtra(StatsManager.EXTRA_STATS_DIMENSIONS_VALUE);
- Log.i(TAG, "Extra stats value: " + intentDimsValue.toString());
- List<StatsDimensionsValue> intentTuple = intentDimsValue.getTupleValueList();
-
- if (!intentTuple.isEmpty()) {
- try {
- // TODO(b/72385333): find more robust way to extract the uid.
- final StatsDimensionsValue intentTupleValue = intentTuple.get(0)
- .getTupleValueList().get(0).getTupleValueList().get(0);
- final int uid = intentTupleValue.getIntValue();
- // TODD(b/72385333): extract anomaly type
- final int anomalyType = 0;
- final String packageName = batteryUtils.getPackageName(uid);
- final long timeMs = System.currentTimeMillis();
- databaseManager.insertAnomaly(packageName, anomalyType, timeMs);
- } catch (NullPointerException | IndexOutOfBoundsException e) {
- Log.e(TAG, "Parse stats dimensions value error.", e);
- }
- }
- }
}
diff --git a/src/com/android/settings/fuelgauge/batterytip/BatteryDatabaseManager.java b/src/com/android/settings/fuelgauge/batterytip/BatteryDatabaseManager.java
index 87c2488..935d493 100644
--- a/src/com/android/settings/fuelgauge/batterytip/BatteryDatabaseManager.java
+++ b/src/com/android/settings/fuelgauge/batterytip/BatteryDatabaseManager.java
@@ -60,18 +60,19 @@
/**
* Insert an anomaly log to database.
- *
- * @param packageName the package name of the app
- * @param type the type of the anomaly
- * @param timestampMs the time when it is happened
+ * @param packageName the package name of the app
+ * @param type the type of the anomaly
+ * @param anomalyState the state of the anomaly
+ * @param timestampMs the time when it is happened
*/
- public synchronized void insertAnomaly(String packageName, int type, long timestampMs) {
+ public synchronized void insertAnomaly(String packageName, int type, int anomalyState,
+ long timestampMs) {
try (SQLiteDatabase db = mDatabaseHelper.getWritableDatabase()) {
ContentValues values = new ContentValues();
values.put(PACKAGE_NAME, packageName);
values.put(ANOMALY_TYPE, type);
+ values.put(ANOMALY_STATE, anomalyState);
values.put(TIME_STAMP_MS, timestampMs);
- values.put(ANOMALY_STATE, AnomalyDatabaseHelper.State.NEW);
db.insert(TABLE_ANOMALY, null, values);
}
}
diff --git a/src/com/android/settings/fuelgauge/batterytip/StatsManagerConfig.java b/src/com/android/settings/fuelgauge/batterytip/StatsManagerConfig.java
index 3b5e97d..62eb7ee 100644
--- a/src/com/android/settings/fuelgauge/batterytip/StatsManagerConfig.java
+++ b/src/com/android/settings/fuelgauge/batterytip/StatsManagerConfig.java
@@ -16,6 +16,16 @@
package com.android.settings.fuelgauge.batterytip;
+import android.support.annotation.IntDef;
+
+import com.google.common.hash.HashFunction;
+import com.google.common.hash.Hashing;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.HashMap;
+import java.util.Map;
+
/**
* This class provides all the configs needed if we want to use {@link android.app.StatsManager}
*/
@@ -30,4 +40,42 @@
* The key that represents subscriber, which is settings app.
*/
public static final long SUBSCRIBER_ID = 1;
+
+ private static final Map<Long, Integer> ANOMALY_TYPE;
+
+ private static final HashFunction HASH_FUNCTION = Hashing.sha256();
+
+ static {
+ ANOMALY_TYPE = new HashMap<>();
+ ANOMALY_TYPE.put(hash("SUBSCRIPTION:SETTINGS_EXCESSIVE_BACKGROUND_SERVICE"),
+ AnomalyType.EXCESSIVE_BG);
+ ANOMALY_TYPE.put(hash("SUBSCRIPTION:SETTINGS_LONG_UNOPTIMIZED_BLE_SCAN"),
+ AnomalyType.BLUETOOTH_SCAN);
+ ANOMALY_TYPE.put(hash("SUBSCRIPTION:SETTINGS_EXCESSIVE_WAKEUPS_IN_BACKGROUND"),
+ AnomalyType.WAKEUP_ALARM);
+ ANOMALY_TYPE.put(hash("SUBSCRIPTION:SETTINGS_EXCESSIVE_WAKELOCK_ALL_SCREEN_OFF"),
+ AnomalyType.WAKE_LOCK);
+ }
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({AnomalyType.NULL,
+ AnomalyType.WAKE_LOCK,
+ AnomalyType.WAKEUP_ALARM,
+ AnomalyType.BLUETOOTH_SCAN,
+ AnomalyType.EXCESSIVE_BG})
+ public @interface AnomalyType {
+ int NULL = -1;
+ int WAKE_LOCK = 0;
+ int WAKEUP_ALARM = 1;
+ int BLUETOOTH_SCAN = 2;
+ int EXCESSIVE_BG = 3;
+ }
+
+ public static int getAnomalyTypeFromSubscriptionId(long subscriptionId) {
+ return ANOMALY_TYPE.getOrDefault(subscriptionId, AnomalyType.NULL);
+ }
+
+ private static long hash(CharSequence value) {
+ return HASH_FUNCTION.hashUnencodedChars(value).asLong();
+ }
}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/BatteryDatabaseManagerTest.java b/tests/robotests/src/com/android/settings/fuelgauge/BatteryDatabaseManagerTest.java
index e835e65..498cd58 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/BatteryDatabaseManagerTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/BatteryDatabaseManagerTest.java
@@ -51,6 +51,7 @@
private static long NOW = System.currentTimeMillis();
private static long ONE_DAY_BEFORE = NOW - DateUtils.DAY_IN_MILLIS;
private static long TWO_DAYS_BEFORE = NOW - 2 * DateUtils.DAY_IN_MILLIS;
+
private Context mContext;
private BatteryDatabaseManager mBatteryDatabaseManager;
@@ -69,8 +70,10 @@
@Test
public void testAllFunctions() {
- mBatteryDatabaseManager.insertAnomaly(PACKAGE_NAME_NEW, TYPE_NEW, NOW);
- mBatteryDatabaseManager.insertAnomaly(PACKAGE_NAME_OLD, TYPE_OLD, TWO_DAYS_BEFORE);
+ mBatteryDatabaseManager.insertAnomaly(PACKAGE_NAME_NEW, TYPE_NEW,
+ AnomalyDatabaseHelper.State.NEW, NOW);
+ mBatteryDatabaseManager.insertAnomaly(PACKAGE_NAME_OLD, TYPE_OLD,
+ AnomalyDatabaseHelper.State.NEW, TWO_DAYS_BEFORE);
// In database, it contains two record
List<AppInfo> totalAppInfos = mBatteryDatabaseManager.queryAllAnomalies(0 /* timeMsAfter */,
@@ -96,8 +99,10 @@
@Test
public void testUpdateAnomalies_updateSuccessfully() {
- mBatteryDatabaseManager.insertAnomaly(PACKAGE_NAME_NEW, TYPE_NEW, NOW);
- mBatteryDatabaseManager.insertAnomaly(PACKAGE_NAME_OLD, TYPE_OLD, NOW);
+ mBatteryDatabaseManager.insertAnomaly(PACKAGE_NAME_NEW, TYPE_NEW,
+ AnomalyDatabaseHelper.State.NEW, NOW);
+ mBatteryDatabaseManager.insertAnomaly(PACKAGE_NAME_OLD, TYPE_OLD,
+ AnomalyDatabaseHelper.State.NEW, NOW);
final AppInfo appInfo = new AppInfo.Builder().setPackageName(PACKAGE_NAME_OLD).build();
final List<AppInfo> updateAppInfos = new ArrayList<>();
updateAppInfos.add(appInfo);
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/AnomalyDetectionJobServiceTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/AnomalyDetectionJobServiceTest.java
new file mode 100644
index 0000000..48c99c5
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/AnomalyDetectionJobServiceTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertEquals;
+import static org.robolectric.RuntimeEnvironment.application;
+
+import android.app.job.JobInfo;
+import android.app.job.JobScheduler;
+import android.content.Intent;
+
+import com.android.settings.R;
+import com.android.settings.TestConfig;
+import com.android.settings.testutils.SettingsRobolectricTestRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Shadows;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowJobScheduler;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class AnomalyDetectionJobServiceTest {
+
+ @Test
+ public void testScheduleCleanUp() {
+ AnomalyDetectionJobService.scheduleAnomalyDetection(application,
+ new Intent());
+
+ ShadowJobScheduler shadowJobScheduler = Shadows.shadowOf(
+ application.getSystemService(JobScheduler.class));
+ List<JobInfo> pendingJobs = shadowJobScheduler.getAllPendingJobs();
+ assertThat(pendingJobs).hasSize(1);
+ JobInfo pendingJob = pendingJobs.get(0);
+ assertThat(pendingJob.getId()).isEqualTo(R.id.job_anomaly_detection);
+ assertThat(pendingJob.getMaxExecutionDelayMillis()).isEqualTo(
+ TimeUnit.MINUTES.toMillis(30));
+ }
+}