1 /*
2  * Copyright (C) 2016 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.app.job.JobInfo;
22 import android.content.BroadcastReceiver;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.IntentFilter;
26 import android.os.Handler;
27 import android.os.Looper;
28 import android.os.Message;
29 import android.os.PowerManager;
30 import android.os.UserHandle;
31 import android.util.ArraySet;
32 import android.util.IndentingPrintWriter;
33 import android.util.Log;
34 import android.util.Slog;
35 import android.util.SparseBooleanArray;
36 import android.util.proto.ProtoOutputStream;
37 
38 import com.android.internal.util.ArrayUtils;
39 import com.android.server.AppSchedulingModuleThread;
40 import com.android.server.DeviceIdleInternal;
41 import com.android.server.LocalServices;
42 import com.android.server.job.JobSchedulerService;
43 import com.android.server.job.StateControllerProto;
44 import com.android.server.job.StateControllerProto.DeviceIdleJobsController.TrackedJob;
45 
46 import java.util.Arrays;
47 import java.util.function.Consumer;
48 import java.util.function.Predicate;
49 
50 /**
51  * When device is dozing, set constraint for all jobs, except whitelisted apps, as not satisfied.
52  * When device is not dozing, set constraint for all jobs as satisfied.
53  */
54 public final class DeviceIdleJobsController extends StateController {
55     private static final String TAG = "JobScheduler.DeviceIdle";
56     private static final boolean DEBUG = JobSchedulerService.DEBUG
57             || Log.isLoggable(TAG, Log.DEBUG);
58 
59     private static final long BACKGROUND_JOBS_DELAY = 3000;
60 
61     static final int PROCESS_BACKGROUND_JOBS = 1;
62 
63     /**
64      * These are jobs added with a special flag to indicate that they should be exempted from doze
65      * when the app is temp whitelisted or in the foreground.
66      */
67     private final ArraySet<JobStatus> mAllowInIdleJobs;
68     private final SparseBooleanArray mForegroundUids = new SparseBooleanArray();
69     private final DeviceIdleUpdateFunctor mDeviceIdleUpdateFunctor;
70     private final DeviceIdleJobsDelayHandler mHandler;
71     private final PowerManager mPowerManager;
72     private final DeviceIdleInternal mLocalDeviceIdleController;
73 
74     /**
75      * True when in device idle mode, so we don't want to schedule any jobs.
76      */
77     private boolean mDeviceIdleMode;
78     private int[] mDeviceIdleWhitelistAppIds;
79     private int[] mPowerSaveTempWhitelistAppIds;
80 
81     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
82         @Override
83         public void onReceive(Context context, Intent intent) {
84             switch (intent.getAction()) {
85                 case PowerManager.ACTION_LIGHT_DEVICE_IDLE_MODE_CHANGED:
86                 case PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED:
87                     updateIdleMode(mPowerManager != null && (mPowerManager.isDeviceIdleMode()
88                             || mPowerManager.isLightDeviceIdleMode()));
89                     break;
90                 case PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED:
91                     synchronized (mLock) {
92                         mDeviceIdleWhitelistAppIds =
93                                 mLocalDeviceIdleController.getPowerSaveWhitelistUserAppIds();
94                         if (DEBUG) {
95                             Slog.d(TAG, "Got whitelist "
96                                     + Arrays.toString(mDeviceIdleWhitelistAppIds));
97                         }
98                     }
99                     break;
100                 case PowerManager.ACTION_POWER_SAVE_TEMP_WHITELIST_CHANGED:
101                     synchronized (mLock) {
102                         mPowerSaveTempWhitelistAppIds =
103                                 mLocalDeviceIdleController.getPowerSaveTempWhitelistAppIds();
104                         if (DEBUG) {
105                             Slog.d(TAG, "Got temp whitelist "
106                                     + Arrays.toString(mPowerSaveTempWhitelistAppIds));
107                         }
108                         final ArraySet<JobStatus> changedJobs = new ArraySet<>();
109                         final long nowElapsed = sElapsedRealtimeClock.millis();
110                         for (int i = 0; i < mAllowInIdleJobs.size(); i++) {
111                             if (updateTaskStateLocked(mAllowInIdleJobs.valueAt(i), nowElapsed)) {
112                                 changedJobs.add(mAllowInIdleJobs.valueAt(i));
113                             }
114                         }
115                         if (changedJobs.size() > 0) {
116                             mStateChangedListener.onControllerStateChanged(changedJobs);
117                         }
118                     }
119                     break;
120             }
121         }
122     };
123 
124     /** Criteria for whether or not we should a job's rush evaluation when the device exits Doze. */
125     private final Predicate<JobStatus> mShouldRushEvaluation = (jobStatus) ->
126             jobStatus.isRequestedExpeditedJob() || mForegroundUids.get(jobStatus.getSourceUid());
127 
DeviceIdleJobsController(JobSchedulerService service)128     public DeviceIdleJobsController(JobSchedulerService service) {
129         super(service);
130 
131         mHandler = new DeviceIdleJobsDelayHandler(AppSchedulingModuleThread.get().getLooper());
132         // Register for device idle mode changes
133         mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
134         mLocalDeviceIdleController =
135                 LocalServices.getService(DeviceIdleInternal.class);
136         mDeviceIdleWhitelistAppIds = mLocalDeviceIdleController.getPowerSaveWhitelistUserAppIds();
137         mPowerSaveTempWhitelistAppIds =
138                 mLocalDeviceIdleController.getPowerSaveTempWhitelistAppIds();
139         mDeviceIdleUpdateFunctor = new DeviceIdleUpdateFunctor();
140         mAllowInIdleJobs = new ArraySet<>();
141         final IntentFilter filter = new IntentFilter();
142         filter.addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED);
143         filter.addAction(PowerManager.ACTION_LIGHT_DEVICE_IDLE_MODE_CHANGED);
144         filter.addAction(PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED);
145         filter.addAction(PowerManager.ACTION_POWER_SAVE_TEMP_WHITELIST_CHANGED);
146         mContext.registerReceiverAsUser(
147                 mBroadcastReceiver, UserHandle.ALL, filter, null, null);
148     }
149 
updateIdleMode(boolean enabled)150     void updateIdleMode(boolean enabled) {
151         boolean changed = false;
152         synchronized (mLock) {
153             if (mDeviceIdleMode != enabled) {
154                 changed = true;
155             }
156             mDeviceIdleMode = enabled;
157             logDeviceWideConstraintStateToStatsd(JobStatus.CONSTRAINT_DEVICE_NOT_DOZING,
158                     !mDeviceIdleMode);
159             if (DEBUG) Slog.d(TAG, "mDeviceIdleMode=" + mDeviceIdleMode);
160             mDeviceIdleUpdateFunctor.prepare();
161             if (enabled) {
162                 mHandler.removeMessages(PROCESS_BACKGROUND_JOBS);
163                 mService.getJobStore().forEachJob(mDeviceIdleUpdateFunctor);
164             } else {
165                 // When coming out of doze, process all foreground uids and EJs immediately,
166                 // while others will be processed after a delay of 3 seconds.
167                 mService.getJobStore().forEachJob(mShouldRushEvaluation, mDeviceIdleUpdateFunctor);
168                 mHandler.sendEmptyMessageDelayed(PROCESS_BACKGROUND_JOBS, BACKGROUND_JOBS_DELAY);
169             }
170         }
171         // Inform the job scheduler service about idle mode changes
172         if (changed) {
173             mStateChangedListener.onDeviceIdleStateChanged(enabled);
174         }
175     }
176 
177     /**
178      *  Called by jobscheduler service to report uid state changes between active and idle
179      */
setUidActiveLocked(int uid, boolean active)180     public void setUidActiveLocked(int uid, boolean active) {
181         final boolean changed = (active != mForegroundUids.get(uid));
182         if (!changed) {
183             return;
184         }
185         if (DEBUG) {
186             Slog.d(TAG, "uid " + uid + " going " + (active ? "active" : "inactive"));
187         }
188         mForegroundUids.put(uid, active);
189         mDeviceIdleUpdateFunctor.prepare();
190         mService.getJobStore().forEachJobForSourceUid(uid, mDeviceIdleUpdateFunctor);
191         if (mDeviceIdleUpdateFunctor.mChangedJobs.size() > 0) {
192             mStateChangedListener.onControllerStateChanged(mDeviceIdleUpdateFunctor.mChangedJobs);
193         }
194     }
195 
196     /**
197      * Checks if the given job's scheduling app id exists in the device idle user whitelist.
198      */
isWhitelistedLocked(JobStatus job)199     boolean isWhitelistedLocked(JobStatus job) {
200         return Arrays.binarySearch(mDeviceIdleWhitelistAppIds,
201                 UserHandle.getAppId(job.getSourceUid())) >= 0;
202     }
203 
204     /**
205      * Checks if the given job's scheduling app id exists in the device idle temp whitelist.
206      */
isTempWhitelistedLocked(JobStatus job)207     boolean isTempWhitelistedLocked(JobStatus job) {
208         return ArrayUtils.contains(mPowerSaveTempWhitelistAppIds,
209                 UserHandle.getAppId(job.getSourceUid()));
210     }
211 
updateTaskStateLocked(JobStatus task, final long nowElapsed)212     private boolean updateTaskStateLocked(JobStatus task, final long nowElapsed) {
213         final boolean allowInIdle = ((task.getFlags()&JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0)
214                 && (mForegroundUids.get(task.getSourceUid()) || isTempWhitelistedLocked(task));
215         final boolean whitelisted = isWhitelistedLocked(task);
216         final boolean enableTask = !mDeviceIdleMode || whitelisted || allowInIdle;
217         return task.setDeviceNotDozingConstraintSatisfied(nowElapsed, enableTask, whitelisted);
218     }
219 
220     @Override
maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob)221     public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) {
222         if ((jobStatus.getFlags()&JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0) {
223             mAllowInIdleJobs.add(jobStatus);
224         }
225         updateTaskStateLocked(jobStatus, sElapsedRealtimeClock.millis());
226     }
227 
228     @Override
maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob)229     public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob) {
230         if ((jobStatus.getFlags()&JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0) {
231             mAllowInIdleJobs.remove(jobStatus);
232         }
233     }
234 
235     @Override
dumpControllerStateLocked(final IndentingPrintWriter pw, final Predicate<JobStatus> predicate)236     public void dumpControllerStateLocked(final IndentingPrintWriter pw,
237             final Predicate<JobStatus> predicate) {
238         pw.println("Idle mode: " + mDeviceIdleMode);
239         pw.println();
240 
241         mService.getJobStore().forEachJob(predicate, (jobStatus) -> {
242             pw.print("#");
243             jobStatus.printUniqueId(pw);
244             pw.print(" from ");
245             UserHandle.formatUid(pw, jobStatus.getSourceUid());
246             pw.print(": ");
247             pw.print(jobStatus.getSourcePackageName());
248             pw.print((jobStatus.satisfiedConstraints
249                     & JobStatus.CONSTRAINT_DEVICE_NOT_DOZING) != 0
250                             ? " RUNNABLE" : " WAITING");
251             if (jobStatus.appHasDozeExemption) {
252                 pw.print(" WHITELISTED");
253             }
254             if (mAllowInIdleJobs.contains(jobStatus)) {
255                 pw.print(" ALLOWED_IN_DOZE");
256             }
257             pw.println();
258         });
259     }
260 
261     @Override
dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, Predicate<JobStatus> predicate)262     public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId,
263             Predicate<JobStatus> predicate) {
264         final long token = proto.start(fieldId);
265         final long mToken = proto.start(StateControllerProto.DEVICE_IDLE);
266 
267         proto.write(StateControllerProto.DeviceIdleJobsController.IS_DEVICE_IDLE_MODE,
268                 mDeviceIdleMode);
269         mService.getJobStore().forEachJob(predicate, (jobStatus) -> {
270             final long jsToken =
271                     proto.start(StateControllerProto.DeviceIdleJobsController.TRACKED_JOBS);
272 
273             jobStatus.writeToShortProto(proto, TrackedJob.INFO);
274             proto.write(TrackedJob.SOURCE_UID, jobStatus.getSourceUid());
275             proto.write(TrackedJob.SOURCE_PACKAGE_NAME, jobStatus.getSourcePackageName());
276             proto.write(TrackedJob.ARE_CONSTRAINTS_SATISFIED,
277                     (jobStatus.satisfiedConstraints & JobStatus.CONSTRAINT_DEVICE_NOT_DOZING) != 0);
278             proto.write(TrackedJob.IS_DOZE_WHITELISTED, jobStatus.appHasDozeExemption);
279             proto.write(TrackedJob.IS_ALLOWED_IN_DOZE, mAllowInIdleJobs.contains(jobStatus));
280 
281             proto.end(jsToken);
282         });
283 
284         proto.end(mToken);
285         proto.end(token);
286     }
287 
288     final class DeviceIdleUpdateFunctor implements Consumer<JobStatus> {
289         final ArraySet<JobStatus> mChangedJobs = new ArraySet<>();
290         long mUpdateTimeElapsed = 0;
291 
prepare()292         void prepare() {
293             mChangedJobs.clear();
294             mUpdateTimeElapsed = sElapsedRealtimeClock.millis();
295         }
296 
297         @Override
accept(JobStatus jobStatus)298         public void accept(JobStatus jobStatus) {
299             if (updateTaskStateLocked(jobStatus, mUpdateTimeElapsed)) {
300                 mChangedJobs.add(jobStatus);
301             }
302         }
303     }
304 
305     final class DeviceIdleJobsDelayHandler extends Handler {
DeviceIdleJobsDelayHandler(Looper looper)306         public DeviceIdleJobsDelayHandler(Looper looper) {
307             super(looper);
308         }
309 
310         @Override
handleMessage(Message msg)311         public void handleMessage(Message msg) {
312             switch (msg.what) {
313                 case PROCESS_BACKGROUND_JOBS:
314                     // Just process all the jobs, the ones in foreground should already be running.
315                     synchronized (mLock) {
316                         mDeviceIdleUpdateFunctor.prepare();
317                         mService.getJobStore().forEachJob(mDeviceIdleUpdateFunctor);
318                         if (mDeviceIdleUpdateFunctor.mChangedJobs.size() > 0) {
319                             mStateChangedListener.onControllerStateChanged(
320                                     mDeviceIdleUpdateFunctor.mChangedJobs);
321                         }
322                     }
323                     break;
324             }
325         }
326     }
327 }
328