1 /* 2 * Copyright (C) 2014 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.server.job.controllers; 18 19 import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; 20 21 import android.annotation.NonNull; 22 import android.app.job.JobInfo; 23 import android.content.BroadcastReceiver; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.IntentFilter; 27 import android.os.BatteryManager; 28 import android.os.BatteryManagerInternal; 29 import android.os.UserHandle; 30 import android.util.ArraySet; 31 import android.util.IndentingPrintWriter; 32 import android.util.Log; 33 import android.util.Slog; 34 import android.util.proto.ProtoOutputStream; 35 36 import com.android.internal.annotations.GuardedBy; 37 import com.android.internal.annotations.VisibleForTesting; 38 import com.android.server.JobSchedulerBackgroundThread; 39 import com.android.server.LocalServices; 40 import com.android.server.job.JobSchedulerService; 41 import com.android.server.job.StateControllerProto; 42 43 import java.util.function.Predicate; 44 45 /** 46 * Simple controller that tracks whether the phone is charging or not. The phone is considered to 47 * be charging when it's been plugged in for more than two minutes, and the system has broadcast 48 * ACTION_BATTERY_OK. 49 */ 50 public final class BatteryController extends RestrictingController { 51 private static final String TAG = "JobScheduler.Battery"; 52 private static final boolean DEBUG = JobSchedulerService.DEBUG 53 || Log.isLoggable(TAG, Log.DEBUG); 54 55 @GuardedBy("mLock") 56 private final ArraySet<JobStatus> mTrackedTasks = new ArraySet<>(); 57 /** 58 * List of jobs that started while the UID was in the TOP state. 59 */ 60 @GuardedBy("mLock") 61 private final ArraySet<JobStatus> mTopStartedJobs = new ArraySet<>(); 62 63 private final PowerTracker mPowerTracker; 64 65 /** 66 * Helper set to avoid too much GC churn from frequent calls to 67 * {@link #maybeReportNewChargingStateLocked()}. 68 */ 69 private final ArraySet<JobStatus> mChangedJobs = new ArraySet<>(); 70 71 @GuardedBy("mLock") 72 private Boolean mLastReportedStatsdBatteryNotLow = null; 73 @GuardedBy("mLock") 74 private Boolean mLastReportedStatsdStablePower = null; 75 BatteryController(JobSchedulerService service)76 public BatteryController(JobSchedulerService service) { 77 super(service); 78 mPowerTracker = new PowerTracker(); 79 mPowerTracker.startTracking(); 80 } 81 82 @Override maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob)83 public void maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob) { 84 if (taskStatus.hasPowerConstraint()) { 85 final long nowElapsed = sElapsedRealtimeClock.millis(); 86 mTrackedTasks.add(taskStatus); 87 taskStatus.setTrackingController(JobStatus.TRACKING_BATTERY); 88 if (taskStatus.hasChargingConstraint()) { 89 if (hasTopExemptionLocked(taskStatus)) { 90 taskStatus.setChargingConstraintSatisfied(nowElapsed, 91 mPowerTracker.isPowerConnected()); 92 } else { 93 taskStatus.setChargingConstraintSatisfied(nowElapsed, 94 mService.isBatteryCharging() && mService.isBatteryNotLow()); 95 } 96 } 97 taskStatus.setBatteryNotLowConstraintSatisfied(nowElapsed, mService.isBatteryNotLow()); 98 } 99 } 100 101 @Override startTrackingRestrictedJobLocked(JobStatus jobStatus)102 public void startTrackingRestrictedJobLocked(JobStatus jobStatus) { 103 maybeStartTrackingJobLocked(jobStatus, null); 104 } 105 106 @Override 107 @GuardedBy("mLock") prepareForExecutionLocked(JobStatus jobStatus)108 public void prepareForExecutionLocked(JobStatus jobStatus) { 109 if (!jobStatus.hasPowerConstraint()) { 110 // Ignore all jobs the controller wouldn't be tracking. 111 return; 112 } 113 if (DEBUG) { 114 Slog.d(TAG, "Prepping for " + jobStatus.toShortString()); 115 } 116 117 final int uid = jobStatus.getSourceUid(); 118 if (mService.getUidBias(uid) == JobInfo.BIAS_TOP_APP) { 119 if (DEBUG) { 120 Slog.d(TAG, jobStatus.toShortString() + " is top started job"); 121 } 122 mTopStartedJobs.add(jobStatus); 123 } 124 } 125 126 @Override 127 @GuardedBy("mLock") unprepareFromExecutionLocked(JobStatus jobStatus)128 public void unprepareFromExecutionLocked(JobStatus jobStatus) { 129 mTopStartedJobs.remove(jobStatus); 130 } 131 132 @Override maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, boolean forUpdate)133 public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, boolean forUpdate) { 134 if (taskStatus.clearTrackingController(JobStatus.TRACKING_BATTERY)) { 135 mTrackedTasks.remove(taskStatus); 136 mTopStartedJobs.remove(taskStatus); 137 } 138 } 139 140 @Override stopTrackingRestrictedJobLocked(JobStatus jobStatus)141 public void stopTrackingRestrictedJobLocked(JobStatus jobStatus) { 142 if (!jobStatus.hasPowerConstraint()) { 143 maybeStopTrackingJobLocked(jobStatus, null, false); 144 } 145 } 146 147 @Override 148 @GuardedBy("mLock") onBatteryStateChangedLocked()149 public void onBatteryStateChangedLocked() { 150 // Update job bookkeeping out of band. 151 JobSchedulerBackgroundThread.getHandler().post(() -> { 152 synchronized (mLock) { 153 maybeReportNewChargingStateLocked(); 154 } 155 }); 156 } 157 158 @Override 159 @GuardedBy("mLock") onUidBiasChangedLocked(int uid, int prevBias, int newBias)160 public void onUidBiasChangedLocked(int uid, int prevBias, int newBias) { 161 if (prevBias == JobInfo.BIAS_TOP_APP || newBias == JobInfo.BIAS_TOP_APP) { 162 maybeReportNewChargingStateLocked(); 163 } 164 } 165 166 @GuardedBy("mLock") hasTopExemptionLocked(@onNull JobStatus taskStatus)167 private boolean hasTopExemptionLocked(@NonNull JobStatus taskStatus) { 168 return mService.getUidBias(taskStatus.getSourceUid()) == JobInfo.BIAS_TOP_APP 169 || mTopStartedJobs.contains(taskStatus); 170 } 171 172 @GuardedBy("mLock") maybeReportNewChargingStateLocked()173 private void maybeReportNewChargingStateLocked() { 174 final boolean powerConnected = mPowerTracker.isPowerConnected(); 175 final boolean stablePower = mService.isBatteryCharging() && mService.isBatteryNotLow(); 176 final boolean batteryNotLow = mService.isBatteryNotLow(); 177 if (DEBUG) { 178 Slog.d(TAG, "maybeReportNewChargingStateLocked: " 179 + powerConnected + "/" + stablePower + "/" + batteryNotLow); 180 } 181 182 if (mLastReportedStatsdStablePower == null 183 || mLastReportedStatsdStablePower != stablePower) { 184 logDeviceWideConstraintStateToStatsd(JobStatus.CONSTRAINT_CHARGING, stablePower); 185 mLastReportedStatsdStablePower = stablePower; 186 } 187 if (mLastReportedStatsdBatteryNotLow == null 188 || mLastReportedStatsdBatteryNotLow != stablePower) { 189 logDeviceWideConstraintStateToStatsd(JobStatus.CONSTRAINT_BATTERY_NOT_LOW, 190 batteryNotLow); 191 mLastReportedStatsdBatteryNotLow = batteryNotLow; 192 } 193 194 final long nowElapsed = sElapsedRealtimeClock.millis(); 195 for (int i = mTrackedTasks.size() - 1; i >= 0; i--) { 196 final JobStatus ts = mTrackedTasks.valueAt(i); 197 if (ts.hasChargingConstraint()) { 198 if (hasTopExemptionLocked(ts) 199 && ts.getEffectivePriority() >= JobInfo.PRIORITY_DEFAULT) { 200 // If the job started while the app was on top or the app is currently on top, 201 // let the job run as long as there's power connected, even if the device isn't 202 // officially charging. 203 // For user requested/initiated jobs, users may be confused when the task stops 204 // running even though the device is plugged in. 205 // Low priority jobs don't need to be exempted. 206 if (ts.setChargingConstraintSatisfied(nowElapsed, powerConnected)) { 207 mChangedJobs.add(ts); 208 } 209 } else if (ts.setChargingConstraintSatisfied(nowElapsed, stablePower)) { 210 mChangedJobs.add(ts); 211 } 212 } 213 if (ts.hasBatteryNotLowConstraint() 214 && ts.setBatteryNotLowConstraintSatisfied(nowElapsed, batteryNotLow)) { 215 mChangedJobs.add(ts); 216 } 217 } 218 if (stablePower || batteryNotLow) { 219 // If one of our conditions has been satisfied, always schedule any newly ready jobs. 220 mStateChangedListener.onRunJobNow(null); 221 } else if (mChangedJobs.size() > 0) { 222 // Otherwise, just let the job scheduler know the state has changed and take care of it 223 // as it thinks is best. 224 mStateChangedListener.onControllerStateChanged(mChangedJobs); 225 } 226 mChangedJobs.clear(); 227 } 228 229 private final class PowerTracker extends BroadcastReceiver { 230 /** 231 * Track whether there is power connected. It doesn't mean the device is charging. 232 * Use {@link JobSchedulerService#isBatteryCharging()} to determine if the device is 233 * charging. 234 */ 235 private boolean mPowerConnected; 236 PowerTracker()237 PowerTracker() { 238 } 239 startTracking()240 void startTracking() { 241 IntentFilter filter = new IntentFilter(); 242 243 filter.addAction(Intent.ACTION_POWER_CONNECTED); 244 filter.addAction(Intent.ACTION_POWER_DISCONNECTED); 245 mContext.registerReceiver(this, filter); 246 247 // Initialize tracker state. 248 BatteryManagerInternal batteryManagerInternal = 249 LocalServices.getService(BatteryManagerInternal.class); 250 mPowerConnected = batteryManagerInternal.isPowered(BatteryManager.BATTERY_PLUGGED_ANY); 251 } 252 isPowerConnected()253 boolean isPowerConnected() { 254 return mPowerConnected; 255 } 256 257 @Override onReceive(Context context, Intent intent)258 public void onReceive(Context context, Intent intent) { 259 synchronized (mLock) { 260 final String action = intent.getAction(); 261 262 if (Intent.ACTION_POWER_CONNECTED.equals(action)) { 263 if (DEBUG) { 264 Slog.d(TAG, "Power connected @ " + sElapsedRealtimeClock.millis()); 265 } 266 if (mPowerConnected) { 267 return; 268 } 269 mPowerConnected = true; 270 } else if (Intent.ACTION_POWER_DISCONNECTED.equals(action)) { 271 if (DEBUG) { 272 Slog.d(TAG, "Power disconnected @ " + sElapsedRealtimeClock.millis()); 273 } 274 if (!mPowerConnected) { 275 return; 276 } 277 mPowerConnected = false; 278 } 279 280 maybeReportNewChargingStateLocked(); 281 } 282 } 283 } 284 285 @VisibleForTesting getTrackedJobs()286 ArraySet<JobStatus> getTrackedJobs() { 287 return mTrackedTasks; 288 } 289 290 @VisibleForTesting getTopStartedJobs()291 ArraySet<JobStatus> getTopStartedJobs() { 292 return mTopStartedJobs; 293 } 294 295 @Override dumpControllerStateLocked(IndentingPrintWriter pw, Predicate<JobStatus> predicate)296 public void dumpControllerStateLocked(IndentingPrintWriter pw, 297 Predicate<JobStatus> predicate) { 298 pw.println("Power connected: " + mPowerTracker.isPowerConnected()); 299 pw.println("Stable power: " + (mService.isBatteryCharging() && mService.isBatteryNotLow())); 300 pw.println("Not low: " + mService.isBatteryNotLow()); 301 302 for (int i = 0; i < mTrackedTasks.size(); i++) { 303 final JobStatus js = mTrackedTasks.valueAt(i); 304 if (!predicate.test(js)) { 305 continue; 306 } 307 pw.print("#"); 308 js.printUniqueId(pw); 309 pw.print(" from "); 310 UserHandle.formatUid(pw, js.getSourceUid()); 311 pw.println(); 312 } 313 } 314 315 @Override dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, Predicate<JobStatus> predicate)316 public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, 317 Predicate<JobStatus> predicate) { 318 final long token = proto.start(fieldId); 319 final long mToken = proto.start(StateControllerProto.BATTERY); 320 321 proto.write(StateControllerProto.BatteryController.IS_ON_STABLE_POWER, 322 mService.isBatteryCharging() && mService.isBatteryNotLow()); 323 proto.write(StateControllerProto.BatteryController.IS_BATTERY_NOT_LOW, 324 mService.isBatteryNotLow()); 325 326 for (int i = 0; i < mTrackedTasks.size(); i++) { 327 final JobStatus js = mTrackedTasks.valueAt(i); 328 if (!predicate.test(js)) { 329 continue; 330 } 331 final long jsToken = proto.start(StateControllerProto.BatteryController.TRACKED_JOBS); 332 js.writeToShortProto(proto, StateControllerProto.BatteryController.TrackedJob.INFO); 333 proto.write(StateControllerProto.BatteryController.TrackedJob.SOURCE_UID, 334 js.getSourceUid()); 335 proto.end(jsToken); 336 } 337 338 proto.end(mToken); 339 proto.end(token); 340 } 341 } 342