| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License |
| */ |
| |
| package com.android.server.job.controllers; |
| |
| import android.app.job.JobInfo; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.os.PowerManager; |
| import android.os.UserHandle; |
| import android.util.ArraySet; |
| import android.util.Slog; |
| import android.util.SparseBooleanArray; |
| import android.util.proto.ProtoOutputStream; |
| |
| import com.android.internal.util.ArrayUtils; |
| import com.android.server.DeviceIdleController; |
| import com.android.server.LocalServices; |
| import com.android.server.job.JobSchedulerService; |
| import com.android.server.job.JobStore; |
| import com.android.server.job.StateControllerProto; |
| import com.android.server.job.StateControllerProto.DeviceIdleJobsController.TrackedJob; |
| |
| import java.io.PrintWriter; |
| import java.util.Arrays; |
| |
| /** |
| * When device is dozing, set constraint for all jobs, except whitelisted apps, as not satisfied. |
| * When device is not dozing, set constraint for all jobs as satisfied. |
| */ |
| public final class DeviceIdleJobsController extends StateController { |
| |
| private static final String LOG_TAG = "DeviceIdleJobsController"; |
| private static final boolean LOG_DEBUG = false; |
| private static final long BACKGROUND_JOBS_DELAY = 3000; |
| |
| static final int PROCESS_BACKGROUND_JOBS = 1; |
| |
| // Singleton factory |
| private static Object sCreationLock = new Object(); |
| private static DeviceIdleJobsController sController; |
| |
| /** |
| * These are jobs added with a special flag to indicate that they should be exempted from doze |
| * when the app is temp whitelisted or in the foreground. |
| */ |
| private final ArraySet<JobStatus> mAllowInIdleJobs; |
| private final SparseBooleanArray mForegroundUids; |
| private final DeviceIdleUpdateFunctor mDeviceIdleUpdateFunctor; |
| private final DeviceIdleJobsDelayHandler mHandler; |
| private final JobSchedulerService mJobSchedulerService; |
| private final PowerManager mPowerManager; |
| private final DeviceIdleController.LocalService mLocalDeviceIdleController; |
| |
| /** |
| * True when in device idle mode, so we don't want to schedule any jobs. |
| */ |
| private boolean mDeviceIdleMode; |
| private int[] mDeviceIdleWhitelistAppIds; |
| private int[] mPowerSaveTempWhitelistAppIds; |
| |
| /** |
| * Returns a singleton for the DeviceIdleJobsController |
| */ |
| public static DeviceIdleJobsController get(JobSchedulerService service) { |
| synchronized (sCreationLock) { |
| if (sController == null) { |
| sController = new DeviceIdleJobsController(service, service.getContext(), |
| service.getLock()); |
| } |
| return sController; |
| } |
| } |
| |
| // onReceive |
| private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| switch (intent.getAction()) { |
| case PowerManager.ACTION_LIGHT_DEVICE_IDLE_MODE_CHANGED: |
| case PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED: |
| updateIdleMode(mPowerManager != null && (mPowerManager.isDeviceIdleMode() |
| || mPowerManager.isLightDeviceIdleMode())); |
| break; |
| case PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED: |
| synchronized (mLock) { |
| mDeviceIdleWhitelistAppIds = |
| mLocalDeviceIdleController.getPowerSaveWhitelistUserAppIds(); |
| if (LOG_DEBUG) { |
| Slog.d(LOG_TAG, "Got whitelist " |
| + Arrays.toString(mDeviceIdleWhitelistAppIds)); |
| } |
| } |
| break; |
| case PowerManager.ACTION_POWER_SAVE_TEMP_WHITELIST_CHANGED: |
| synchronized (mLock) { |
| mPowerSaveTempWhitelistAppIds = |
| mLocalDeviceIdleController.getPowerSaveTempWhitelistAppIds(); |
| if (LOG_DEBUG) { |
| Slog.d(LOG_TAG, "Got temp whitelist " |
| + Arrays.toString(mPowerSaveTempWhitelistAppIds)); |
| } |
| boolean changed = false; |
| for (int i = 0; i < mAllowInIdleJobs.size(); i++) { |
| changed |= updateTaskStateLocked(mAllowInIdleJobs.valueAt(i)); |
| } |
| if (changed) { |
| mStateChangedListener.onControllerStateChanged(); |
| } |
| } |
| break; |
| } |
| } |
| }; |
| |
| private DeviceIdleJobsController(JobSchedulerService jobSchedulerService, Context context, |
| Object lock) { |
| super(jobSchedulerService, context, lock); |
| |
| mJobSchedulerService = jobSchedulerService; |
| mHandler = new DeviceIdleJobsDelayHandler(context.getMainLooper()); |
| // Register for device idle mode changes |
| mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); |
| mLocalDeviceIdleController = |
| LocalServices.getService(DeviceIdleController.LocalService.class); |
| mDeviceIdleWhitelistAppIds = mLocalDeviceIdleController.getPowerSaveWhitelistUserAppIds(); |
| mPowerSaveTempWhitelistAppIds = |
| mLocalDeviceIdleController.getPowerSaveTempWhitelistAppIds(); |
| mDeviceIdleUpdateFunctor = new DeviceIdleUpdateFunctor(); |
| mAllowInIdleJobs = new ArraySet<>(); |
| mForegroundUids = new SparseBooleanArray(); |
| final IntentFilter filter = new IntentFilter(); |
| filter.addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED); |
| filter.addAction(PowerManager.ACTION_LIGHT_DEVICE_IDLE_MODE_CHANGED); |
| filter.addAction(PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED); |
| filter.addAction(PowerManager.ACTION_POWER_SAVE_TEMP_WHITELIST_CHANGED); |
| mContext.registerReceiverAsUser( |
| mBroadcastReceiver, UserHandle.ALL, filter, null, null); |
| } |
| |
| void updateIdleMode(boolean enabled) { |
| boolean changed = false; |
| synchronized (mLock) { |
| if (mDeviceIdleMode != enabled) { |
| changed = true; |
| } |
| mDeviceIdleMode = enabled; |
| if (LOG_DEBUG) Slog.d(LOG_TAG, "mDeviceIdleMode=" + mDeviceIdleMode); |
| if (enabled) { |
| mHandler.removeMessages(PROCESS_BACKGROUND_JOBS); |
| mJobSchedulerService.getJobStore().forEachJob(mDeviceIdleUpdateFunctor); |
| } else { |
| // When coming out of doze, process all foreground uids immediately, while others |
| // will be processed after a delay of 3 seconds. |
| for (int i = 0; i < mForegroundUids.size(); i++) { |
| if (mForegroundUids.valueAt(i)) { |
| mJobSchedulerService.getJobStore().forEachJobForSourceUid( |
| mForegroundUids.keyAt(i), mDeviceIdleUpdateFunctor); |
| } |
| } |
| mHandler.sendEmptyMessageDelayed(PROCESS_BACKGROUND_JOBS, BACKGROUND_JOBS_DELAY); |
| } |
| } |
| // Inform the job scheduler service about idle mode changes |
| if (changed) { |
| mStateChangedListener.onDeviceIdleStateChanged(enabled); |
| } |
| } |
| |
| /** |
| * Called by jobscheduler service to report uid state changes between active and idle |
| */ |
| public void setUidActiveLocked(int uid, boolean active) { |
| final boolean changed = (active != mForegroundUids.get(uid)); |
| if (!changed) { |
| return; |
| } |
| if (LOG_DEBUG) { |
| Slog.d(LOG_TAG, "uid " + uid + " going " + (active ? "active" : "inactive")); |
| } |
| mForegroundUids.put(uid, active); |
| mDeviceIdleUpdateFunctor.mChanged = false; |
| mJobSchedulerService.getJobStore().forEachJobForSourceUid(uid, mDeviceIdleUpdateFunctor); |
| if (mDeviceIdleUpdateFunctor.mChanged) { |
| mStateChangedListener.onControllerStateChanged(); |
| } |
| } |
| |
| /** |
| * Checks if the given job's scheduling app id exists in the device idle user whitelist. |
| */ |
| boolean isWhitelistedLocked(JobStatus job) { |
| return Arrays.binarySearch(mDeviceIdleWhitelistAppIds, |
| UserHandle.getAppId(job.getSourceUid())) >= 0; |
| } |
| |
| /** |
| * Checks if the given job's scheduling app id exists in the device idle temp whitelist. |
| */ |
| boolean isTempWhitelistedLocked(JobStatus job) { |
| return ArrayUtils.contains(mPowerSaveTempWhitelistAppIds, |
| UserHandle.getAppId(job.getSourceUid())); |
| } |
| |
| private boolean updateTaskStateLocked(JobStatus task) { |
| final boolean allowInIdle = ((task.getFlags()&JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0) |
| && (mForegroundUids.get(task.getSourceUid()) || isTempWhitelistedLocked(task)); |
| final boolean whitelisted = isWhitelistedLocked(task); |
| final boolean enableTask = !mDeviceIdleMode || whitelisted || allowInIdle; |
| return task.setDeviceNotDozingConstraintSatisfied(enableTask, whitelisted); |
| } |
| |
| @Override |
| public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) { |
| if ((jobStatus.getFlags()&JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0) { |
| mAllowInIdleJobs.add(jobStatus); |
| } |
| updateTaskStateLocked(jobStatus); |
| } |
| |
| @Override |
| public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, |
| boolean forUpdate) { |
| if ((jobStatus.getFlags()&JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0) { |
| mAllowInIdleJobs.remove(jobStatus); |
| } |
| } |
| |
| @Override |
| public void dumpControllerStateLocked(final PrintWriter pw, final int filterUid) { |
| pw.println("DeviceIdleJobsController"); |
| pw.println("mDeviceIdleMode=" + mDeviceIdleMode); |
| mJobSchedulerService.getJobStore().forEachJob(new JobStore.JobStatusFunctor() { |
| @Override public void process(JobStatus jobStatus) { |
| if (!jobStatus.shouldDump(filterUid)) { |
| return; |
| } |
| pw.print(" #"); |
| jobStatus.printUniqueId(pw); |
| pw.print(" from "); |
| UserHandle.formatUid(pw, jobStatus.getSourceUid()); |
| pw.print(": "); |
| pw.print(jobStatus.getSourcePackageName()); |
| pw.print((jobStatus.satisfiedConstraints |
| & JobStatus.CONSTRAINT_DEVICE_NOT_DOZING) != 0 |
| ? " RUNNABLE" : " WAITING"); |
| if (jobStatus.dozeWhitelisted) { |
| pw.print(" WHITELISTED"); |
| } |
| if (mAllowInIdleJobs.contains(jobStatus)) { |
| pw.print(" ALLOWED_IN_DOZE"); |
| } |
| pw.println(); |
| } |
| }); |
| } |
| |
| @Override |
| public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, int filterUid) { |
| final long token = proto.start(fieldId); |
| final long mToken = proto.start(StateControllerProto.DEVICE_IDLE); |
| |
| proto.write(StateControllerProto.DeviceIdleJobsController.IS_DEVICE_IDLE_MODE, |
| mDeviceIdleMode); |
| mJobSchedulerService.getJobStore().forEachJob(new JobStore.JobStatusFunctor() { |
| @Override public void process(JobStatus jobStatus) { |
| if (!jobStatus.shouldDump(filterUid)) { |
| return; |
| } |
| final long jsToken = |
| proto.start(StateControllerProto.DeviceIdleJobsController.TRACKED_JOBS); |
| |
| jobStatus.writeToShortProto(proto, TrackedJob.INFO); |
| proto.write(TrackedJob.SOURCE_UID, jobStatus.getSourceUid()); |
| proto.write(TrackedJob.SOURCE_PACKAGE_NAME, jobStatus.getSourcePackageName()); |
| proto.write(TrackedJob.ARE_CONSTRAINTS_SATISFIED, |
| (jobStatus.satisfiedConstraints & |
| JobStatus.CONSTRAINT_DEVICE_NOT_DOZING) != 0); |
| proto.write(TrackedJob.IS_DOZE_WHITELISTED, jobStatus.dozeWhitelisted); |
| proto.write(TrackedJob.IS_ALLOWED_IN_DOZE, mAllowInIdleJobs.contains(jobStatus)); |
| |
| proto.end(jsToken); |
| } |
| }); |
| |
| proto.end(mToken); |
| proto.end(token); |
| } |
| |
| final class DeviceIdleUpdateFunctor implements JobStore.JobStatusFunctor { |
| boolean mChanged; |
| |
| @Override |
| public void process(JobStatus jobStatus) { |
| mChanged |= updateTaskStateLocked(jobStatus); |
| } |
| } |
| |
| final class DeviceIdleJobsDelayHandler extends Handler { |
| public DeviceIdleJobsDelayHandler(Looper looper) { |
| super(looper); |
| } |
| |
| @Override |
| public void handleMessage(Message msg) { |
| switch (msg.what) { |
| case PROCESS_BACKGROUND_JOBS: |
| // Just process all the jobs, the ones in foreground should already be running. |
| synchronized (mLock) { |
| mDeviceIdleUpdateFunctor.mChanged = false; |
| mJobSchedulerService.getJobStore().forEachJob(mDeviceIdleUpdateFunctor); |
| if (mDeviceIdleUpdateFunctor.mChanged) { |
| mStateChangedListener.onControllerStateChanged(); |
| } |
| } |
| break; |
| } |
| } |
| } |
| } |