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
17package com.android.server.job.controllers;
18
19import android.app.job.JobInfo;
20import android.content.BroadcastReceiver;
21import android.content.Context;
22import android.content.Intent;
23import android.content.IntentFilter;
24import android.os.Handler;
25import android.os.Looper;
26import android.os.Message;
27import android.os.PowerManager;
28import android.os.UserHandle;
29import android.util.ArraySet;
30import android.util.Log;
31import android.util.Slog;
32import android.util.SparseBooleanArray;
33import android.util.proto.ProtoOutputStream;
34
35import com.android.internal.util.ArrayUtils;
36import com.android.internal.util.IndentingPrintWriter;
37import com.android.server.DeviceIdleController;
38import com.android.server.LocalServices;
39import com.android.server.job.JobSchedulerService;
40import com.android.server.job.StateControllerProto;
41import com.android.server.job.StateControllerProto.DeviceIdleJobsController.TrackedJob;
42
43import java.util.Arrays;
44import java.util.function.Consumer;
45import 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 */
51public 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
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
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     */
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     */
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     */
201    boolean isTempWhitelistedLocked(JobStatus job) {
202        return ArrayUtils.contains(mPowerSaveTempWhitelistAppIds,
203                UserHandle.getAppId(job.getSourceUid()));
204    }
205
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
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
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
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
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
288        public void accept(JobStatus jobStatus) {
289            mChanged |= updateTaskStateLocked(jobStatus);
290        }
291    }
292
293    final class DeviceIdleJobsDelayHandler extends Handler {
294        public DeviceIdleJobsDelayHandler(Looper looper) {
295            super(looper);
296        }
297
298        @Override
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