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