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