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.content.ComponentName; 30 import android.content.ContentResolver; 31 import android.content.Context; 32 import android.content.Intent; 33 import android.os.Bundle; 34 import android.os.StatsDimensionsValue; 35 import android.os.UserManager; 36 import android.provider.Settings; 37 import android.support.annotation.GuardedBy; 38 import android.support.annotation.VisibleForTesting; 39 import android.util.Log; 40 import android.util.Pair; 41 42 import com.android.internal.logging.nano.MetricsProto; 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 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 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 120 public boolean onStopJob(JobParameters jobParameters) { 121 synchronized (mLock) { 122 mIsJobCanceled = true; 123 } 124 return true; // Need to reschedule 125 } 126 127 @VisibleForTesting 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 154 if (batteryUtils.shouldHideAnomaly(powerWhitelistBackend, uid, anomalyInfo)) { 155 metricsFeatureProvider.action(context, 156 MetricsProto.MetricsEvent.ACTION_ANOMALY_IGNORED, 157 packageName, 158 Pair.create(MetricsProto.MetricsEvent.FIELD_CONTEXT, 159 anomalyInfo.anomalyType), 160 Pair.create(MetricsProto.MetricsEvent.FIELD_APP_VERSION_CODE, 161 versionCode)); 162 } else { 163 if (autoFeatureOn && anomalyInfo.autoRestriction) { 164 // Auto restrict this app 165 batteryUtils.setForceAppStandby(uid, packageName, 166 AppOpsManager.MODE_IGNORED); 167 databaseManager.insertAnomaly(uid, packageName, anomalyInfo.anomalyType, 168 AnomalyDatabaseHelper.State.AUTO_HANDLED, 169 timeMs); 170 } else { 171 databaseManager.insertAnomaly(uid, packageName, anomalyInfo.anomalyType, 172 AnomalyDatabaseHelper.State.NEW, 173 timeMs); 174 } 175 metricsFeatureProvider.action(context, 176 MetricsProto.MetricsEvent.ACTION_ANOMALY_TRIGGERED, 177 packageName, 178 Pair.create(MetricsProto.MetricsEvent.FIELD_ANOMALY_TYPE, 179 anomalyInfo.anomalyType), 180 Pair.create(MetricsProto.MetricsEvent.FIELD_APP_VERSION_CODE, 181 versionCode)); 182 } 183 184 } catch (NullPointerException | IndexOutOfBoundsException e) { 185 Log.e(TAG, "Parse stats dimensions value error.", e); 186 } 187 } 188 189 /** 190 * Extract the uid from {@link StatsDimensionsValue} 191 * 192 * The uid dimension has the format: 1:<int> inside the tuple list. Here are some examples: 193 * 1. Excessive bg anomaly: 27:{1:10089|} 194 * 2. Wakeup alarm anomaly: 35:{1:{1:{1:10013|}|}|} 195 * 3. Bluetooth anomaly: 3:{1:{1:{1:10140|}|}|} 196 */ 197 @VisibleForTesting 198 int extractUidFromStatsDimensionsValue(StatsDimensionsValue statsDimensionsValue) { 199 if (statsDimensionsValue == null) { 200 return UID_NULL; 201 } 202 if (statsDimensionsValue.isValueType(INT_VALUE_TYPE) 203 && statsDimensionsValue.getField() == STATSD_UID_FILED) { 204 // Find out the real uid 205 return statsDimensionsValue.getIntValue(); 206 } 207 if (statsDimensionsValue.isValueType(TUPLE_VALUE_TYPE)) { 208 final List<StatsDimensionsValue> values = statsDimensionsValue.getTupleValueList(); 209 for (int i = 0, size = values.size(); i < size; i++) { 210 int uid = extractUidFromStatsDimensionsValue(values.get(i)); 211 if (uid != UID_NULL) { 212 return uid; 213 } 214 } 215 } 216 217 return UID_NULL; 218 } 219 220 @VisibleForTesting 221 JobWorkItem dequeueWork(JobParameters parameters) { 222 synchronized (mLock) { 223 if (mIsJobCanceled) { 224 return null; 225 } 226 227 return parameters.dequeueWork(); 228 } 229 } 230 231 @VisibleForTesting 232 void completeWork(JobParameters parameters, JobWorkItem item) { 233 synchronized (mLock) { 234 if (mIsJobCanceled) { 235 return; 236 } 237 238 parameters.completeWork(item); 239 } 240 } 241 } 242