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