• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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