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