1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.fuelgauge.batterytip;
18 
19 import static android.os.StatsDimensionsValue.INT_VALUE_TYPE;
20 import static android.os.StatsDimensionsValue.TUPLE_VALUE_TYPE;
21 
22 import android.app.AppOpsManager;
23 import android.app.StatsManager;
24 import android.app.job.JobInfo;
25 import android.app.job.JobParameters;
26 import android.app.job.JobScheduler;
27 import android.app.job.JobService;
28 import android.app.job.JobWorkItem;
29 import android.app.settings.SettingsEnums;
30 import android.content.ComponentName;
31 import android.content.ContentResolver;
32 import android.content.Context;
33 import android.content.Intent;
34 import android.os.Bundle;
35 import android.os.StatsDimensionsValue;
36 import android.os.UserManager;
37 import android.provider.Settings;
38 import android.util.Log;
39 
40 import androidx.annotation.GuardedBy;
41 import androidx.annotation.VisibleForTesting;
42 
43 import com.android.internal.util.ArrayUtils;
44 import com.android.settings.R;
45 import com.android.settings.fuelgauge.BatteryUtils;
46 import com.android.settings.fuelgauge.PowerUsageFeatureProvider;
47 import com.android.settings.overlay.FeatureFactory;
48 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
49 import com.android.settingslib.fuelgauge.PowerWhitelistBackend;
50 import com.android.settingslib.utils.ThreadUtils;
51 
52 import java.util.ArrayList;
53 import java.util.List;
54 import java.util.concurrent.TimeUnit;
55 
56 /** A JobService to store anomaly data to anomaly database */
57 public class AnomalyDetectionJobService extends JobService {
58     private static final String TAG = "AnomalyDetectionService";
59     private static final int ON = 1;
60     @VisibleForTesting
61     static final int UID_NULL = -1;
62     @VisibleForTesting
63     static final int STATSD_UID_FILED = 1;
64     @VisibleForTesting
65     static final long MAX_DELAY_MS = TimeUnit.MINUTES.toMillis(30);
66 
67     private final Object mLock = new Object();
68     @GuardedBy("mLock")
69     @VisibleForTesting
70     boolean mIsJobCanceled = false;
71 
scheduleAnomalyDetection(Context context, Intent intent)72     public static void scheduleAnomalyDetection(Context context, Intent intent) {
73         final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
74         final ComponentName component = new ComponentName(context,
75                 AnomalyDetectionJobService.class);
76         final JobInfo.Builder jobBuilder =
77                 new JobInfo.Builder(R.integer.job_anomaly_detection, component)
78                         .setOverrideDeadline(MAX_DELAY_MS);
79 
80         if (jobScheduler.enqueue(jobBuilder.build(), new JobWorkItem(intent))
81                 != JobScheduler.RESULT_SUCCESS) {
82             Log.i(TAG, "Anomaly detection job service enqueue failed.");
83         }
84     }
85 
86     @Override
onStartJob(JobParameters params)87     public boolean onStartJob(JobParameters params) {
88         synchronized (mLock) {
89             mIsJobCanceled = false;
90         }
91         ThreadUtils.postOnBackgroundThread(() -> {
92             final Context context = AnomalyDetectionJobService.this;
93             final BatteryDatabaseManager batteryDatabaseManager =
94                     BatteryDatabaseManager.getInstance(this);
95             final BatteryTipPolicy policy = new BatteryTipPolicy(this);
96             final BatteryUtils batteryUtils = BatteryUtils.getInstance(this);
97             final ContentResolver contentResolver = getContentResolver();
98             final UserManager userManager = getSystemService(UserManager.class);
99             final PowerWhitelistBackend powerWhitelistBackend =
100                     PowerWhitelistBackend.getInstance(context);
101             final PowerUsageFeatureProvider powerUsageFeatureProvider = FeatureFactory
102                     .getFactory(this).getPowerUsageFeatureProvider(this);
103             final MetricsFeatureProvider metricsFeatureProvider = FeatureFactory
104                     .getFactory(this).getMetricsFeatureProvider();
105 
106             for (JobWorkItem item = dequeueWork(params); item != null; item = dequeueWork(params)) {
107                 saveAnomalyToDatabase(context, userManager,
108                         batteryDatabaseManager, batteryUtils, policy, powerWhitelistBackend,
109                         contentResolver, powerUsageFeatureProvider, metricsFeatureProvider,
110                         item.getIntent().getExtras());
111 
112                 completeWork(params, item);
113             }
114         });
115 
116         return true;
117     }
118 
119     @Override
onStopJob(JobParameters jobParameters)120     public boolean onStopJob(JobParameters jobParameters) {
121         synchronized (mLock) {
122             mIsJobCanceled = true;
123         }
124         return true; // Need to reschedule
125     }
126 
127     @VisibleForTesting
saveAnomalyToDatabase(Context context, UserManager userManager, BatteryDatabaseManager databaseManager, BatteryUtils batteryUtils, BatteryTipPolicy policy, PowerWhitelistBackend powerWhitelistBackend, ContentResolver contentResolver, PowerUsageFeatureProvider powerUsageFeatureProvider, MetricsFeatureProvider metricsFeatureProvider, Bundle bundle)128     void saveAnomalyToDatabase(Context context, UserManager userManager,
129             BatteryDatabaseManager databaseManager, BatteryUtils batteryUtils,
130             BatteryTipPolicy policy, PowerWhitelistBackend powerWhitelistBackend,
131             ContentResolver contentResolver, PowerUsageFeatureProvider powerUsageFeatureProvider,
132             MetricsFeatureProvider metricsFeatureProvider, Bundle bundle) {
133         // The Example of intentDimsValue is: 35:{1:{1:{1:10013|}|}|}
134         final StatsDimensionsValue intentDimsValue =
135                 bundle.getParcelable(StatsManager.EXTRA_STATS_DIMENSIONS_VALUE);
136         final long timeMs = bundle.getLong(AnomalyDetectionReceiver.KEY_ANOMALY_TIMESTAMP,
137                 System.currentTimeMillis());
138         final ArrayList<String> cookies = bundle.getStringArrayList(
139                 StatsManager.EXTRA_STATS_BROADCAST_SUBSCRIBER_COOKIES);
140         final AnomalyInfo anomalyInfo = new AnomalyInfo(
141                 !ArrayUtils.isEmpty(cookies) ? cookies.get(0) : "");
142         Log.i(TAG, "Extra stats value: " + intentDimsValue.toString());
143 
144         try {
145             final int uid = extractUidFromStatsDimensionsValue(intentDimsValue);
146             final boolean autoFeatureOn = powerUsageFeatureProvider.isSmartBatterySupported()
147                     ? Settings.Global.getInt(contentResolver,
148                     Settings.Global.ADAPTIVE_BATTERY_MANAGEMENT_ENABLED, ON) == ON
149                     : Settings.Global.getInt(contentResolver,
150                             Settings.Global.APP_AUTO_RESTRICTION_ENABLED, ON) == ON;
151             final String packageName = batteryUtils.getPackageName(uid);
152             final long versionCode = batteryUtils.getAppLongVersionCode(packageName);
153             final String versionedPackage = packageName + "/" + versionCode;
154             if (batteryUtils.shouldHideAnomaly(powerWhitelistBackend, uid, anomalyInfo)) {
155                 metricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN,
156                         SettingsEnums.ACTION_ANOMALY_IGNORED,
157                         SettingsEnums.PAGE_UNKNOWN,
158                         versionedPackage,
159                         anomalyInfo.anomalyType);
160             } else {
161                 if (autoFeatureOn && anomalyInfo.autoRestriction) {
162                     // Auto restrict this app
163                     batteryUtils.setForceAppStandby(uid, packageName,
164                             AppOpsManager.MODE_IGNORED);
165                     databaseManager.insertAnomaly(uid, packageName, anomalyInfo.anomalyType,
166                             AnomalyDatabaseHelper.State.AUTO_HANDLED,
167                             timeMs);
168                 } else {
169                     databaseManager.insertAnomaly(uid, packageName, anomalyInfo.anomalyType,
170                             AnomalyDatabaseHelper.State.NEW,
171                             timeMs);
172                 }
173                 metricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN,
174                         SettingsEnums.ACTION_ANOMALY_TRIGGERED,
175                         SettingsEnums.PAGE_UNKNOWN,
176                         versionedPackage,
177                         anomalyInfo.anomalyType);
178             }
179 
180         } catch (NullPointerException | IndexOutOfBoundsException e) {
181             Log.e(TAG, "Parse stats dimensions value error.", e);
182         }
183     }
184 
185     /**
186      * Extract the uid from {@link StatsDimensionsValue}
187      *
188      * The uid dimension has the format: 1:<int> inside the tuple list. Here are some examples:
189      * 1. Excessive bg anomaly: 27:{1:10089|}
190      * 2. Wakeup alarm anomaly: 35:{1:{1:{1:10013|}|}|}
191      * 3. Bluetooth anomaly:    3:{1:{1:{1:10140|}|}|}
192      */
193     @VisibleForTesting
extractUidFromStatsDimensionsValue(StatsDimensionsValue statsDimensionsValue)194     int extractUidFromStatsDimensionsValue(StatsDimensionsValue statsDimensionsValue) {
195         if (statsDimensionsValue == null) {
196             return UID_NULL;
197         }
198         if (statsDimensionsValue.isValueType(INT_VALUE_TYPE)
199                 && statsDimensionsValue.getField() == STATSD_UID_FILED) {
200             // Find out the real uid
201             return statsDimensionsValue.getIntValue();
202         }
203         if (statsDimensionsValue.isValueType(TUPLE_VALUE_TYPE)) {
204             final List<StatsDimensionsValue> values = statsDimensionsValue.getTupleValueList();
205             for (int i = 0, size = values.size(); i < size; i++) {
206                 int uid = extractUidFromStatsDimensionsValue(values.get(i));
207                 if (uid != UID_NULL) {
208                     return uid;
209                 }
210             }
211         }
212 
213         return UID_NULL;
214     }
215 
216     @VisibleForTesting
dequeueWork(JobParameters parameters)217     JobWorkItem dequeueWork(JobParameters parameters) {
218         synchronized (mLock) {
219             if (mIsJobCanceled) {
220                 return null;
221             }
222 
223             return parameters.dequeueWork();
224         }
225     }
226 
227     @VisibleForTesting
completeWork(JobParameters parameters, JobWorkItem item)228     void completeWork(JobParameters parameters, JobWorkItem item) {
229         synchronized (mLock) {
230             if (mIsJobCanceled) {
231                 return;
232             }
233 
234             parameters.completeWork(item);
235         }
236     }
237 }
238