diff --git a/Android.mk b/Android.mk index e816dfd3d3a..61851cf1b6d 100644 --- a/Android.mk +++ b/Android.mk @@ -40,6 +40,7 @@ LOCAL_JAVA_LIBRARIES := \ 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 9332ac86d3e..d6a194ac142 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -3309,6 +3309,9 @@ + + diff --git a/res/values/ids.xml b/res/values/ids.xml index d5c9291f2fb..76322ffaed6 100644 --- a/res/values/ids.xml +++ b/res/values/ids.xml @@ -19,6 +19,7 @@ + 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 00000000000..19fc0d58559 --- /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: 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 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 88f399ffcd5..0a24b001c40 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.app.StatsManager; 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 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 87c248820da..935d4932575 100644 --- a/src/com/android/settings/fuelgauge/batterytip/BatteryDatabaseManager.java +++ b/src/com/android/settings/fuelgauge/batterytip/BatteryDatabaseManager.java @@ -60,18 +60,19 @@ public class BatteryDatabaseManager { /** * 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 3b5e97dd0f9..62eb7eebbbb 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 @@ public class StatsManagerConfig { * The key that represents subscriber, which is settings app. */ public static final long SUBSCRIBER_ID = 1; + + private static final Map 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 e835e65a9b1..498cd587138 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 @@ public class BatteryDatabaseManagerTest { 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 @@ public class BatteryDatabaseManagerTest { @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 totalAppInfos = mBatteryDatabaseManager.queryAllAnomalies(0 /* timeMsAfter */, @@ -96,8 +99,10 @@ public class BatteryDatabaseManagerTest { @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 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 00000000000..48c99c5f2eb --- /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 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)); + } +}