diff options
Diffstat (limited to 'apex')
62 files changed, 24844 insertions, 0 deletions
diff --git a/apex/jobscheduler/OWNERS b/apex/jobscheduler/OWNERS new file mode 100644 index 000000000000..d004eed2a0db --- /dev/null +++ b/apex/jobscheduler/OWNERS @@ -0,0 +1,6 @@ +yamasani@google.com +omakoto@google.com +ctate@android.com +ctate@google.com +kwekua@google.com +suprabh@google.com
\ No newline at end of file diff --git a/apex/jobscheduler/README_js-mainline.md b/apex/jobscheduler/README_js-mainline.md new file mode 100644 index 000000000000..c1ad666e3e05 --- /dev/null +++ b/apex/jobscheduler/README_js-mainline.md @@ -0,0 +1,38 @@ +# Making Job Scheduler into a Mainline Module + +## TODOs + +See also: +- http://go/moving-js-code-for-mainline +- http://go/jobscheduler-code-dependencies-2019-07 + +- [ ] Move this into `frameworks/apex/jobscheduler/...`. Currently it's in `frameworks/base/apex/...` +because `frameworks/apex/` is not a part of any git projects. (and also working on multiple +projects is a pain.) + +## Current structure + +- JS service side classes are put in `jobscheduler-service.jar`. +It's *not* included in services.jar, and instead it's put in the system server classpath, +which currently looks like the following: +`SYSTEMSERVERCLASSPATH=/system/framework/services.jar:/system/framework/jobscheduler-service.jar:/system/framework/ethernet-service.jar:/system/framework/wifi-service.jar:/system/framework/com.android.location.provider.jar` + + (Note `jobscheduler-service.jar` will be put at the end in http://ag/9128109) + + `SYSTEMSERVERCLASSPATH` is generated from `PRODUCT_SYSTEM_SERVER_JARS`. + +- JS framework side classes are put in `jobscheduler-framework.jar`, +and the rest of the framework code is put in `framework-minus-apex.jar`, +as of http://ag/9145619. + + However these jar files are *not* put on the device. We still generate + `framework.jar` merging the two jar files, and this jar file is what's + put on the device and loaded by Zygote. + + +This is *not* the final design. From a gerrit comment on http://ag/9145619: + +> This CL is just the first step, and the current state isn't not really the final form. For now we just want to have two separate jars, which makes it easier for us to analyze dependencies between them, and I wanted to minimize the change to the rest of the system. So, for example, zygote will still only have "framework.jar" in its classpath, instead of the two jars for now. +> But yes, eventually, we won't even be able to have the monolithic "framework.jar" file because of mainline, so we need to figure out how to build the system without creating it. At that point zygote will have the two separate jar files in its classpath. +> When we reach that point, we should revisit the naming of it, and yes, maybe the simple "framework.jar" is a good option. +> But again, for now, I want to make this change as transparent as possible to the rest of the world. diff --git a/apex/jobscheduler/framework/Android.bp b/apex/jobscheduler/framework/Android.bp new file mode 100644 index 000000000000..621ff9a92bc5 --- /dev/null +++ b/apex/jobscheduler/framework/Android.bp @@ -0,0 +1,30 @@ +filegroup { + name: "jobscheduler-framework-source", + srcs: [ + "java/**/*.java", + "java/android/app/job/IJobCallback.aidl", + "java/android/app/job/IJobScheduler.aidl", + "java/android/app/job/IJobService.aidl", + "java/android/os/IDeviceIdleController.aidl", + ], + path: "java", +} + +java_library { + name: "jobscheduler-framework", + installable: true, + sdk_version: "core_platform", + + srcs: [ + ":jobscheduler-framework-source", + ], + + aidl: { + export_include_dirs: [ + "java", + ], + }, + libs: [ + "framework-minus-apex", + ], +} diff --git a/apex/jobscheduler/framework/java/android/app/DeviceIdleFrameworkInitializer.java b/apex/jobscheduler/framework/java/android/app/DeviceIdleFrameworkInitializer.java new file mode 100644 index 000000000000..a807eb1d3311 --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/DeviceIdleFrameworkInitializer.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2019 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 android.app; + +import android.content.Context; +import android.os.DeviceIdleManager; +import android.os.IDeviceIdleController; +import android.os.PowerManager; +import android.os.RemoteException; +import android.os.ServiceManager; + +/** + * This class needs to be pre-loaded by zygote. This is where the device idle manager wrapper + * is registered. + * + * @hide + */ +public class DeviceIdleFrameworkInitializer { + private static IDeviceIdleController sIDeviceIdleController; + + static { + SystemServiceRegistry.registerCachedService( + Context.DEVICE_IDLE_CONTROLLER, DeviceIdleManager.class, + (context, b) -> new DeviceIdleManager( + context.getOuterContext(), IDeviceIdleController.Stub.asInterface(b))); + PowerManager.setIsIgnoringBatteryOptimizationsCallback((packageName) -> { + // No need for synchronization on sIDeviceIdleController; worst case + // we just initialize it twice. + if (sIDeviceIdleController == null) { + sIDeviceIdleController = IDeviceIdleController.Stub.asInterface( + ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER)); + } + try { + return sIDeviceIdleController.isPowerSaveWhitelistApp(packageName); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + }); + } +} diff --git a/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java b/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java new file mode 100644 index 000000000000..f59e7a4ae6ec --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2014 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 android.app; + +import android.app.job.IJobScheduler; +import android.app.job.JobInfo; +import android.app.job.JobScheduler; +import android.app.job.JobSnapshot; +import android.app.job.JobWorkItem; +import android.os.RemoteException; + +import java.util.List; + + +/** + * Concrete implementation of the JobScheduler interface + * + * Note android.app.job is the better package to put this class, but we can't move it there + * because that'd break robolectric. Grr. + * + * @hide + */ +public class JobSchedulerImpl extends JobScheduler { + IJobScheduler mBinder; + + public JobSchedulerImpl(IJobScheduler binder) { + mBinder = binder; + } + + @Override + public int schedule(JobInfo job) { + try { + return mBinder.schedule(job); + } catch (RemoteException e) { + return JobScheduler.RESULT_FAILURE; + } + } + + @Override + public int enqueue(JobInfo job, JobWorkItem work) { + try { + return mBinder.enqueue(job, work); + } catch (RemoteException e) { + return JobScheduler.RESULT_FAILURE; + } + } + + @Override + public int scheduleAsPackage(JobInfo job, String packageName, int userId, String tag) { + try { + return mBinder.scheduleAsPackage(job, packageName, userId, tag); + } catch (RemoteException e) { + return JobScheduler.RESULT_FAILURE; + } + } + + @Override + public void cancel(int jobId) { + try { + mBinder.cancel(jobId); + } catch (RemoteException e) {} + + } + + @Override + public void cancelAll() { + try { + mBinder.cancelAll(); + } catch (RemoteException e) {} + + } + + @Override + public List<JobInfo> getAllPendingJobs() { + try { + return mBinder.getAllPendingJobs().getList(); + } catch (RemoteException e) { + return null; + } + } + + @Override + public JobInfo getPendingJob(int jobId) { + try { + return mBinder.getPendingJob(jobId); + } catch (RemoteException e) { + return null; + } + } + + @Override + public List<JobInfo> getStartedJobs() { + try { + return mBinder.getStartedJobs(); + } catch (RemoteException e) { + return null; + } + } + + @Override + public List<JobSnapshot> getAllJobSnapshots() { + try { + return mBinder.getAllJobSnapshots().getList(); + } catch (RemoteException e) { + return null; + } + } +} diff --git a/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl b/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl new file mode 100644 index 000000000000..d281da037fde --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl @@ -0,0 +1,68 @@ +/** + * Copyright 2014, 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 android.app.job; + +import android.app.job.JobWorkItem; + +/** + * The server side of the JobScheduler IPC protocols. The app-side implementation + * invokes on this interface to indicate completion of the (asynchronous) instructions + * issued by the server. + * + * In all cases, the 'who' parameter is the caller's service binder, used to track + * which Job Service instance is reporting. + * + * {@hide} + */ +interface IJobCallback { + /** + * Immediate callback to the system after sending a start signal, used to quickly detect ANR. + * + * @param jobId Unique integer used to identify this job. + * @param ongoing True to indicate that the client is processing the job. False if the job is + * complete + */ + @UnsupportedAppUsage + void acknowledgeStartMessage(int jobId, boolean ongoing); + /** + * Immediate callback to the system after sending a stop signal, used to quickly detect ANR. + * + * @param jobId Unique integer used to identify this job. + * @param reschedule Whether or not to reschedule this job. + */ + @UnsupportedAppUsage + void acknowledgeStopMessage(int jobId, boolean reschedule); + /* + * Called to deqeue next work item for the job. + */ + @UnsupportedAppUsage + JobWorkItem dequeueWork(int jobId); + /* + * Called to report that job has completed processing a work item. + */ + @UnsupportedAppUsage + boolean completeWork(int jobId, int workId); + /* + * Tell the job manager that the client is done with its execution, so that it can go on to + * the next one and stop attributing wakelock time to us etc. + * + * @param jobId Unique integer used to identify this job. + * @param reschedule Whether or not to reschedule this job. + */ + @UnsupportedAppUsage + void jobFinished(int jobId, boolean reschedule); +} diff --git a/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl b/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl new file mode 100644 index 000000000000..3006f50e54fc --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl @@ -0,0 +1,38 @@ +/** + * Copyright (C) 2014 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 android.app.job; + +import android.app.job.JobInfo; +import android.app.job.JobSnapshot; +import android.app.job.JobWorkItem; +import android.content.pm.ParceledListSlice; + + /** + * IPC interface that supports the app-facing {@link #JobScheduler} api. + * {@hide} + */ +interface IJobScheduler { + int schedule(in JobInfo job); + int enqueue(in JobInfo job, in JobWorkItem work); + int scheduleAsPackage(in JobInfo job, String packageName, int userId, String tag); + void cancel(int jobId); + void cancelAll(); + ParceledListSlice getAllPendingJobs(); + JobInfo getPendingJob(int jobId); + List<JobInfo> getStartedJobs(); + ParceledListSlice getAllJobSnapshots(); +} diff --git a/apex/jobscheduler/framework/java/android/app/job/IJobService.aidl b/apex/jobscheduler/framework/java/android/app/job/IJobService.aidl new file mode 100644 index 000000000000..22ad252b9639 --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/IJobService.aidl @@ -0,0 +1,34 @@ +/** + * Copyright 2014, 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 android.app.job; + +import android.app.job.JobParameters; + +/** + * Interface that the framework uses to communicate with application code that implements a + * JobService. End user code does not implement this interface directly; instead, the app's + * service implementation will extend android.app.job.JobService. + * {@hide} + */ +oneway interface IJobService { + /** Begin execution of application's job. */ + @UnsupportedAppUsage + void startJob(in JobParameters jobParams); + /** Stop execution of application's job. */ + @UnsupportedAppUsage + void stopJob(in JobParameters jobParams); +} diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.aidl b/apex/jobscheduler/framework/java/android/app/job/JobInfo.aidl new file mode 100644 index 000000000000..7b198a8ab14d --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (C) 2014 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 android.app.job; + +parcelable JobInfo; diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java new file mode 100644 index 000000000000..8b3b3a28f2bc --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java @@ -0,0 +1,1597 @@ +/* + * Copyright (C) 2014 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 android.app.job; + +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN; +import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.util.TimeUtils.formatDuration; + +import android.annotation.BytesLong; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.annotation.UnsupportedAppUsage; +import android.content.ClipData; +import android.content.ComponentName; +import android.net.NetworkRequest; +import android.net.NetworkSpecifier; +import android.net.Uri; +import android.os.BaseBundle; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.PersistableBundle; +import android.util.Log; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Objects; + +/** + * Container of data passed to the {@link android.app.job.JobScheduler} fully encapsulating the + * parameters required to schedule work against the calling application. These are constructed + * using the {@link JobInfo.Builder}. + * You must specify at least one sort of constraint on the JobInfo object that you are creating. + * The goal here is to provide the scheduler with high-level semantics about the work you want to + * accomplish. Doing otherwise with throw an exception in your app. + */ +public class JobInfo implements Parcelable { + private static String TAG = "JobInfo"; + + /** @hide */ + @IntDef(prefix = { "NETWORK_TYPE_" }, value = { + NETWORK_TYPE_NONE, + NETWORK_TYPE_ANY, + NETWORK_TYPE_UNMETERED, + NETWORK_TYPE_NOT_ROAMING, + NETWORK_TYPE_CELLULAR, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface NetworkType {} + + /** Default. */ + public static final int NETWORK_TYPE_NONE = 0; + /** This job requires network connectivity. */ + public static final int NETWORK_TYPE_ANY = 1; + /** This job requires network connectivity that is unmetered. */ + public static final int NETWORK_TYPE_UNMETERED = 2; + /** This job requires network connectivity that is not roaming. */ + public static final int NETWORK_TYPE_NOT_ROAMING = 3; + /** This job requires network connectivity that is a cellular network. */ + public static final int NETWORK_TYPE_CELLULAR = 4; + + /** + * This job requires metered connectivity such as most cellular data + * networks. + * + * @deprecated Cellular networks may be unmetered, or Wi-Fi networks may be + * metered, so this isn't a good way of selecting a specific + * transport. Instead, use {@link #NETWORK_TYPE_CELLULAR} or + * {@link android.net.NetworkRequest.Builder#addTransportType(int)} + * if your job requires a specific network transport. + */ + @Deprecated + public static final int NETWORK_TYPE_METERED = NETWORK_TYPE_CELLULAR; + + /** Sentinel value indicating that bytes are unknown. */ + public static final int NETWORK_BYTES_UNKNOWN = -1; + + /** + * Amount of backoff a job has initially by default, in milliseconds. + */ + public static final long DEFAULT_INITIAL_BACKOFF_MILLIS = 30000L; // 30 seconds. + + /** + * Maximum backoff we allow for a job, in milliseconds. + */ + public static final long MAX_BACKOFF_DELAY_MILLIS = 5 * 60 * 60 * 1000; // 5 hours. + + /** @hide */ + @IntDef(prefix = { "BACKOFF_POLICY_" }, value = { + BACKOFF_POLICY_LINEAR, + BACKOFF_POLICY_EXPONENTIAL, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface BackoffPolicy {} + + /** + * Linearly back-off a failed job. See + * {@link android.app.job.JobInfo.Builder#setBackoffCriteria(long, int)} + * retry_time(current_time, num_failures) = + * current_time + initial_backoff_millis * num_failures, num_failures >= 1 + */ + public static final int BACKOFF_POLICY_LINEAR = 0; + + /** + * Exponentially back-off a failed job. See + * {@link android.app.job.JobInfo.Builder#setBackoffCriteria(long, int)} + * + * retry_time(current_time, num_failures) = + * current_time + initial_backoff_millis * 2 ^ (num_failures - 1), num_failures >= 1 + */ + public static final int BACKOFF_POLICY_EXPONENTIAL = 1; + + /* Minimum interval for a periodic job, in milliseconds. */ + private static final long MIN_PERIOD_MILLIS = 15 * 60 * 1000L; // 15 minutes + + /* Minimum flex for a periodic job, in milliseconds. */ + private static final long MIN_FLEX_MILLIS = 5 * 60 * 1000L; // 5 minutes + + /** + * Minimum backoff interval for a job, in milliseconds + * @hide + */ + public static final long MIN_BACKOFF_MILLIS = 10 * 1000L; // 10 seconds + + /** + * Query the minimum interval allowed for periodic scheduled jobs. Attempting + * to declare a smaller period that this when scheduling a job will result in a + * job that is still periodic, but will run with this effective period. + * + * @return The minimum available interval for scheduling periodic jobs, in milliseconds. + */ + public static final long getMinPeriodMillis() { + return MIN_PERIOD_MILLIS; + } + + /** + * Query the minimum flex time allowed for periodic scheduled jobs. Attempting + * to declare a shorter flex time than this when scheduling such a job will + * result in this amount as the effective flex time for the job. + * + * @return The minimum available flex time for scheduling periodic jobs, in milliseconds. + */ + public static final long getMinFlexMillis() { + return MIN_FLEX_MILLIS; + } + + /** + * Query the minimum automatic-reschedule backoff interval permitted for jobs. + * @hide + */ + public static final long getMinBackoffMillis() { + return MIN_BACKOFF_MILLIS; + } + + /** + * Default type of backoff. + * @hide + */ + public static final int DEFAULT_BACKOFF_POLICY = BACKOFF_POLICY_EXPONENTIAL; + + /** + * Default of {@link #getPriority}. + * @hide + */ + public static final int PRIORITY_DEFAULT = 0; + + /** + * Value of {@link #getPriority} for expedited syncs. + * @hide + */ + public static final int PRIORITY_SYNC_EXPEDITED = 10; + + /** + * Value of {@link #getPriority} for first time initialization syncs. + * @hide + */ + public static final int PRIORITY_SYNC_INITIALIZATION = 20; + + /** + * Value of {@link #getPriority} for a BFGS app (overrides the supplied + * JobInfo priority if it is smaller). + * @hide + */ + public static final int PRIORITY_BOUND_FOREGROUND_SERVICE = 30; + + /** @hide For backward compatibility. */ + @UnsupportedAppUsage + public static final int PRIORITY_FOREGROUND_APP = PRIORITY_BOUND_FOREGROUND_SERVICE; + + /** + * Value of {@link #getPriority} for a FG service app (overrides the supplied + * JobInfo priority if it is smaller). + * @hide + */ + @UnsupportedAppUsage + public static final int PRIORITY_FOREGROUND_SERVICE = 35; + + /** + * Value of {@link #getPriority} for the current top app (overrides the supplied + * JobInfo priority if it is smaller). + * @hide + */ + public static final int PRIORITY_TOP_APP = 40; + + /** + * Adjustment of {@link #getPriority} if the app has often (50% or more of the time) + * been running jobs. + * @hide + */ + public static final int PRIORITY_ADJ_OFTEN_RUNNING = -40; + + /** + * Adjustment of {@link #getPriority} if the app has always (90% or more of the time) + * been running jobs. + * @hide + */ + public static final int PRIORITY_ADJ_ALWAYS_RUNNING = -80; + + /** + * Indicates that the implementation of this job will be using + * {@link JobService#startForeground(int, android.app.Notification)} to run + * in the foreground. + * <p> + * When set, the internal scheduling of this job will ignore any background + * network restrictions for the requesting app. Note that this flag alone + * doesn't actually place your {@link JobService} in the foreground; you + * still need to post the notification yourself. + * <p> + * To use this flag, the caller must hold the + * {@link android.Manifest.permission#CONNECTIVITY_INTERNAL} permission. + * + * @hide + */ + @UnsupportedAppUsage + public static final int FLAG_WILL_BE_FOREGROUND = 1 << 0; + + /** + * Allows this job to run despite doze restrictions as long as the app is in the foreground + * or on the temporary whitelist + * @hide + */ + public static final int FLAG_IMPORTANT_WHILE_FOREGROUND = 1 << 1; + + /** + * @hide + */ + public static final int FLAG_PREFETCH = 1 << 2; + + /** + * This job needs to be exempted from the app standby throttling. Only the system (UID 1000) + * can set it. Jobs with a time constrant must not have it. + * + * @hide + */ + public static final int FLAG_EXEMPT_FROM_APP_STANDBY = 1 << 3; + + /** + * @hide + */ + public static final int CONSTRAINT_FLAG_CHARGING = 1 << 0; + + /** + * @hide + */ + public static final int CONSTRAINT_FLAG_BATTERY_NOT_LOW = 1 << 1; + + /** + * @hide + */ + public static final int CONSTRAINT_FLAG_DEVICE_IDLE = 1 << 2; + + /** + * @hide + */ + public static final int CONSTRAINT_FLAG_STORAGE_NOT_LOW = 1 << 3; + + @UnsupportedAppUsage + private final int jobId; + private final PersistableBundle extras; + private final Bundle transientExtras; + private final ClipData clipData; + private final int clipGrantFlags; + @UnsupportedAppUsage + private final ComponentName service; + private final int constraintFlags; + private final TriggerContentUri[] triggerContentUris; + private final long triggerContentUpdateDelay; + private final long triggerContentMaxDelay; + private final boolean hasEarlyConstraint; + private final boolean hasLateConstraint; + private final NetworkRequest networkRequest; + private final long networkDownloadBytes; + private final long networkUploadBytes; + private final long minLatencyMillis; + private final long maxExecutionDelayMillis; + private final boolean isPeriodic; + private final boolean isPersisted; + private final long intervalMillis; + private final long flexMillis; + private final long initialBackoffMillis; + private final int backoffPolicy; + private final int priority; + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) + private final int flags; + + /** + * Unique job id associated with this application (uid). This is the same job ID + * you supplied in the {@link Builder} constructor. + */ + public int getId() { + return jobId; + } + + /** + * @see JobInfo.Builder#setExtras(PersistableBundle) + */ + public @NonNull PersistableBundle getExtras() { + return extras; + } + + /** + * @see JobInfo.Builder#setTransientExtras(Bundle) + */ + public @NonNull Bundle getTransientExtras() { + return transientExtras; + } + + /** + * @see JobInfo.Builder#setClipData(ClipData, int) + */ + public @Nullable ClipData getClipData() { + return clipData; + } + + /** + * @see JobInfo.Builder#setClipData(ClipData, int) + */ + public int getClipGrantFlags() { + return clipGrantFlags; + } + + /** + * Name of the service endpoint that will be called back into by the JobScheduler. + */ + public @NonNull ComponentName getService() { + return service; + } + + /** @hide */ + public int getPriority() { + return priority; + } + + /** @hide */ + public int getFlags() { + return flags; + } + + /** @hide */ + public boolean isExemptedFromAppStandby() { + return ((flags & FLAG_EXEMPT_FROM_APP_STANDBY) != 0) && !isPeriodic(); + } + + /** + * @see JobInfo.Builder#setRequiresCharging(boolean) + */ + public boolean isRequireCharging() { + return (constraintFlags & CONSTRAINT_FLAG_CHARGING) != 0; + } + + /** + * @see JobInfo.Builder#setRequiresBatteryNotLow(boolean) + */ + public boolean isRequireBatteryNotLow() { + return (constraintFlags & CONSTRAINT_FLAG_BATTERY_NOT_LOW) != 0; + } + + /** + * @see JobInfo.Builder#setRequiresDeviceIdle(boolean) + */ + public boolean isRequireDeviceIdle() { + return (constraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0; + } + + /** + * @see JobInfo.Builder#setRequiresStorageNotLow(boolean) + */ + public boolean isRequireStorageNotLow() { + return (constraintFlags & CONSTRAINT_FLAG_STORAGE_NOT_LOW) != 0; + } + + /** + * @hide + */ + public int getConstraintFlags() { + return constraintFlags; + } + + /** + * Which content: URIs must change for the job to be scheduled. Returns null + * if there are none required. + * @see JobInfo.Builder#addTriggerContentUri(TriggerContentUri) + */ + public @Nullable TriggerContentUri[] getTriggerContentUris() { + return triggerContentUris; + } + + /** + * When triggering on content URI changes, this is the delay from when a change + * is detected until the job is scheduled. + * @see JobInfo.Builder#setTriggerContentUpdateDelay(long) + */ + public long getTriggerContentUpdateDelay() { + return triggerContentUpdateDelay; + } + + /** + * When triggering on content URI changes, this is the maximum delay we will + * use before scheduling the job. + * @see JobInfo.Builder#setTriggerContentMaxDelay(long) + */ + public long getTriggerContentMaxDelay() { + return triggerContentMaxDelay; + } + + /** + * Return the basic description of the kind of network this job requires. + * + * @deprecated This method attempts to map {@link #getRequiredNetwork()} + * into the set of simple constants, which results in a loss of + * fidelity. Callers should move to using + * {@link #getRequiredNetwork()} directly. + * @see Builder#setRequiredNetworkType(int) + */ + @Deprecated + public @NetworkType int getNetworkType() { + if (networkRequest == null) { + return NETWORK_TYPE_NONE; + } else if (networkRequest.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_METERED)) { + return NETWORK_TYPE_UNMETERED; + } else if (networkRequest.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_ROAMING)) { + return NETWORK_TYPE_NOT_ROAMING; + } else if (networkRequest.networkCapabilities.hasTransport(TRANSPORT_CELLULAR)) { + return NETWORK_TYPE_CELLULAR; + } else { + return NETWORK_TYPE_ANY; + } + } + + /** + * Return the detailed description of the kind of network this job requires, + * or {@code null} if no specific kind of network is required. + * + * @see Builder#setRequiredNetwork(NetworkRequest) + */ + public @Nullable NetworkRequest getRequiredNetwork() { + return networkRequest; + } + + /** + * Return the estimated size of download traffic that will be performed by + * this job, in bytes. + * + * @return Estimated size of download traffic, or + * {@link #NETWORK_BYTES_UNKNOWN} when unknown. + * @see Builder#setEstimatedNetworkBytes(long, long) + */ + public @BytesLong long getEstimatedNetworkDownloadBytes() { + return networkDownloadBytes; + } + + /** + * Return the estimated size of upload traffic that will be performed by + * this job, in bytes. + * + * @return Estimated size of upload traffic, or + * {@link #NETWORK_BYTES_UNKNOWN} when unknown. + * @see Builder#setEstimatedNetworkBytes(long, long) + */ + public @BytesLong long getEstimatedNetworkUploadBytes() { + return networkUploadBytes; + } + + /** + * Set for a job that does not recur periodically, to specify a delay after which the job + * will be eligible for execution. This value is not set if the job recurs periodically. + * @see JobInfo.Builder#setMinimumLatency(long) + */ + public long getMinLatencyMillis() { + return minLatencyMillis; + } + + /** + * @see JobInfo.Builder#setOverrideDeadline(long) + */ + public long getMaxExecutionDelayMillis() { + return maxExecutionDelayMillis; + } + + /** + * Track whether this job will repeat with a given period. + * @see JobInfo.Builder#setPeriodic(long) + * @see JobInfo.Builder#setPeriodic(long, long) + */ + public boolean isPeriodic() { + return isPeriodic; + } + + /** + * @see JobInfo.Builder#setPersisted(boolean) + */ + public boolean isPersisted() { + return isPersisted; + } + + /** + * Set to the interval between occurrences of this job. This value is <b>not</b> set if the + * job does not recur periodically. + * @see JobInfo.Builder#setPeriodic(long) + * @see JobInfo.Builder#setPeriodic(long, long) + */ + public long getIntervalMillis() { + return intervalMillis; + } + + /** + * Flex time for this job. Only valid if this is a periodic job. The job can + * execute at any time in a window of flex length at the end of the period. + * @see JobInfo.Builder#setPeriodic(long) + * @see JobInfo.Builder#setPeriodic(long, long) + */ + public long getFlexMillis() { + return flexMillis; + } + + /** + * The amount of time the JobScheduler will wait before rescheduling a failed job. This value + * will be increased depending on the backoff policy specified at job creation time. Defaults + * to 30 seconds, minimum is currently 10 seconds. + * @see JobInfo.Builder#setBackoffCriteria(long, int) + */ + public long getInitialBackoffMillis() { + return initialBackoffMillis; + } + + /** + * Return the backoff policy of this job. + * @see JobInfo.Builder#setBackoffCriteria(long, int) + */ + public @BackoffPolicy int getBackoffPolicy() { + return backoffPolicy; + } + + /** + * @see JobInfo.Builder#setImportantWhileForeground(boolean) + */ + public boolean isImportantWhileForeground() { + return (flags & FLAG_IMPORTANT_WHILE_FOREGROUND) != 0; + } + + /** + * @see JobInfo.Builder#setPrefetch(boolean) + */ + public boolean isPrefetch() { + return (flags & FLAG_PREFETCH) != 0; + } + + /** + * User can specify an early constraint of 0L, which is valid, so we keep track of whether the + * function was called at all. + * @hide + */ + public boolean hasEarlyConstraint() { + return hasEarlyConstraint; + } + + /** + * User can specify a late constraint of 0L, which is valid, so we keep track of whether the + * function was called at all. + * @hide + */ + public boolean hasLateConstraint() { + return hasLateConstraint; + } + + private static boolean kindofEqualsBundle(BaseBundle a, BaseBundle b) { + return (a == b) || (a != null && a.kindofEquals(b)); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof JobInfo)) { + return false; + } + JobInfo j = (JobInfo) o; + if (jobId != j.jobId) { + return false; + } + // XXX won't be correct if one is parcelled and the other not. + if (!kindofEqualsBundle(extras, j.extras)) { + return false; + } + // XXX won't be correct if one is parcelled and the other not. + if (!kindofEqualsBundle(transientExtras, j.transientExtras)) { + return false; + } + // XXX for now we consider two different clip data objects to be different, + // regardless of whether their contents are the same. + if (clipData != j.clipData) { + return false; + } + if (clipGrantFlags != j.clipGrantFlags) { + return false; + } + if (!Objects.equals(service, j.service)) { + return false; + } + if (constraintFlags != j.constraintFlags) { + return false; + } + if (!Arrays.equals(triggerContentUris, j.triggerContentUris)) { + return false; + } + if (triggerContentUpdateDelay != j.triggerContentUpdateDelay) { + return false; + } + if (triggerContentMaxDelay != j.triggerContentMaxDelay) { + return false; + } + if (hasEarlyConstraint != j.hasEarlyConstraint) { + return false; + } + if (hasLateConstraint != j.hasLateConstraint) { + return false; + } + if (!Objects.equals(networkRequest, j.networkRequest)) { + return false; + } + if (networkDownloadBytes != j.networkDownloadBytes) { + return false; + } + if (networkUploadBytes != j.networkUploadBytes) { + return false; + } + if (minLatencyMillis != j.minLatencyMillis) { + return false; + } + if (maxExecutionDelayMillis != j.maxExecutionDelayMillis) { + return false; + } + if (isPeriodic != j.isPeriodic) { + return false; + } + if (isPersisted != j.isPersisted) { + return false; + } + if (intervalMillis != j.intervalMillis) { + return false; + } + if (flexMillis != j.flexMillis) { + return false; + } + if (initialBackoffMillis != j.initialBackoffMillis) { + return false; + } + if (backoffPolicy != j.backoffPolicy) { + return false; + } + if (priority != j.priority) { + return false; + } + if (flags != j.flags) { + return false; + } + return true; + } + + @Override + public int hashCode() { + int hashCode = jobId; + if (extras != null) { + hashCode = 31 * hashCode + extras.hashCode(); + } + if (transientExtras != null) { + hashCode = 31 * hashCode + transientExtras.hashCode(); + } + if (clipData != null) { + hashCode = 31 * hashCode + clipData.hashCode(); + } + hashCode = 31*hashCode + clipGrantFlags; + if (service != null) { + hashCode = 31 * hashCode + service.hashCode(); + } + hashCode = 31 * hashCode + constraintFlags; + if (triggerContentUris != null) { + hashCode = 31 * hashCode + Arrays.hashCode(triggerContentUris); + } + hashCode = 31 * hashCode + Long.hashCode(triggerContentUpdateDelay); + hashCode = 31 * hashCode + Long.hashCode(triggerContentMaxDelay); + hashCode = 31 * hashCode + Boolean.hashCode(hasEarlyConstraint); + hashCode = 31 * hashCode + Boolean.hashCode(hasLateConstraint); + if (networkRequest != null) { + hashCode = 31 * hashCode + networkRequest.hashCode(); + } + hashCode = 31 * hashCode + Long.hashCode(networkDownloadBytes); + hashCode = 31 * hashCode + Long.hashCode(networkUploadBytes); + hashCode = 31 * hashCode + Long.hashCode(minLatencyMillis); + hashCode = 31 * hashCode + Long.hashCode(maxExecutionDelayMillis); + hashCode = 31 * hashCode + Boolean.hashCode(isPeriodic); + hashCode = 31 * hashCode + Boolean.hashCode(isPersisted); + hashCode = 31 * hashCode + Long.hashCode(intervalMillis); + hashCode = 31 * hashCode + Long.hashCode(flexMillis); + hashCode = 31 * hashCode + Long.hashCode(initialBackoffMillis); + hashCode = 31 * hashCode + backoffPolicy; + hashCode = 31 * hashCode + priority; + hashCode = 31 * hashCode + flags; + return hashCode; + } + + private JobInfo(Parcel in) { + jobId = in.readInt(); + extras = in.readPersistableBundle(); + transientExtras = in.readBundle(); + if (in.readInt() != 0) { + clipData = ClipData.CREATOR.createFromParcel(in); + clipGrantFlags = in.readInt(); + } else { + clipData = null; + clipGrantFlags = 0; + } + service = in.readParcelable(null); + constraintFlags = in.readInt(); + triggerContentUris = in.createTypedArray(TriggerContentUri.CREATOR); + triggerContentUpdateDelay = in.readLong(); + triggerContentMaxDelay = in.readLong(); + if (in.readInt() != 0) { + networkRequest = NetworkRequest.CREATOR.createFromParcel(in); + } else { + networkRequest = null; + } + networkDownloadBytes = in.readLong(); + networkUploadBytes = in.readLong(); + minLatencyMillis = in.readLong(); + maxExecutionDelayMillis = in.readLong(); + isPeriodic = in.readInt() == 1; + isPersisted = in.readInt() == 1; + intervalMillis = in.readLong(); + flexMillis = in.readLong(); + initialBackoffMillis = in.readLong(); + backoffPolicy = in.readInt(); + hasEarlyConstraint = in.readInt() == 1; + hasLateConstraint = in.readInt() == 1; + priority = in.readInt(); + flags = in.readInt(); + } + + private JobInfo(JobInfo.Builder b) { + jobId = b.mJobId; + extras = b.mExtras.deepCopy(); + transientExtras = b.mTransientExtras.deepCopy(); + clipData = b.mClipData; + clipGrantFlags = b.mClipGrantFlags; + service = b.mJobService; + constraintFlags = b.mConstraintFlags; + triggerContentUris = b.mTriggerContentUris != null + ? b.mTriggerContentUris.toArray(new TriggerContentUri[b.mTriggerContentUris.size()]) + : null; + triggerContentUpdateDelay = b.mTriggerContentUpdateDelay; + triggerContentMaxDelay = b.mTriggerContentMaxDelay; + networkRequest = b.mNetworkRequest; + networkDownloadBytes = b.mNetworkDownloadBytes; + networkUploadBytes = b.mNetworkUploadBytes; + minLatencyMillis = b.mMinLatencyMillis; + maxExecutionDelayMillis = b.mMaxExecutionDelayMillis; + isPeriodic = b.mIsPeriodic; + isPersisted = b.mIsPersisted; + intervalMillis = b.mIntervalMillis; + flexMillis = b.mFlexMillis; + initialBackoffMillis = b.mInitialBackoffMillis; + backoffPolicy = b.mBackoffPolicy; + hasEarlyConstraint = b.mHasEarlyConstraint; + hasLateConstraint = b.mHasLateConstraint; + priority = b.mPriority; + flags = b.mFlags; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(jobId); + out.writePersistableBundle(extras); + out.writeBundle(transientExtras); + if (clipData != null) { + out.writeInt(1); + clipData.writeToParcel(out, flags); + out.writeInt(clipGrantFlags); + } else { + out.writeInt(0); + } + out.writeParcelable(service, flags); + out.writeInt(constraintFlags); + out.writeTypedArray(triggerContentUris, flags); + out.writeLong(triggerContentUpdateDelay); + out.writeLong(triggerContentMaxDelay); + if (networkRequest != null) { + out.writeInt(1); + networkRequest.writeToParcel(out, flags); + } else { + out.writeInt(0); + } + out.writeLong(networkDownloadBytes); + out.writeLong(networkUploadBytes); + out.writeLong(minLatencyMillis); + out.writeLong(maxExecutionDelayMillis); + out.writeInt(isPeriodic ? 1 : 0); + out.writeInt(isPersisted ? 1 : 0); + out.writeLong(intervalMillis); + out.writeLong(flexMillis); + out.writeLong(initialBackoffMillis); + out.writeInt(backoffPolicy); + out.writeInt(hasEarlyConstraint ? 1 : 0); + out.writeInt(hasLateConstraint ? 1 : 0); + out.writeInt(priority); + out.writeInt(this.flags); + } + + public static final @android.annotation.NonNull Creator<JobInfo> CREATOR = new Creator<JobInfo>() { + @Override + public JobInfo createFromParcel(Parcel in) { + return new JobInfo(in); + } + + @Override + public JobInfo[] newArray(int size) { + return new JobInfo[size]; + } + }; + + @Override + public String toString() { + return "(job:" + jobId + "/" + service.flattenToShortString() + ")"; + } + + /** + * Information about a content URI modification that a job would like to + * trigger on. + */ + public static final class TriggerContentUri implements Parcelable { + private final Uri mUri; + private final int mFlags; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, prefix = { "FLAG_" }, value = { + FLAG_NOTIFY_FOR_DESCENDANTS, + }) + public @interface Flags { } + + /** + * Flag for trigger: also trigger if any descendants of the given URI change. + * Corresponds to the <var>notifyForDescendants</var> of + * {@link android.content.ContentResolver#registerContentObserver}. + */ + public static final int FLAG_NOTIFY_FOR_DESCENDANTS = 1<<0; + + /** + * Create a new trigger description. + * @param uri The URI to observe. Must be non-null. + * @param flags Flags for the observer. + */ + public TriggerContentUri(@NonNull Uri uri, @Flags int flags) { + mUri = uri; + mFlags = flags; + } + + /** + * Return the Uri this trigger was created for. + */ + public Uri getUri() { + return mUri; + } + + /** + * Return the flags supplied for the trigger. + */ + public @Flags int getFlags() { + return mFlags; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof TriggerContentUri)) { + return false; + } + TriggerContentUri t = (TriggerContentUri) o; + return Objects.equals(t.mUri, mUri) && t.mFlags == mFlags; + } + + @Override + public int hashCode() { + return (mUri == null ? 0 : mUri.hashCode()) ^ mFlags; + } + + private TriggerContentUri(Parcel in) { + mUri = Uri.CREATOR.createFromParcel(in); + mFlags = in.readInt(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + mUri.writeToParcel(out, flags); + out.writeInt(mFlags); + } + + public static final @android.annotation.NonNull Creator<TriggerContentUri> CREATOR = new Creator<TriggerContentUri>() { + @Override + public TriggerContentUri createFromParcel(Parcel in) { + return new TriggerContentUri(in); + } + + @Override + public TriggerContentUri[] newArray(int size) { + return new TriggerContentUri[size]; + } + }; + } + + /** Builder class for constructing {@link JobInfo} objects. */ + public static final class Builder { + private final int mJobId; + private final ComponentName mJobService; + private PersistableBundle mExtras = PersistableBundle.EMPTY; + private Bundle mTransientExtras = Bundle.EMPTY; + private ClipData mClipData; + private int mClipGrantFlags; + private int mPriority = PRIORITY_DEFAULT; + private int mFlags; + // Requirements. + private int mConstraintFlags; + private NetworkRequest mNetworkRequest; + private long mNetworkDownloadBytes = NETWORK_BYTES_UNKNOWN; + private long mNetworkUploadBytes = NETWORK_BYTES_UNKNOWN; + private ArrayList<TriggerContentUri> mTriggerContentUris; + private long mTriggerContentUpdateDelay = -1; + private long mTriggerContentMaxDelay = -1; + private boolean mIsPersisted; + // One-off parameters. + private long mMinLatencyMillis; + private long mMaxExecutionDelayMillis; + // Periodic parameters. + private boolean mIsPeriodic; + private boolean mHasEarlyConstraint; + private boolean mHasLateConstraint; + private long mIntervalMillis; + private long mFlexMillis; + // Back-off parameters. + private long mInitialBackoffMillis = DEFAULT_INITIAL_BACKOFF_MILLIS; + private int mBackoffPolicy = DEFAULT_BACKOFF_POLICY; + /** Easy way to track whether the client has tried to set a back-off policy. */ + private boolean mBackoffPolicySet = false; + + /** + * Initialize a new Builder to construct a {@link JobInfo}. + * + * @param jobId Application-provided id for this job. Subsequent calls to cancel, or + * jobs created with the same jobId, will update the pre-existing job with + * the same id. This ID must be unique across all clients of the same uid + * (not just the same package). You will want to make sure this is a stable + * id across app updates, so probably not based on a resource ID. + * @param jobService The endpoint that you implement that will receive the callback from the + * JobScheduler. + */ + public Builder(int jobId, @NonNull ComponentName jobService) { + mJobService = jobService; + mJobId = jobId; + } + + /** @hide */ + @UnsupportedAppUsage + public Builder setPriority(int priority) { + mPriority = priority; + return this; + } + + /** @hide */ + @UnsupportedAppUsage + public Builder setFlags(int flags) { + mFlags = flags; + return this; + } + + /** + * Set optional extras. This is persisted, so we only allow primitive types. + * @param extras Bundle containing extras you want the scheduler to hold on to for you. + * @see JobInfo#getExtras() + */ + public Builder setExtras(@NonNull PersistableBundle extras) { + mExtras = extras; + return this; + } + + /** + * Set optional transient extras. + * + * <p>Because setting this property is not compatible with persisted + * jobs, doing so will throw an {@link java.lang.IllegalArgumentException} when + * {@link android.app.job.JobInfo.Builder#build()} is called.</p> + * + * @param extras Bundle containing extras you want the scheduler to hold on to for you. + * @see JobInfo#getTransientExtras() + */ + public Builder setTransientExtras(@NonNull Bundle extras) { + mTransientExtras = extras; + return this; + } + + /** + * Set a {@link ClipData} associated with this Job. + * + * <p>The main purpose of providing a ClipData is to allow granting of + * URI permissions for data associated with the clip. The exact kind + * of permission grant to perform is specified through <var>grantFlags</var>. + * + * <p>If the ClipData contains items that are Intents, any + * grant flags in those Intents will be ignored. Only flags provided as an argument + * to this method are respected, and will be applied to all Uri or + * Intent items in the clip (or sub-items of the clip). + * + * <p>Because setting this property is not compatible with persisted + * jobs, doing so will throw an {@link java.lang.IllegalArgumentException} when + * {@link android.app.job.JobInfo.Builder#build()} is called.</p> + * + * @param clip The new clip to set. May be null to clear the current clip. + * @param grantFlags The desired permissions to grant for any URIs. This should be + * a combination of {@link android.content.Intent#FLAG_GRANT_READ_URI_PERMISSION}, + * {@link android.content.Intent#FLAG_GRANT_WRITE_URI_PERMISSION}, and + * {@link android.content.Intent#FLAG_GRANT_PREFIX_URI_PERMISSION}. + * @see JobInfo#getClipData() + * @see JobInfo#getClipGrantFlags() + */ + public Builder setClipData(@Nullable ClipData clip, int grantFlags) { + mClipData = clip; + mClipGrantFlags = grantFlags; + return this; + } + + /** + * Set basic description of the kind of network your job requires. If + * you need more precise control over network capabilities, see + * {@link #setRequiredNetwork(NetworkRequest)}. + * <p> + * If your job doesn't need a network connection, you don't need to call + * this method, as the default value is {@link #NETWORK_TYPE_NONE}. + * <p> + * Calling this method defines network as a strict requirement for your + * job. If the network requested is not available your job will never + * run. See {@link #setOverrideDeadline(long)} to change this behavior. + * Calling this method will override any requirements previously defined + * by {@link #setRequiredNetwork(NetworkRequest)}; you typically only + * want to call one of these methods. + * <p class="note"> + * When your job executes in + * {@link JobService#onStartJob(JobParameters)}, be sure to use the + * specific network returned by {@link JobParameters#getNetwork()}, + * otherwise you'll use the default network which may not meet this + * constraint. + * + * @see #setRequiredNetwork(NetworkRequest) + * @see JobInfo#getNetworkType() + * @see JobParameters#getNetwork() + */ + public Builder setRequiredNetworkType(@NetworkType int networkType) { + if (networkType == NETWORK_TYPE_NONE) { + return setRequiredNetwork(null); + } else { + final NetworkRequest.Builder builder = new NetworkRequest.Builder(); + + // All types require validated Internet + builder.addCapability(NET_CAPABILITY_INTERNET); + builder.addCapability(NET_CAPABILITY_VALIDATED); + builder.removeCapability(NET_CAPABILITY_NOT_VPN); + + if (networkType == NETWORK_TYPE_ANY) { + // No other capabilities + } else if (networkType == NETWORK_TYPE_UNMETERED) { + builder.addCapability(NET_CAPABILITY_NOT_METERED); + } else if (networkType == NETWORK_TYPE_NOT_ROAMING) { + builder.addCapability(NET_CAPABILITY_NOT_ROAMING); + } else if (networkType == NETWORK_TYPE_CELLULAR) { + builder.addTransportType(TRANSPORT_CELLULAR); + } + + return setRequiredNetwork(builder.build()); + } + } + + /** + * Set detailed description of the kind of network your job requires. + * <p> + * If your job doesn't need a network connection, you don't need to call + * this method, as the default is {@code null}. + * <p> + * Calling this method defines network as a strict requirement for your + * job. If the network requested is not available your job will never + * run. See {@link #setOverrideDeadline(long)} to change this behavior. + * Calling this method will override any requirements previously defined + * by {@link #setRequiredNetworkType(int)}; you typically only want to + * call one of these methods. + * <p class="note"> + * When your job executes in + * {@link JobService#onStartJob(JobParameters)}, be sure to use the + * specific network returned by {@link JobParameters#getNetwork()}, + * otherwise you'll use the default network which may not meet this + * constraint. + * + * @param networkRequest The detailed description of the kind of network + * this job requires, or {@code null} if no specific kind of + * network is required. Defining a {@link NetworkSpecifier} + * is only supported for jobs that aren't persisted. + * @see #setRequiredNetworkType(int) + * @see JobInfo#getRequiredNetwork() + * @see JobParameters#getNetwork() + */ + public Builder setRequiredNetwork(@Nullable NetworkRequest networkRequest) { + mNetworkRequest = networkRequest; + return this; + } + + /** + * Set the estimated size of network traffic that will be performed by + * this job, in bytes. + * <p> + * Apps are encouraged to provide values that are as accurate as + * possible, but when the exact size isn't available, an + * order-of-magnitude estimate can be provided instead. Here are some + * specific examples: + * <ul> + * <li>A job that is backing up a photo knows the exact size of that + * photo, so it should provide that size as the estimate. + * <li>A job that refreshes top news stories wouldn't know an exact + * size, but if the size is expected to be consistently around 100KB, it + * can provide that order-of-magnitude value as the estimate. + * <li>A job that synchronizes email could end up using an extreme range + * of data, from under 1KB when nothing has changed, to dozens of MB + * when there are new emails with attachments. Jobs that cannot provide + * reasonable estimates should use the sentinel value + * {@link JobInfo#NETWORK_BYTES_UNKNOWN}. + * </ul> + * Note that the system may choose to delay jobs with large network + * usage estimates when the device has a poor network connection, in + * order to save battery. + * <p> + * The values provided here only reflect the traffic that will be + * performed by the base job; if you're using {@link JobWorkItem} then + * you also need to define the network traffic used by each work item + * when constructing them. + * + * @param downloadBytes The estimated size of network traffic that will + * be downloaded by this job, in bytes. + * @param uploadBytes The estimated size of network traffic that will be + * uploaded by this job, in bytes. + * @see JobInfo#getEstimatedNetworkDownloadBytes() + * @see JobInfo#getEstimatedNetworkUploadBytes() + * @see JobWorkItem#JobWorkItem(android.content.Intent, long, long) + */ + public Builder setEstimatedNetworkBytes(@BytesLong long downloadBytes, + @BytesLong long uploadBytes) { + mNetworkDownloadBytes = downloadBytes; + mNetworkUploadBytes = uploadBytes; + return this; + } + + /** + * Specify that to run this job, the device must be charging (or be a + * non-battery-powered device connected to permanent power, such as Android TV + * devices). This defaults to {@code false}. + * + * <p class="note">For purposes of running jobs, a battery-powered device + * "charging" is not quite the same as simply being connected to power. If the + * device is so busy that the battery is draining despite a power connection, jobs + * with this constraint will <em>not</em> run. This can happen during some + * common use cases such as video chat, particularly if the device is plugged in + * to USB rather than to wall power. + * + * @param requiresCharging Pass {@code true} to require that the device be + * charging in order to run the job. + * @see JobInfo#isRequireCharging() + */ + public Builder setRequiresCharging(boolean requiresCharging) { + mConstraintFlags = (mConstraintFlags&~CONSTRAINT_FLAG_CHARGING) + | (requiresCharging ? CONSTRAINT_FLAG_CHARGING : 0); + return this; + } + + /** + * Specify that to run this job, the device's battery level must not be low. + * This defaults to false. If true, the job will only run when the battery level + * is not low, which is generally the point where the user is given a "low battery" + * warning. + * @param batteryNotLow Whether or not the device's battery level must not be low. + * @see JobInfo#isRequireBatteryNotLow() + */ + public Builder setRequiresBatteryNotLow(boolean batteryNotLow) { + mConstraintFlags = (mConstraintFlags&~CONSTRAINT_FLAG_BATTERY_NOT_LOW) + | (batteryNotLow ? CONSTRAINT_FLAG_BATTERY_NOT_LOW : 0); + return this; + } + + /** + * When set {@code true}, ensure that this job will not run if the device is in active use. + * The default state is {@code false}: that is, the for the job to be runnable even when + * someone is interacting with the device. + * + * <p>This state is a loose definition provided by the system. In general, it means that + * the device is not currently being used interactively, and has not been in use for some + * time. As such, it is a good time to perform resource heavy jobs. Bear in mind that + * battery usage will still be attributed to your application, and surfaced to the user in + * battery stats.</p> + * + * <p class="note">Despite the similar naming, this job constraint is <em>not</em> + * related to the system's "device idle" or "doze" states. This constraint only + * determines whether a job is allowed to run while the device is directly in use. + * + * @param requiresDeviceIdle Pass {@code true} to prevent the job from running + * while the device is being used interactively. + * @see JobInfo#isRequireDeviceIdle() + */ + public Builder setRequiresDeviceIdle(boolean requiresDeviceIdle) { + mConstraintFlags = (mConstraintFlags&~CONSTRAINT_FLAG_DEVICE_IDLE) + | (requiresDeviceIdle ? CONSTRAINT_FLAG_DEVICE_IDLE : 0); + return this; + } + + /** + * Specify that to run this job, the device's available storage must not be low. + * This defaults to false. If true, the job will only run when the device is not + * in a low storage state, which is generally the point where the user is given a + * "low storage" warning. + * @param storageNotLow Whether or not the device's available storage must not be low. + * @see JobInfo#isRequireStorageNotLow() + */ + public Builder setRequiresStorageNotLow(boolean storageNotLow) { + mConstraintFlags = (mConstraintFlags&~CONSTRAINT_FLAG_STORAGE_NOT_LOW) + | (storageNotLow ? CONSTRAINT_FLAG_STORAGE_NOT_LOW : 0); + return this; + } + + /** + * Add a new content: URI that will be monitored with a + * {@link android.database.ContentObserver}, and will cause the job to execute if changed. + * If you have any trigger content URIs associated with a job, it will not execute until + * there has been a change report for one or more of them. + * + * <p>Note that trigger URIs can not be used in combination with + * {@link #setPeriodic(long)} or {@link #setPersisted(boolean)}. To continually monitor + * for content changes, you need to schedule a new JobInfo observing the same URIs + * before you finish execution of the JobService handling the most recent changes. + * Following this pattern will ensure you do not lost any content changes: while your + * job is running, the system will continue monitoring for content changes, and propagate + * any it sees over to the next job you schedule.</p> + * + * <p>Because setting this property is not compatible with periodic or + * persisted jobs, doing so will throw an {@link java.lang.IllegalArgumentException} when + * {@link android.app.job.JobInfo.Builder#build()} is called.</p> + * + * <p>The following example shows how this feature can be used to monitor for changes + * in the photos on a device.</p> + * + * {@sample development/samples/ApiDemos/src/com/example/android/apis/content/PhotosContentJob.java + * job} + * + * @param uri The content: URI to monitor. + * @see JobInfo#getTriggerContentUris() + */ + public Builder addTriggerContentUri(@NonNull TriggerContentUri uri) { + if (mTriggerContentUris == null) { + mTriggerContentUris = new ArrayList<>(); + } + mTriggerContentUris.add(uri); + return this; + } + + /** + * Set the delay (in milliseconds) from when a content change is detected until + * the job is scheduled. If there are more changes during that time, the delay + * will be reset to start at the time of the most recent change. + * @param durationMs Delay after most recent content change, in milliseconds. + * @see JobInfo#getTriggerContentUpdateDelay() + */ + public Builder setTriggerContentUpdateDelay(long durationMs) { + mTriggerContentUpdateDelay = durationMs; + return this; + } + + /** + * Set the maximum total delay (in milliseconds) that is allowed from the first + * time a content change is detected until the job is scheduled. + * @param durationMs Delay after initial content change, in milliseconds. + * @see JobInfo#getTriggerContentMaxDelay() + */ + public Builder setTriggerContentMaxDelay(long durationMs) { + mTriggerContentMaxDelay = durationMs; + return this; + } + + /** + * Specify that this job should recur with the provided interval, not more than once per + * period. You have no control over when within this interval this job will be executed, + * only the guarantee that it will be executed at most once within this interval. + * Setting this function on the builder with {@link #setMinimumLatency(long)} or + * {@link #setOverrideDeadline(long)} will result in an error. + * @param intervalMillis Millisecond interval for which this job will repeat. + * @see JobInfo#getIntervalMillis() + * @see JobInfo#getFlexMillis() + */ + public Builder setPeriodic(long intervalMillis) { + return setPeriodic(intervalMillis, intervalMillis); + } + + /** + * Specify that this job should recur with the provided interval and flex. The job can + * execute at any time in a window of flex length at the end of the period. + * @param intervalMillis Millisecond interval for which this job will repeat. A minimum + * value of {@link #getMinPeriodMillis()} is enforced. + * @param flexMillis Millisecond flex for this job. Flex is clamped to be at least + * {@link #getMinFlexMillis()} or 5 percent of the period, whichever is + * higher. + * @see JobInfo#getIntervalMillis() + * @see JobInfo#getFlexMillis() + */ + public Builder setPeriodic(long intervalMillis, long flexMillis) { + final long minPeriod = getMinPeriodMillis(); + if (intervalMillis < minPeriod) { + Log.w(TAG, "Requested interval " + formatDuration(intervalMillis) + " for job " + + mJobId + " is too small; raising to " + formatDuration(minPeriod)); + intervalMillis = minPeriod; + } + + final long percentClamp = 5 * intervalMillis / 100; + final long minFlex = Math.max(percentClamp, getMinFlexMillis()); + if (flexMillis < minFlex) { + Log.w(TAG, "Requested flex " + formatDuration(flexMillis) + " for job " + mJobId + + " is too small; raising to " + formatDuration(minFlex)); + flexMillis = minFlex; + } + + mIsPeriodic = true; + mIntervalMillis = intervalMillis; + mFlexMillis = flexMillis; + mHasEarlyConstraint = mHasLateConstraint = true; + return this; + } + + /** + * Specify that this job should be delayed by the provided amount of time. + * Because it doesn't make sense setting this property on a periodic job, doing so will + * throw an {@link java.lang.IllegalArgumentException} when + * {@link android.app.job.JobInfo.Builder#build()} is called. + * @param minLatencyMillis Milliseconds before which this job will not be considered for + * execution. + * @see JobInfo#getMinLatencyMillis() + */ + public Builder setMinimumLatency(long minLatencyMillis) { + mMinLatencyMillis = minLatencyMillis; + mHasEarlyConstraint = true; + return this; + } + + /** + * Set deadline which is the maximum scheduling latency. The job will be run by this + * deadline even if other requirements are not met. Because it doesn't make sense setting + * this property on a periodic job, doing so will throw an + * {@link java.lang.IllegalArgumentException} when + * {@link android.app.job.JobInfo.Builder#build()} is called. + * @see JobInfo#getMaxExecutionDelayMillis() + */ + public Builder setOverrideDeadline(long maxExecutionDelayMillis) { + mMaxExecutionDelayMillis = maxExecutionDelayMillis; + mHasLateConstraint = true; + return this; + } + + /** + * Set up the back-off/retry policy. + * This defaults to some respectable values: {30 seconds, Exponential}. We cap back-off at + * 5hrs. + * Note that trying to set a backoff criteria for a job with + * {@link #setRequiresDeviceIdle(boolean)} will throw an exception when you call build(). + * This is because back-off typically does not make sense for these types of jobs. See + * {@link android.app.job.JobService#jobFinished(android.app.job.JobParameters, boolean)} + * for more description of the return value for the case of a job executing while in idle + * mode. + * @param initialBackoffMillis Millisecond time interval to wait initially when job has + * failed. + * @see JobInfo#getInitialBackoffMillis() + * @see JobInfo#getBackoffPolicy() + */ + public Builder setBackoffCriteria(long initialBackoffMillis, + @BackoffPolicy int backoffPolicy) { + final long minBackoff = getMinBackoffMillis(); + if (initialBackoffMillis < minBackoff) { + Log.w(TAG, "Requested backoff " + formatDuration(initialBackoffMillis) + " for job " + + mJobId + " is too small; raising to " + formatDuration(minBackoff)); + initialBackoffMillis = minBackoff; + } + + mBackoffPolicySet = true; + mInitialBackoffMillis = initialBackoffMillis; + mBackoffPolicy = backoffPolicy; + return this; + } + + /** + * Setting this to true indicates that this job is important while the scheduling app + * is in the foreground or on the temporary whitelist for background restrictions. + * This means that the system will relax doze restrictions on this job during this time. + * + * Apps should use this flag only for short jobs that are essential for the app to function + * properly in the foreground. + * + * Note that once the scheduling app is no longer whitelisted from background restrictions + * and in the background, or the job failed due to unsatisfied constraints, + * this job should be expected to behave like other jobs without this flag. + * + * @param importantWhileForeground whether to relax doze restrictions for this job when the + * app is in the foreground. False by default. + * @see JobInfo#isImportantWhileForeground() + */ + public Builder setImportantWhileForeground(boolean importantWhileForeground) { + if (importantWhileForeground) { + mFlags |= FLAG_IMPORTANT_WHILE_FOREGROUND; + } else { + mFlags &= (~FLAG_IMPORTANT_WHILE_FOREGROUND); + } + return this; + } + + /** + * Setting this to true indicates that this job is designed to prefetch + * content that will make a material improvement to the experience of + * the specific user of this device. For example, fetching top headlines + * of interest to the current user. + * <p> + * The system may use this signal to relax the network constraints you + * originally requested, such as allowing a + * {@link JobInfo#NETWORK_TYPE_UNMETERED} job to run over a metered + * network when there is a surplus of metered data available. The system + * may also use this signal in combination with end user usage patterns + * to ensure data is prefetched before the user launches your app. + * @see JobInfo#isPrefetch() + */ + public Builder setPrefetch(boolean prefetch) { + if (prefetch) { + mFlags |= FLAG_PREFETCH; + } else { + mFlags &= (~FLAG_PREFETCH); + } + return this; + } + + /** + * Set whether or not to persist this job across device reboots. + * + * @param isPersisted True to indicate that the job will be written to + * disk and loaded at boot. + * @see JobInfo#isPersisted() + */ + @RequiresPermission(android.Manifest.permission.RECEIVE_BOOT_COMPLETED) + public Builder setPersisted(boolean isPersisted) { + mIsPersisted = isPersisted; + return this; + } + + /** + * @return The job object to hand to the JobScheduler. This object is immutable. + */ + public JobInfo build() { + // Check that network estimates require network type + if ((mNetworkDownloadBytes > 0 || mNetworkUploadBytes > 0) && mNetworkRequest == null) { + throw new IllegalArgumentException( + "Can't provide estimated network usage without requiring a network"); + } + // We can't serialize network specifiers + if (mIsPersisted && mNetworkRequest != null + && mNetworkRequest.networkCapabilities.getNetworkSpecifier() != null) { + throw new IllegalArgumentException( + "Network specifiers aren't supported for persistent jobs"); + } + // Check that a deadline was not set on a periodic job. + if (mIsPeriodic) { + if (mMaxExecutionDelayMillis != 0L) { + throw new IllegalArgumentException("Can't call setOverrideDeadline() on a " + + "periodic job."); + } + if (mMinLatencyMillis != 0L) { + throw new IllegalArgumentException("Can't call setMinimumLatency() on a " + + "periodic job"); + } + if (mTriggerContentUris != null) { + throw new IllegalArgumentException("Can't call addTriggerContentUri() on a " + + "periodic job"); + } + } + if (mIsPersisted) { + if (mTriggerContentUris != null) { + throw new IllegalArgumentException("Can't call addTriggerContentUri() on a " + + "persisted job"); + } + if (!mTransientExtras.isEmpty()) { + throw new IllegalArgumentException("Can't call setTransientExtras() on a " + + "persisted job"); + } + if (mClipData != null) { + throw new IllegalArgumentException("Can't call setClipData() on a " + + "persisted job"); + } + } + if ((mFlags & FLAG_IMPORTANT_WHILE_FOREGROUND) != 0 && mHasEarlyConstraint) { + throw new IllegalArgumentException("An important while foreground job cannot " + + "have a time delay"); + } + if (mBackoffPolicySet && (mConstraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0) { + throw new IllegalArgumentException("An idle mode job will not respect any" + + " back-off policy, so calling setBackoffCriteria with" + + " setRequiresDeviceIdle is an error."); + } + return new JobInfo(this); + } + + /** + * @hide + */ + public String summarize() { + final String service = (mJobService != null) + ? mJobService.flattenToShortString() + : "null"; + return "JobInfo.Builder{job:" + mJobId + "/" + service + "}"; + } + } + + /** + * Convert a priority integer into a human readable string for debugging. + * @hide + */ + public static String getPriorityString(int priority) { + switch (priority) { + case PRIORITY_DEFAULT: + return PRIORITY_DEFAULT + " [DEFAULT]"; + case PRIORITY_SYNC_EXPEDITED: + return PRIORITY_SYNC_EXPEDITED + " [SYNC_EXPEDITED]"; + case PRIORITY_SYNC_INITIALIZATION: + return PRIORITY_SYNC_INITIALIZATION + " [SYNC_INITIALIZATION]"; + case PRIORITY_BOUND_FOREGROUND_SERVICE: + return PRIORITY_BOUND_FOREGROUND_SERVICE + " [BFGS_APP]"; + case PRIORITY_FOREGROUND_SERVICE: + return PRIORITY_FOREGROUND_SERVICE + " [FGS_APP]"; + case PRIORITY_TOP_APP: + return PRIORITY_TOP_APP + " [TOP_APP]"; + + // PRIORITY_ADJ_* are adjustments and not used as real priorities. + // No need to convert to strings. + } + return priority + " [UNKNOWN]"; + } +} diff --git a/apex/jobscheduler/framework/java/android/app/job/JobParameters.aidl b/apex/jobscheduler/framework/java/android/app/job/JobParameters.aidl new file mode 100644 index 000000000000..e7551b9ab9f2 --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/JobParameters.aidl @@ -0,0 +1,19 @@ +/** + * Copyright 2014, 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 android.app.job; + +parcelable JobParameters; diff --git a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java new file mode 100644 index 000000000000..150cdbc3cacf --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java @@ -0,0 +1,377 @@ +/* + * Copyright (C) 2014 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 android.app.job; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UnsupportedAppUsage; +import android.app.job.IJobCallback; +import android.content.ClipData; +import android.net.Network; +import android.net.Uri; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.PersistableBundle; +import android.os.RemoteException; + +/** + * Contains the parameters used to configure/identify your job. You do not create this object + * yourself, instead it is handed in to your application by the System. + */ +public class JobParameters implements Parcelable { + + /** @hide */ + public static final int REASON_CANCELED = JobProtoEnums.STOP_REASON_CANCELLED; // 0. + /** @hide */ + public static final int REASON_CONSTRAINTS_NOT_SATISFIED = + JobProtoEnums.STOP_REASON_CONSTRAINTS_NOT_SATISFIED; //1. + /** @hide */ + public static final int REASON_PREEMPT = JobProtoEnums.STOP_REASON_PREEMPT; // 2. + /** @hide */ + public static final int REASON_TIMEOUT = JobProtoEnums.STOP_REASON_TIMEOUT; // 3. + /** @hide */ + public static final int REASON_DEVICE_IDLE = JobProtoEnums.STOP_REASON_DEVICE_IDLE; // 4. + /** @hide */ + public static final int REASON_DEVICE_THERMAL = JobProtoEnums.STOP_REASON_DEVICE_THERMAL; // 5. + + /** + * All the stop reason codes. This should be regarded as an immutable array at runtime. + * + * Note the order of these values will affect "dumpsys batterystats", and we do not want to + * change the order of existing fields, so adding new fields is okay but do not remove or + * change existing fields. When deprecating a field, just replace that with "-1" in this array. + * + * @hide + */ + public static final int[] JOB_STOP_REASON_CODES = { + REASON_CANCELED, + REASON_CONSTRAINTS_NOT_SATISFIED, + REASON_PREEMPT, + REASON_TIMEOUT, + REASON_DEVICE_IDLE, + REASON_DEVICE_THERMAL, + }; + + /** @hide */ + public static String getReasonName(int reason) { + switch (reason) { + case REASON_CANCELED: return "canceled"; + case REASON_CONSTRAINTS_NOT_SATISFIED: return "constraints"; + case REASON_PREEMPT: return "preempt"; + case REASON_TIMEOUT: return "timeout"; + case REASON_DEVICE_IDLE: return "device_idle"; + case REASON_DEVICE_THERMAL: return "thermal"; + default: return "unknown:" + reason; + } + } + + @UnsupportedAppUsage + private final int jobId; + private final PersistableBundle extras; + private final Bundle transientExtras; + private final ClipData clipData; + private final int clipGrantFlags; + @UnsupportedAppUsage + private final IBinder callback; + private final boolean overrideDeadlineExpired; + private final Uri[] mTriggeredContentUris; + private final String[] mTriggeredContentAuthorities; + private final Network network; + + private int stopReason; // Default value of stopReason is REASON_CANCELED + private String debugStopReason; // Human readable stop reason for debugging. + + /** @hide */ + public JobParameters(IBinder callback, int jobId, PersistableBundle extras, + Bundle transientExtras, ClipData clipData, int clipGrantFlags, + boolean overrideDeadlineExpired, Uri[] triggeredContentUris, + String[] triggeredContentAuthorities, Network network) { + this.jobId = jobId; + this.extras = extras; + this.transientExtras = transientExtras; + this.clipData = clipData; + this.clipGrantFlags = clipGrantFlags; + this.callback = callback; + this.overrideDeadlineExpired = overrideDeadlineExpired; + this.mTriggeredContentUris = triggeredContentUris; + this.mTriggeredContentAuthorities = triggeredContentAuthorities; + this.network = network; + } + + /** + * @return The unique id of this job, specified at creation time. + */ + public int getJobId() { + return jobId; + } + + /** + * Reason onStopJob() was called on this job. + * @hide + */ + public int getStopReason() { + return stopReason; + } + + /** + * Reason onStopJob() was called on this job. + * @hide + */ + public String getDebugStopReason() { + return debugStopReason; + } + + /** + * @return The extras you passed in when constructing this job with + * {@link android.app.job.JobInfo.Builder#setExtras(android.os.PersistableBundle)}. This will + * never be null. If you did not set any extras this will be an empty bundle. + */ + public @NonNull PersistableBundle getExtras() { + return extras; + } + + /** + * @return The transient extras you passed in when constructing this job with + * {@link android.app.job.JobInfo.Builder#setTransientExtras(android.os.Bundle)}. This will + * never be null. If you did not set any extras this will be an empty bundle. + */ + public @NonNull Bundle getTransientExtras() { + return transientExtras; + } + + /** + * @return The clip you passed in when constructing this job with + * {@link android.app.job.JobInfo.Builder#setClipData(ClipData, int)}. Will be null + * if it was not set. + */ + public @Nullable ClipData getClipData() { + return clipData; + } + + /** + * @return The clip grant flags you passed in when constructing this job with + * {@link android.app.job.JobInfo.Builder#setClipData(ClipData, int)}. Will be 0 + * if it was not set. + */ + public int getClipGrantFlags() { + return clipGrantFlags; + } + + /** + * For jobs with {@link android.app.job.JobInfo.Builder#setOverrideDeadline(long)} set, this + * provides an easy way to tell whether the job is being executed due to the deadline + * expiring. Note: If the job is running because its deadline expired, it implies that its + * constraints will not be met. + */ + public boolean isOverrideDeadlineExpired() { + return overrideDeadlineExpired; + } + + /** + * For jobs with {@link android.app.job.JobInfo.Builder#addTriggerContentUri} set, this + * reports which URIs have triggered the job. This will be null if either no URIs have + * triggered it (it went off due to a deadline or other reason), or the number of changed + * URIs is too large to report. Whether or not the number of URIs is too large, you can + * always use {@link #getTriggeredContentAuthorities()} to determine whether the job was + * triggered due to any content changes and the authorities they are associated with. + */ + public @Nullable Uri[] getTriggeredContentUris() { + return mTriggeredContentUris; + } + + /** + * For jobs with {@link android.app.job.JobInfo.Builder#addTriggerContentUri} set, this + * reports which content authorities have triggered the job. It will only be null if no + * authorities have triggered it -- that is, the job executed for some other reason, such + * as a deadline expiring. If this is non-null, you can use {@link #getTriggeredContentUris()} + * to retrieve the details of which URIs changed (as long as that has not exceeded the maximum + * number it can reported). + */ + public @Nullable String[] getTriggeredContentAuthorities() { + return mTriggeredContentAuthorities; + } + + /** + * Return the network that should be used to perform any network requests + * for this job. + * <p> + * Devices may have multiple active network connections simultaneously, or + * they may not have a default network route at all. To correctly handle all + * situations like this, your job should always use the network returned by + * this method instead of implicitly using the default network route. + * <p> + * Note that the system may relax the constraints you originally requested, + * such as allowing a {@link JobInfo#NETWORK_TYPE_UNMETERED} job to run over + * a metered network when there is a surplus of metered data available. + * + * @return the network that should be used to perform any network requests + * for this job, or {@code null} if this job didn't set any required + * network type. + * @see JobInfo.Builder#setRequiredNetworkType(int) + */ + public @Nullable Network getNetwork() { + return network; + } + + /** + * Dequeue the next pending {@link JobWorkItem} from these JobParameters associated with their + * currently running job. Calling this method when there is no more work available and all + * previously dequeued work has been completed will result in the system taking care of + * stopping the job for you -- + * you should not call {@link JobService#jobFinished(JobParameters, boolean)} yourself + * (otherwise you risk losing an upcoming JobWorkItem that is being enqueued at the same time). + * + * <p>Once you are done with the {@link JobWorkItem} returned by this method, you must call + * {@link #completeWork(JobWorkItem)} with it to inform the system that you are done + * executing the work. The job will not be finished until all dequeued work has been + * completed. You do not, however, have to complete each returned work item before deqeueing + * the next one -- you can use {@link #dequeueWork()} multiple times before completing + * previous work if you want to process work in parallel, and you can complete the work + * in whatever order you want.</p> + * + * <p>If the job runs to the end of its available time period before all work has been + * completed, it will stop as normal. You should return true from + * {@link JobService#onStopJob(JobParameters)} in order to have the job rescheduled, and by + * doing so any pending as well as remaining uncompleted work will be re-queued + * for the next time the job runs.</p> + * + * <p>This example shows how to construct a JobService that will serially dequeue and + * process work that is available for it:</p> + * + * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/JobWorkService.java + * service} + * + * @return Returns a new {@link JobWorkItem} if there is one pending, otherwise null. + * If null is returned, the system will also stop the job if all work has also been completed. + * (This means that for correct operation, you must always call dequeueWork() after you have + * completed other work, to check either for more work or allow the system to stop the job.) + */ + public @Nullable JobWorkItem dequeueWork() { + try { + return getCallback().dequeueWork(getJobId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Report the completion of executing a {@link JobWorkItem} previously returned by + * {@link #dequeueWork()}. This tells the system you are done with the + * work associated with that item, so it will not be returned again. Note that if this + * is the last work in the queue, completing it here will <em>not</em> finish the overall + * job -- for that to happen, you still need to call {@link #dequeueWork()} + * again. + * + * <p>If you are enqueueing work into a job, you must call this method for each piece + * of work you process. Do <em>not</em> call + * {@link JobService#jobFinished(JobParameters, boolean)} + * or else you can lose work in your queue.</p> + * + * @param work The work you have completed processing, as previously returned by + * {@link #dequeueWork()} + */ + public void completeWork(@NonNull JobWorkItem work) { + try { + if (!getCallback().completeWork(getJobId(), work.getWorkId())) { + throw new IllegalArgumentException("Given work is not active: " + work); + } + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** @hide */ + @UnsupportedAppUsage + public IJobCallback getCallback() { + return IJobCallback.Stub.asInterface(callback); + } + + private JobParameters(Parcel in) { + jobId = in.readInt(); + extras = in.readPersistableBundle(); + transientExtras = in.readBundle(); + if (in.readInt() != 0) { + clipData = ClipData.CREATOR.createFromParcel(in); + clipGrantFlags = in.readInt(); + } else { + clipData = null; + clipGrantFlags = 0; + } + callback = in.readStrongBinder(); + overrideDeadlineExpired = in.readInt() == 1; + mTriggeredContentUris = in.createTypedArray(Uri.CREATOR); + mTriggeredContentAuthorities = in.createStringArray(); + if (in.readInt() != 0) { + network = Network.CREATOR.createFromParcel(in); + } else { + network = null; + } + stopReason = in.readInt(); + debugStopReason = in.readString(); + } + + /** @hide */ + public void setStopReason(int reason, String debugStopReason) { + stopReason = reason; + this.debugStopReason = debugStopReason; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(jobId); + dest.writePersistableBundle(extras); + dest.writeBundle(transientExtras); + if (clipData != null) { + dest.writeInt(1); + clipData.writeToParcel(dest, flags); + dest.writeInt(clipGrantFlags); + } else { + dest.writeInt(0); + } + dest.writeStrongBinder(callback); + dest.writeInt(overrideDeadlineExpired ? 1 : 0); + dest.writeTypedArray(mTriggeredContentUris, flags); + dest.writeStringArray(mTriggeredContentAuthorities); + if (network != null) { + dest.writeInt(1); + network.writeToParcel(dest, flags); + } else { + dest.writeInt(0); + } + dest.writeInt(stopReason); + dest.writeString(debugStopReason); + } + + public static final @android.annotation.NonNull Creator<JobParameters> CREATOR = new Creator<JobParameters>() { + @Override + public JobParameters createFromParcel(Parcel in) { + return new JobParameters(in); + } + + @Override + public JobParameters[] newArray(int size) { + return new JobParameters[size]; + } + }; +} diff --git a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java new file mode 100644 index 000000000000..08b1c2b9f548 --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2014 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 android.app.job; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.annotation.SystemService; +import android.content.ClipData; +import android.content.Context; +import android.os.Bundle; +import android.os.PersistableBundle; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.List; + +/** + * This is an API for scheduling various types of jobs against the framework that will be executed + * in your application's own process. + * <p> + * See {@link android.app.job.JobInfo} for more description of the types of jobs that can be run + * and how to construct them. You will construct these JobInfo objects and pass them to the + * JobScheduler with {@link #schedule(JobInfo)}. When the criteria declared are met, the + * system will execute this job on your application's {@link android.app.job.JobService}. + * You identify the service component that implements the logic for your job when you + * construct the JobInfo using + * {@link android.app.job.JobInfo.Builder#JobInfo.Builder(int,android.content.ComponentName)}. + * </p> + * <p> + * The framework will be intelligent about when it executes jobs, and attempt to batch + * and defer them as much as possible. Typically if you don't specify a deadline on a job, it + * can be run at any moment depending on the current state of the JobScheduler's internal queue. + * <p> + * While a job is running, the system holds a wakelock on behalf of your app. For this reason, + * you do not need to take any action to guarantee that the device stays awake for the + * duration of the job. + * </p> + * <p>You do not + * instantiate this class directly; instead, retrieve it through + * {@link android.content.Context#getSystemService + * Context.getSystemService(Context.JOB_SCHEDULER_SERVICE)}. + */ +@SystemService(Context.JOB_SCHEDULER_SERVICE) +public abstract class JobScheduler { + /** @hide */ + @IntDef(prefix = { "RESULT_" }, value = { + RESULT_FAILURE, + RESULT_SUCCESS, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Result {} + + /** + * Returned from {@link #schedule(JobInfo)} when an invalid parameter was supplied. This can occur + * if the run-time for your job is too short, or perhaps the system can't resolve the + * requisite {@link JobService} in your package. + */ + public static final int RESULT_FAILURE = 0; + /** + * Returned from {@link #schedule(JobInfo)} if this job has been successfully scheduled. + */ + public static final int RESULT_SUCCESS = 1; + + /** + * Schedule a job to be executed. Will replace any currently scheduled job with the same + * ID with the new information in the {@link JobInfo}. If a job with the given ID is currently + * running, it will be stopped. + * + * @param job The job you wish scheduled. See + * {@link android.app.job.JobInfo.Builder JobInfo.Builder} for more detail on the sorts of jobs + * you can schedule. + * @return the result of the schedule request. + */ + public abstract @Result int schedule(@NonNull JobInfo job); + + /** + * Similar to {@link #schedule}, but allows you to enqueue work for a new <em>or existing</em> + * job. If a job with the same ID is already scheduled, it will be replaced with the + * new {@link JobInfo}, but any previously enqueued work will remain and be dispatched the + * next time it runs. If a job with the same ID is already running, the new work will be + * enqueued for it. + * + * <p>The work you enqueue is later retrieved through + * {@link JobParameters#dequeueWork() JobParameters.dequeueWork}. Be sure to see there + * about how to process work; the act of enqueueing work changes how you should handle the + * overall lifecycle of an executing job.</p> + * + * <p>It is strongly encouraged that you use the same {@link JobInfo} for all work you + * enqueue. This will allow the system to optimally schedule work along with any pending + * and/or currently running work. If the JobInfo changes from the last time the job was + * enqueued, the system will need to update the associated JobInfo, which can cause a disruption + * in execution. In particular, this can result in any currently running job that is processing + * previous work to be stopped and restarted with the new JobInfo.</p> + * + * <p>It is recommended that you avoid using + * {@link JobInfo.Builder#setExtras(PersistableBundle)} or + * {@link JobInfo.Builder#setTransientExtras(Bundle)} with a JobInfo you are using to + * enqueue work. The system will try to compare these extras with the previous JobInfo, + * but there are situations where it may get this wrong and count the JobInfo as changing. + * (That said, you should be relatively safe with a simple set of consistent data in these + * fields.) You should never use {@link JobInfo.Builder#setClipData(ClipData, int)} with + * work you are enqueue, since currently this will always be treated as a different JobInfo, + * even if the ClipData contents are exactly the same.</p> + * + * @param job The job you wish to enqueue work for. See + * {@link android.app.job.JobInfo.Builder JobInfo.Builder} for more detail on the sorts of jobs + * you can schedule. + * @param work New work to enqueue. This will be available later when the job starts running. + * @return the result of the enqueue request. + */ + public abstract @Result int enqueue(@NonNull JobInfo job, @NonNull JobWorkItem work); + + /** + * + * @param job The job to be scheduled. + * @param packageName The package on behalf of which the job is to be scheduled. This will be + * used to track battery usage and appIdleState. + * @param userId User on behalf of whom this job is to be scheduled. + * @param tag Debugging tag for dumps associated with this job (instead of the service class) + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS) + public abstract @Result int scheduleAsPackage(@NonNull JobInfo job, @NonNull String packageName, + int userId, String tag); + + /** + * Cancel the specified job. If the job is currently executing, it is stopped + * immediately and the return value from its {@link JobService#onStopJob(JobParameters)} + * method is ignored. + * + * @param jobId unique identifier for the job to be canceled, as supplied to + * {@link JobInfo.Builder#JobInfo.Builder(int, android.content.ComponentName) + * JobInfo.Builder(int, android.content.ComponentName)}. + */ + public abstract void cancel(int jobId); + + /** + * Cancel <em>all</em> jobs that have been scheduled by the calling application. + */ + public abstract void cancelAll(); + + /** + * Retrieve all jobs that have been scheduled by the calling application. + * + * @return a list of all of the app's scheduled jobs. This includes jobs that are + * currently started as well as those that are still waiting to run. + */ + public abstract @NonNull List<JobInfo> getAllPendingJobs(); + + /** + * Look up the description of a scheduled job. + * + * @return The {@link JobInfo} description of the given scheduled job, or {@code null} + * if the supplied job ID does not correspond to any job. + */ + public abstract @Nullable JobInfo getPendingJob(int jobId); + + /** + * <b>For internal system callers only!</b> + * Returns a list of all currently-executing jobs. + * @hide + */ + public abstract List<JobInfo> getStartedJobs(); + + /** + * <b>For internal system callers only!</b> + * Returns a snapshot of the state of all jobs known to the system. + * + * <p class="note">This is a slow operation, so it should be called sparingly. + * @hide + */ + public abstract List<JobSnapshot> getAllJobSnapshots(); +}
\ No newline at end of file diff --git a/apex/jobscheduler/framework/java/android/app/job/JobSchedulerFrameworkInitializer.java b/apex/jobscheduler/framework/java/android/app/job/JobSchedulerFrameworkInitializer.java new file mode 100644 index 000000000000..c90b8728bf4a --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/JobSchedulerFrameworkInitializer.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2019 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 android.app.job; + +import android.app.JobSchedulerImpl; +import android.app.SystemServiceRegistry; +import android.content.Context; +import android.os.BatteryStats; + +/** + * This class needs to be pre-loaded by zygote. This is where the job scheduler service wrapper + * is registered. + * + * @hide + */ +public class JobSchedulerFrameworkInitializer { + static { + SystemServiceRegistry.registerStaticService( + Context.JOB_SCHEDULER_SERVICE, JobScheduler.class, + (b) -> new JobSchedulerImpl(IJobScheduler.Stub.asInterface(b))); + + BatteryStats.setJobStopReasons(JobParameters.JOB_STOP_REASON_CODES, + JobParameters::getReasonName); + } +} diff --git a/apex/jobscheduler/framework/java/android/app/job/JobService.java b/apex/jobscheduler/framework/java/android/app/job/JobService.java new file mode 100644 index 000000000000..61afadab9b0c --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/JobService.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2014 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 android.app.job; + +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; + +/** + * <p>Entry point for the callback from the {@link android.app.job.JobScheduler}.</p> + * <p>This is the base class that handles asynchronous requests that were previously scheduled. You + * are responsible for overriding {@link JobService#onStartJob(JobParameters)}, which is where + * you will implement your job logic.</p> + * <p>This service executes each incoming job on a {@link android.os.Handler} running on your + * application's main thread. This means that you <b>must</b> offload your execution logic to + * another thread/handler/{@link android.os.AsyncTask} of your choosing. Not doing so will result + * in blocking any future callbacks from the JobManager - specifically + * {@link #onStopJob(android.app.job.JobParameters)}, which is meant to inform you that the + * scheduling requirements are no longer being met.</p> + */ +public abstract class JobService extends Service { + private static final String TAG = "JobService"; + + /** + * Job services must be protected with this permission: + * + * <pre class="prettyprint"> + * <service android:name="MyJobService" + * android:permission="android.permission.BIND_JOB_SERVICE" > + * ... + * </service> + * </pre> + * + * <p>If a job service is declared in the manifest but not protected with this + * permission, that service will be ignored by the system. + */ + public static final String PERMISSION_BIND = + "android.permission.BIND_JOB_SERVICE"; + + private JobServiceEngine mEngine; + + /** @hide */ + public final IBinder onBind(Intent intent) { + if (mEngine == null) { + mEngine = new JobServiceEngine(this) { + @Override + public boolean onStartJob(JobParameters params) { + return JobService.this.onStartJob(params); + } + + @Override + public boolean onStopJob(JobParameters params) { + return JobService.this.onStopJob(params); + } + }; + } + return mEngine.getBinder(); + } + + /** + * Call this to inform the JobScheduler that the job has finished its work. When the + * system receives this message, it releases the wakelock being held for the job. + * <p> + * You can request that the job be scheduled again by passing {@code true} as + * the <code>wantsReschedule</code> parameter. This will apply back-off policy + * for the job; this policy can be adjusted through the + * {@link android.app.job.JobInfo.Builder#setBackoffCriteria(long, int)} method + * when the job is originally scheduled. The job's initial + * requirements are preserved when jobs are rescheduled, regardless of backed-off + * policy. + * <p class="note"> + * A job running while the device is dozing will not be rescheduled with the normal back-off + * policy. Instead, the job will be re-added to the queue and executed again during + * a future idle maintenance window. + * </p> + * + * @param params The parameters identifying this job, as supplied to + * the job in the {@link #onStartJob(JobParameters)} callback. + * @param wantsReschedule {@code true} if this job should be rescheduled according + * to the back-off criteria specified when it was first scheduled; {@code false} + * otherwise. + */ + public final void jobFinished(JobParameters params, boolean wantsReschedule) { + mEngine.jobFinished(params, wantsReschedule); + } + + /** + * Called to indicate that the job has begun executing. Override this method with the + * logic for your job. Like all other component lifecycle callbacks, this method executes + * on your application's main thread. + * <p> + * Return {@code true} from this method if your job needs to continue running. If you + * do this, the job remains active until you call + * {@link #jobFinished(JobParameters, boolean)} to tell the system that it has completed + * its work, or until the job's required constraints are no longer satisfied. For + * example, if the job was scheduled using + * {@link JobInfo.Builder#setRequiresCharging(boolean) setRequiresCharging(true)}, + * it will be immediately halted by the system if the user unplugs the device from power, + * the job's {@link #onStopJob(JobParameters)} callback will be invoked, and the app + * will be expected to shut down all ongoing work connected with that job. + * <p> + * The system holds a wakelock on behalf of your app as long as your job is executing. + * This wakelock is acquired before this method is invoked, and is not released until either + * you call {@link #jobFinished(JobParameters, boolean)}, or after the system invokes + * {@link #onStopJob(JobParameters)} to notify your job that it is being shut down + * prematurely. + * <p> + * Returning {@code false} from this method means your job is already finished. The + * system's wakelock for the job will be released, and {@link #onStopJob(JobParameters)} + * will not be invoked. + * + * @param params Parameters specifying info about this job, including the optional + * extras configured with {@link JobInfo.Builder#setExtras(android.os.PersistableBundle). + * This object serves to identify this specific running job instance when calling + * {@link #jobFinished(JobParameters, boolean)}. + * @return {@code true} if your service will continue running, using a separate thread + * when appropriate. {@code false} means that this job has completed its work. + */ + public abstract boolean onStartJob(JobParameters params); + + /** + * This method is called if the system has determined that you must stop execution of your job + * even before you've had a chance to call {@link #jobFinished(JobParameters, boolean)}. + * + * <p>This will happen if the requirements specified at schedule time are no longer met. For + * example you may have requested WiFi with + * {@link android.app.job.JobInfo.Builder#setRequiredNetworkType(int)}, yet while your + * job was executing the user toggled WiFi. Another example is if you had specified + * {@link android.app.job.JobInfo.Builder#setRequiresDeviceIdle(boolean)}, and the phone left its + * idle maintenance window. You are solely responsible for the behavior of your application + * upon receipt of this message; your app will likely start to misbehave if you ignore it. + * <p> + * Once this method returns, the system releases the wakelock that it is holding on + * behalf of the job.</p> + * + * @param params The parameters identifying this job, as supplied to + * the job in the {@link #onStartJob(JobParameters)} callback. + * @return {@code true} to indicate to the JobManager whether you'd like to reschedule + * this job based on the retry criteria provided at job creation-time; or {@code false} + * to end the job entirely. Regardless of the value returned, your job must stop executing. + */ + public abstract boolean onStopJob(JobParameters params); +} diff --git a/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java b/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java new file mode 100644 index 000000000000..ab94da843635 --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2014 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 android.app.job; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.util.Log; + +import com.android.internal.annotations.GuardedBy; + +import java.lang.ref.WeakReference; + +/** + * Helper for implementing a {@link android.app.Service} that interacts with + * {@link JobScheduler}. This is not intended for use by regular applications, but + * allows frameworks built on top of the platform to create their own + * {@link android.app.Service} that interact with {@link JobScheduler} as well as + * add in additional functionality. If you just want to execute jobs normally, you + * should instead be looking at {@link JobService}. + */ +public abstract class JobServiceEngine { + private static final String TAG = "JobServiceEngine"; + + /** + * Identifier for a message that will result in a call to + * {@link #onStartJob(android.app.job.JobParameters)}. + */ + private static final int MSG_EXECUTE_JOB = 0; + /** + * Message that will result in a call to {@link #onStopJob(android.app.job.JobParameters)}. + */ + private static final int MSG_STOP_JOB = 1; + /** + * Message that the client has completed execution of this job. + */ + private static final int MSG_JOB_FINISHED = 2; + + private final IJobService mBinder; + + /** + * Handler we post jobs to. Responsible for calling into the client logic, and handling the + * callback to the system. + */ + JobHandler mHandler; + + static final class JobInterface extends IJobService.Stub { + final WeakReference<JobServiceEngine> mService; + + JobInterface(JobServiceEngine service) { + mService = new WeakReference<>(service); + } + + @Override + public void startJob(JobParameters jobParams) throws RemoteException { + JobServiceEngine service = mService.get(); + if (service != null) { + Message m = Message.obtain(service.mHandler, MSG_EXECUTE_JOB, jobParams); + m.sendToTarget(); + } + } + + @Override + public void stopJob(JobParameters jobParams) throws RemoteException { + JobServiceEngine service = mService.get(); + if (service != null) { + Message m = Message.obtain(service.mHandler, MSG_STOP_JOB, jobParams); + m.sendToTarget(); + } + } + } + + /** + * Runs on application's main thread - callbacks are meant to offboard work to some other + * (app-specified) mechanism. + * @hide + */ + class JobHandler extends Handler { + JobHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + final JobParameters params = (JobParameters) msg.obj; + switch (msg.what) { + case MSG_EXECUTE_JOB: + try { + boolean workOngoing = JobServiceEngine.this.onStartJob(params); + ackStartMessage(params, workOngoing); + } catch (Exception e) { + Log.e(TAG, "Error while executing job: " + params.getJobId()); + throw new RuntimeException(e); + } + break; + case MSG_STOP_JOB: + try { + boolean ret = JobServiceEngine.this.onStopJob(params); + ackStopMessage(params, ret); + } catch (Exception e) { + Log.e(TAG, "Application unable to handle onStopJob.", e); + throw new RuntimeException(e); + } + break; + case MSG_JOB_FINISHED: + final boolean needsReschedule = (msg.arg2 == 1); + IJobCallback callback = params.getCallback(); + if (callback != null) { + try { + callback.jobFinished(params.getJobId(), needsReschedule); + } catch (RemoteException e) { + Log.e(TAG, "Error reporting job finish to system: binder has gone" + + "away."); + } + } else { + Log.e(TAG, "finishJob() called for a nonexistent job id."); + } + break; + default: + Log.e(TAG, "Unrecognised message received."); + break; + } + } + + private void ackStartMessage(JobParameters params, boolean workOngoing) { + final IJobCallback callback = params.getCallback(); + final int jobId = params.getJobId(); + if (callback != null) { + try { + callback.acknowledgeStartMessage(jobId, workOngoing); + } catch(RemoteException e) { + Log.e(TAG, "System unreachable for starting job."); + } + } else { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Attempting to ack a job that has already been processed."); + } + } + } + + private void ackStopMessage(JobParameters params, boolean reschedule) { + final IJobCallback callback = params.getCallback(); + final int jobId = params.getJobId(); + if (callback != null) { + try { + callback.acknowledgeStopMessage(jobId, reschedule); + } catch(RemoteException e) { + Log.e(TAG, "System unreachable for stopping job."); + } + } else { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Attempting to ack a job that has already been processed."); + } + } + } + } + + /** + * Create a new engine, ready for use. + * + * @param service The {@link Service} that is creating this engine and in which it will run. + */ + public JobServiceEngine(Service service) { + mBinder = new JobInterface(this); + mHandler = new JobHandler(service.getMainLooper()); + } + + /** + * Retrieve the engine's IPC interface that should be returned by + * {@link Service#onBind(Intent)}. + */ + public final IBinder getBinder() { + return mBinder.asBinder(); + } + + /** + * Engine's report that a job has started. See + * {@link JobService#onStartJob(JobParameters) JobService.onStartJob} for more information. + */ + public abstract boolean onStartJob(JobParameters params); + + /** + * Engine's report that a job has stopped. See + * {@link JobService#onStopJob(JobParameters) JobService.onStopJob} for more information. + */ + public abstract boolean onStopJob(JobParameters params); + + /** + * Call in to engine to report that a job has finished executing. See + * {@link JobService#jobFinished(JobParameters, boolean)} JobService.jobFinished} for more + * information. + */ + public void jobFinished(JobParameters params, boolean needsReschedule) { + if (params == null) { + throw new NullPointerException("params"); + } + Message m = Message.obtain(mHandler, MSG_JOB_FINISHED, params); + m.arg2 = needsReschedule ? 1 : 0; + m.sendToTarget(); + } +}
\ No newline at end of file diff --git a/apex/jobscheduler/framework/java/android/app/job/JobSnapshot.aidl b/apex/jobscheduler/framework/java/android/app/job/JobSnapshot.aidl new file mode 100644 index 000000000000..d40f4e39ea2e --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/JobSnapshot.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (C) 2018 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 android.app.job; + +parcelable JobSnapshot; diff --git a/apex/jobscheduler/framework/java/android/app/job/JobSnapshot.java b/apex/jobscheduler/framework/java/android/app/job/JobSnapshot.java new file mode 100644 index 000000000000..2c58908a6064 --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/JobSnapshot.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2018 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 android.app.job; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Current-state snapshot of a scheduled job. These snapshots are not used in apps; + * they exist only within the system process across the local call surface where JobStatus + * is not directly accessible at build time. + * + * Constraints that the underlying job does not require are always reported as + * being currently satisfied. + * @hide + */ +public class JobSnapshot implements Parcelable { + private final JobInfo mJob; + private final int mSatisfiedConstraints; + private final boolean mIsRunnable; + + public JobSnapshot(JobInfo info, int satisfiedMask, boolean runnable) { + mJob = info; + mSatisfiedConstraints = satisfiedMask; + mIsRunnable = runnable; + } + + public JobSnapshot(Parcel in) { + mJob = JobInfo.CREATOR.createFromParcel(in); + mSatisfiedConstraints = in.readInt(); + mIsRunnable = in.readBoolean(); + } + + private boolean satisfied(int flag) { + return (mSatisfiedConstraints & flag) != 0; + } + + /** + * Returning JobInfo bound to this snapshot + * @return JobInfo of this snapshot + */ + public JobInfo getJobInfo() { + return mJob; + } + + /** + * Is this job actually runnable at this moment? + */ + public boolean isRunnable() { + return mIsRunnable; + } + + /** + * @see JobInfo.Builder#setRequiresCharging(boolean) + */ + public boolean isChargingSatisfied() { + return !mJob.isRequireCharging() + || satisfied(JobInfo.CONSTRAINT_FLAG_CHARGING); + } + + /** + * @see JobInfo.Builder#setRequiresBatteryNotLow(boolean) + */ + public boolean isBatteryNotLowSatisfied() { + return !mJob.isRequireBatteryNotLow() + || satisfied(JobInfo.CONSTRAINT_FLAG_BATTERY_NOT_LOW); + } + + /** + * @see JobInfo.Builder#setRequiresDeviceIdle(boolean) + */ + public boolean isRequireDeviceIdleSatisfied() { + return !mJob.isRequireDeviceIdle() + || satisfied(JobInfo.CONSTRAINT_FLAG_DEVICE_IDLE); + } + + public boolean isRequireStorageNotLowSatisfied() { + return !mJob.isRequireStorageNotLow() + || satisfied(JobInfo.CONSTRAINT_FLAG_STORAGE_NOT_LOW); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + mJob.writeToParcel(out, flags); + out.writeInt(mSatisfiedConstraints); + out.writeBoolean(mIsRunnable); + } + + public static final @android.annotation.NonNull Creator<JobSnapshot> CREATOR = new Creator<JobSnapshot>() { + @Override + public JobSnapshot createFromParcel(Parcel in) { + return new JobSnapshot(in); + } + + @Override + public JobSnapshot[] newArray(int size) { + return new JobSnapshot[size]; + } + }; +} diff --git a/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.aidl b/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.aidl new file mode 100644 index 000000000000..e8fe47d07865 --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.aidl @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2017 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 android.app.job; + +/** @hide */ +parcelable JobWorkItem; diff --git a/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.java b/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.java new file mode 100644 index 000000000000..c6631fa76494 --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.java @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2017 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 android.app.job; + +import static android.app.job.JobInfo.NETWORK_BYTES_UNKNOWN; + +import android.annotation.BytesLong; +import android.annotation.UnsupportedAppUsage; +import android.content.Intent; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * A unit of work that can be enqueued for a job using + * {@link JobScheduler#enqueue JobScheduler.enqueue}. See + * {@link JobParameters#dequeueWork() JobParameters.dequeueWork} for more details. + */ +final public class JobWorkItem implements Parcelable { + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) + final Intent mIntent; + final long mNetworkDownloadBytes; + final long mNetworkUploadBytes; + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) + int mDeliveryCount; + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) + int mWorkId; + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) + Object mGrants; + + /** + * Create a new piece of work, which can be submitted to + * {@link JobScheduler#enqueue JobScheduler.enqueue}. + * + * @param intent The general Intent describing this work. + */ + public JobWorkItem(Intent intent) { + mIntent = intent; + mNetworkDownloadBytes = NETWORK_BYTES_UNKNOWN; + mNetworkUploadBytes = NETWORK_BYTES_UNKNOWN; + } + + /** + * Create a new piece of work, which can be submitted to + * {@link JobScheduler#enqueue JobScheduler.enqueue}. + * <p> + * See {@link JobInfo.Builder#setEstimatedNetworkBytes(long, long)} for + * details about how to estimate network traffic. + * + * @param intent The general Intent describing this work. + * @param downloadBytes The estimated size of network traffic that will be + * downloaded by this job work item, in bytes. + * @param uploadBytes The estimated size of network traffic that will be + * uploaded by this job work item, in bytes. + */ + public JobWorkItem(Intent intent, @BytesLong long downloadBytes, @BytesLong long uploadBytes) { + mIntent = intent; + mNetworkDownloadBytes = downloadBytes; + mNetworkUploadBytes = uploadBytes; + } + + /** + * Return the Intent associated with this work. + */ + public Intent getIntent() { + return mIntent; + } + + /** + * Return the estimated size of download traffic that will be performed by + * this job, in bytes. + * + * @return Estimated size of download traffic, or + * {@link JobInfo#NETWORK_BYTES_UNKNOWN} when unknown. + */ + public @BytesLong long getEstimatedNetworkDownloadBytes() { + return mNetworkDownloadBytes; + } + + /** + * Return the estimated size of upload traffic that will be performed by + * this job work item, in bytes. + * + * @return Estimated size of upload traffic, or + * {@link JobInfo#NETWORK_BYTES_UNKNOWN} when unknown. + */ + public @BytesLong long getEstimatedNetworkUploadBytes() { + return mNetworkUploadBytes; + } + + /** + * Return the count of the number of times this work item has been delivered + * to the job. The value will be > 1 if it has been redelivered because the job + * was stopped or crashed while it had previously been delivered but before the + * job had called {@link JobParameters#completeWork JobParameters.completeWork} for it. + */ + public int getDeliveryCount() { + return mDeliveryCount; + } + + /** + * @hide + */ + public void bumpDeliveryCount() { + mDeliveryCount++; + } + + /** + * @hide + */ + public void setWorkId(int id) { + mWorkId = id; + } + + /** + * @hide + */ + public int getWorkId() { + return mWorkId; + } + + /** + * @hide + */ + public void setGrants(Object grants) { + mGrants = grants; + } + + /** + * @hide + */ + public Object getGrants() { + return mGrants; + } + + public String toString() { + StringBuilder sb = new StringBuilder(64); + sb.append("JobWorkItem{id="); + sb.append(mWorkId); + sb.append(" intent="); + sb.append(mIntent); + if (mNetworkDownloadBytes != NETWORK_BYTES_UNKNOWN) { + sb.append(" downloadBytes="); + sb.append(mNetworkDownloadBytes); + } + if (mNetworkUploadBytes != NETWORK_BYTES_UNKNOWN) { + sb.append(" uploadBytes="); + sb.append(mNetworkUploadBytes); + } + if (mDeliveryCount != 0) { + sb.append(" dcount="); + sb.append(mDeliveryCount); + } + sb.append("}"); + return sb.toString(); + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel out, int flags) { + if (mIntent != null) { + out.writeInt(1); + mIntent.writeToParcel(out, 0); + } else { + out.writeInt(0); + } + out.writeLong(mNetworkDownloadBytes); + out.writeLong(mNetworkUploadBytes); + out.writeInt(mDeliveryCount); + out.writeInt(mWorkId); + } + + public static final @android.annotation.NonNull Parcelable.Creator<JobWorkItem> CREATOR + = new Parcelable.Creator<JobWorkItem>() { + public JobWorkItem createFromParcel(Parcel in) { + return new JobWorkItem(in); + } + + public JobWorkItem[] newArray(int size) { + return new JobWorkItem[size]; + } + }; + + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) + JobWorkItem(Parcel in) { + if (in.readInt() != 0) { + mIntent = Intent.CREATOR.createFromParcel(in); + } else { + mIntent = null; + } + mNetworkDownloadBytes = in.readLong(); + mNetworkUploadBytes = in.readLong(); + mDeliveryCount = in.readInt(); + mWorkId = in.readInt(); + } +} diff --git a/apex/jobscheduler/framework/java/android/os/DeviceIdleManager.java b/apex/jobscheduler/framework/java/android/os/DeviceIdleManager.java new file mode 100644 index 000000000000..9039f921b3ba --- /dev/null +++ b/apex/jobscheduler/framework/java/android/os/DeviceIdleManager.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2018 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 android.os; + +import android.annotation.NonNull; +import android.annotation.SystemService; +import android.annotation.TestApi; +import android.content.Context; + +/** + * Access to the service that keeps track of device idleness and drives low power mode based on + * that. + * + * @hide + */ +@TestApi +@SystemService(Context.DEVICE_IDLE_CONTROLLER) +public class DeviceIdleManager { + private final Context mContext; + private final IDeviceIdleController mService; + + /** + * @hide + */ + public DeviceIdleManager(@NonNull Context context, @NonNull IDeviceIdleController service) { + mContext = context; + mService = service; + } + + /** + * @return package names the system has white-listed to opt out of power save restrictions, + * except for device idle mode. + */ + public @NonNull String[] getSystemPowerWhitelistExceptIdle() { + try { + return mService.getSystemPowerWhitelistExceptIdle(); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + return new String[0]; + } + } + + /** + * @return package names the system has white-listed to opt out of power save restrictions for + * all modes. + */ + public @NonNull String[] getSystemPowerWhitelist() { + try { + return mService.getSystemPowerWhitelist(); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + return new String[0]; + } + } +} diff --git a/apex/jobscheduler/framework/java/android/os/IDeviceIdleController.aidl b/apex/jobscheduler/framework/java/android/os/IDeviceIdleController.aidl new file mode 100644 index 000000000000..9d5becbf77cd --- /dev/null +++ b/apex/jobscheduler/framework/java/android/os/IDeviceIdleController.aidl @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2015, 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 android.os; + +import android.os.UserHandle; + +/** @hide */ +interface IDeviceIdleController { + void addPowerSaveWhitelistApp(String name); + void removePowerSaveWhitelistApp(String name); + /* Removes an app from the system whitelist. Calling restoreSystemPowerWhitelistApp will add + the app back into the system whitelist */ + void removeSystemPowerWhitelistApp(String name); + void restoreSystemPowerWhitelistApp(String name); + String[] getRemovedSystemPowerWhitelistApps(); + String[] getSystemPowerWhitelistExceptIdle(); + String[] getSystemPowerWhitelist(); + String[] getUserPowerWhitelist(); + String[] getFullPowerWhitelistExceptIdle(); + String[] getFullPowerWhitelist(); + int[] getAppIdWhitelistExceptIdle(); + int[] getAppIdWhitelist(); + int[] getAppIdUserWhitelist(); + int[] getAppIdTempWhitelist(); + boolean isPowerSaveWhitelistExceptIdleApp(String name); + boolean isPowerSaveWhitelistApp(String name); + @UnsupportedAppUsage + void addPowerSaveTempWhitelistApp(String name, long duration, int userId, String reason); + long addPowerSaveTempWhitelistAppForMms(String name, int userId, String reason); + long addPowerSaveTempWhitelistAppForSms(String name, int userId, String reason); + void exitIdle(String reason); + int setPreIdleTimeoutMode(int Mode); + void resetPreIdleTimeoutMode(); +} diff --git a/apex/jobscheduler/framework/java/com/android/server/DeviceIdleInternal.java b/apex/jobscheduler/framework/java/com/android/server/DeviceIdleInternal.java new file mode 100644 index 000000000000..127324936e09 --- /dev/null +++ b/apex/jobscheduler/framework/java/com/android/server/DeviceIdleInternal.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2019 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; + +import com.android.server.deviceidle.IDeviceIdleConstraint; + +public interface DeviceIdleInternal { + void onConstraintStateChanged(IDeviceIdleConstraint constraint, boolean active); + + void registerDeviceIdleConstraint(IDeviceIdleConstraint constraint, String name, + @IDeviceIdleConstraint.MinimumState int minState); + + void unregisterDeviceIdleConstraint(IDeviceIdleConstraint constraint); + + void exitIdle(String reason); + + // duration in milliseconds + void addPowerSaveTempWhitelistApp(int callingUid, String packageName, + long duration, int userId, boolean sync, String reason); + + // duration in milliseconds + void addPowerSaveTempWhitelistAppDirect(int uid, long duration, boolean sync, + String reason); + + // duration in milliseconds + long getNotificationWhitelistDuration(); + + void setJobsActive(boolean active); + + // Up-call from alarm manager. + void setAlarmsActive(boolean active); + + boolean isAppOnWhitelist(int appid); + + int[] getPowerSaveWhitelistUserAppIds(); + + int[] getPowerSaveTempWhitelistAppIds(); +} diff --git a/apex/jobscheduler/framework/java/com/android/server/deviceidle/ConstraintController.java b/apex/jobscheduler/framework/java/com/android/server/deviceidle/ConstraintController.java new file mode 100644 index 000000000000..6d52f7188d99 --- /dev/null +++ b/apex/jobscheduler/framework/java/com/android/server/deviceidle/ConstraintController.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2018 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.deviceidle; + +/** + * Device idle constraints for a specific form factor or use-case. + */ +public interface ConstraintController { + /** + * Begin any general continuing work and register all constraints. + */ + void start(); + + /** + * Unregister all constraints and stop any general work. + */ + void stop(); +} diff --git a/apex/jobscheduler/framework/java/com/android/server/deviceidle/IDeviceIdleConstraint.java b/apex/jobscheduler/framework/java/com/android/server/deviceidle/IDeviceIdleConstraint.java new file mode 100644 index 000000000000..f1f957307716 --- /dev/null +++ b/apex/jobscheduler/framework/java/com/android/server/deviceidle/IDeviceIdleConstraint.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2018 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.deviceidle; + +import android.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Implemented by OEM and/or Form Factor. System ones are built into the + * image regardless of build flavour but may still be switched off at run time. + * Individual feature flags at build time control which are used. We may + * also explore a local override for quick testing. + */ +public interface IDeviceIdleConstraint { + + /** + * A state for this constraint to block descent from. + * + * <p>These states are a subset of the states in DeviceIdleController that make sense for + * constraints to be able to block on. For example, {@link #SENSING_OR_ABOVE} clearly has + * defined "above" and "below" states. However, a hypothetical {@code QUICK_DOZE_OR_ABOVE} + * state would not have clear semantics as to what transitions should be blocked and which + * should be allowed. + */ + @IntDef(flag = false, value = { + ACTIVE, + SENSING_OR_ABOVE, + }) + @Retention(RetentionPolicy.SOURCE) + @interface MinimumState {} + + int ACTIVE = 0; + int SENSING_OR_ABOVE = 1; + + /** + * Begin tracking events for this constraint. + * + * <p>The device idle controller has reached a point where it is waiting for the all-clear + * from this tracker (possibly among others) in order to continue with progression into + * idle state. It will not proceed until one of the following happens: + * <ul> + * <li>The constraint reports inactive with {@code .setActive(false)}.</li> + * <li>The constraint is unregistered with {@code .unregisterDeviceIdleConstraint(this)}.</li> + * <li>A transition timeout in DeviceIdleController fires. + * </ul> + */ + void startMonitoring(); + + /** Stop checking for new events and do not call into LocalService with updates any more. */ + void stopMonitoring(); +} diff --git a/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java b/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java new file mode 100644 index 000000000000..eefb9fafd286 --- /dev/null +++ b/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java @@ -0,0 +1,119 @@ +/* + * 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; + +import android.app.job.JobInfo; +import android.util.proto.ProtoOutputStream; + +import java.util.List; + +/** + * JobScheduler local system service interface. + * {@hide} Only for use within the system server. + */ +public interface JobSchedulerInternal { + + /** + * Returns a list of pending jobs scheduled by the system service. + */ + List<JobInfo> getSystemScheduledPendingJobs(); + + /** + * Cancel the jobs for a given uid (e.g. when app data is cleared) + */ + void cancelJobsForUid(int uid, String reason); + + /** + * These are for activity manager to communicate to use what is currently performing backups. + */ + void addBackingUpUid(int uid); + void removeBackingUpUid(int uid); + void clearAllBackingUpUids(); + + /** + * The user has started interacting with the app. Take any appropriate action. + */ + void reportAppUsage(String packageName, int userId); + + /** + * Report a snapshot of sync-related jobs back to the sync manager + */ + JobStorePersistStats getPersistStats(); + + /** + * Stats about the first load after boot and the most recent save. + */ + public class JobStorePersistStats { + public int countAllJobsLoaded = -1; + public int countSystemServerJobsLoaded = -1; + public int countSystemSyncManagerJobsLoaded = -1; + + public int countAllJobsSaved = -1; + public int countSystemServerJobsSaved = -1; + public int countSystemSyncManagerJobsSaved = -1; + + public JobStorePersistStats() { + } + + public JobStorePersistStats(JobStorePersistStats source) { + countAllJobsLoaded = source.countAllJobsLoaded; + countSystemServerJobsLoaded = source.countSystemServerJobsLoaded; + countSystemSyncManagerJobsLoaded = source.countSystemSyncManagerJobsLoaded; + + countAllJobsSaved = source.countAllJobsSaved; + countSystemServerJobsSaved = source.countSystemServerJobsSaved; + countSystemSyncManagerJobsSaved = source.countSystemSyncManagerJobsSaved; + } + + @Override + public String toString() { + return "FirstLoad: " + + countAllJobsLoaded + "/" + + countSystemServerJobsLoaded + "/" + + countSystemSyncManagerJobsLoaded + + " LastSave: " + + countAllJobsSaved + "/" + + countSystemServerJobsSaved + "/" + + countSystemSyncManagerJobsSaved; + } + + /** + * Write the persist stats to the specified field. + */ + public void writeToProto(ProtoOutputStream proto, long fieldId) { + final long token = proto.start(fieldId); + + final long flToken = proto.start(JobStorePersistStatsProto.FIRST_LOAD); + proto.write(JobStorePersistStatsProto.Stats.NUM_TOTAL_JOBS, countAllJobsLoaded); + proto.write(JobStorePersistStatsProto.Stats.NUM_SYSTEM_SERVER_JOBS, + countSystemServerJobsLoaded); + proto.write(JobStorePersistStatsProto.Stats.NUM_SYSTEM_SYNC_MANAGER_JOBS, + countSystemSyncManagerJobsLoaded); + proto.end(flToken); + + final long lsToken = proto.start(JobStorePersistStatsProto.LAST_SAVE); + proto.write(JobStorePersistStatsProto.Stats.NUM_TOTAL_JOBS, countAllJobsSaved); + proto.write(JobStorePersistStatsProto.Stats.NUM_SYSTEM_SERVER_JOBS, + countSystemServerJobsSaved); + proto.write(JobStorePersistStatsProto.Stats.NUM_SYSTEM_SYNC_MANAGER_JOBS, + countSystemSyncManagerJobsSaved); + proto.end(lsToken); + + proto.end(token); + } + } +} diff --git a/apex/jobscheduler/service/Android.bp b/apex/jobscheduler/service/Android.bp new file mode 100644 index 000000000000..ca6dc45a356a --- /dev/null +++ b/apex/jobscheduler/service/Android.bp @@ -0,0 +1,15 @@ +// Job Scheduler Service jar, which will eventually be put in the jobscheduler mainline apex. +// jobscheduler-service needs to be added to PRODUCT_SYSTEM_SERVER_JARS. +java_library { + name: "jobscheduler-service", + installable: true, + + srcs: [ + "java/**/*.java", + ], + + libs: [ + "framework", + "services.core", + ], +} diff --git a/apex/jobscheduler/service/java/com/android/server/AnyMotionDetector.java b/apex/jobscheduler/service/java/com/android/server/AnyMotionDetector.java new file mode 100644 index 000000000000..8c5ee7f35027 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/AnyMotionDetector.java @@ -0,0 +1,520 @@ +/* + * Copyright (C) 2015 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; + +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.os.Handler; +import android.os.Message; +import android.os.PowerManager; +import android.os.SystemClock; +import android.util.Slog; + +/** + * Determines if the device has been set upon a stationary object. + */ +public class AnyMotionDetector { + interface DeviceIdleCallback { + public void onAnyMotionResult(int result); + } + + private static final String TAG = "AnyMotionDetector"; + + private static final boolean DEBUG = false; + + /** Stationary status is unknown due to insufficient orientation measurements. */ + public static final int RESULT_UNKNOWN = -1; + + /** Device is stationary, e.g. still on a table. */ + public static final int RESULT_STATIONARY = 0; + + /** Device has been moved. */ + public static final int RESULT_MOVED = 1; + + /** Orientation measurements are being performed or are planned. */ + private static final int STATE_INACTIVE = 0; + + /** No orientation measurements are being performed or are planned. */ + private static final int STATE_ACTIVE = 1; + + /** Current measurement state. */ + private int mState; + + /** Threshold energy above which the device is considered moving. */ + private final float THRESHOLD_ENERGY = 5f; + + /** The duration of the accelerometer orientation measurement. */ + private static final long ORIENTATION_MEASUREMENT_DURATION_MILLIS = 2500; + + /** The maximum duration we will collect accelerometer data. */ + private static final long ACCELEROMETER_DATA_TIMEOUT_MILLIS = 3000; + + /** The interval between accelerometer orientation measurements. */ + private static final long ORIENTATION_MEASUREMENT_INTERVAL_MILLIS = 5000; + + /** The maximum duration we will hold a wakelock to determine stationary status. */ + private static final long WAKELOCK_TIMEOUT_MILLIS = 30000; + + /** + * The duration in milliseconds after which an orientation measurement is considered + * too stale to be used. + */ + private static final int STALE_MEASUREMENT_TIMEOUT_MILLIS = 2 * 60 * 1000; + + /** The accelerometer sampling interval. */ + private static final int SAMPLING_INTERVAL_MILLIS = 40; + + private final Handler mHandler; + private final Object mLock = new Object(); + private Sensor mAccelSensor; + private SensorManager mSensorManager; + private PowerManager.WakeLock mWakeLock; + + /** Threshold angle in degrees beyond which the device is considered moving. */ + private final float mThresholdAngle; + + /** The minimum number of samples required to detect AnyMotion. */ + private int mNumSufficientSamples; + + /** True if an orientation measurement is in progress. */ + private boolean mMeasurementInProgress; + + /** True if sendMessageDelayed() for the mMeasurementTimeout callback has been scheduled */ + private boolean mMeasurementTimeoutIsActive; + + /** True if sendMessageDelayed() for the mWakelockTimeout callback has been scheduled */ + private boolean mWakelockTimeoutIsActive; + + /** True if sendMessageDelayed() for the mSensorRestart callback has been scheduled */ + private boolean mSensorRestartIsActive; + + /** The most recent gravity vector. */ + private Vector3 mCurrentGravityVector = null; + + /** The second most recent gravity vector. */ + private Vector3 mPreviousGravityVector = null; + + /** Running sum of squared errors. */ + private RunningSignalStats mRunningStats; + + private DeviceIdleCallback mCallback = null; + + public AnyMotionDetector(PowerManager pm, Handler handler, SensorManager sm, + DeviceIdleCallback callback, float thresholdAngle) { + if (DEBUG) Slog.d(TAG, "AnyMotionDetector instantiated."); + synchronized (mLock) { + mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); + mWakeLock.setReferenceCounted(false); + mHandler = handler; + mSensorManager = sm; + mAccelSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + mMeasurementInProgress = false; + mMeasurementTimeoutIsActive = false; + mWakelockTimeoutIsActive = false; + mSensorRestartIsActive = false; + mState = STATE_INACTIVE; + mCallback = callback; + mThresholdAngle = thresholdAngle; + mRunningStats = new RunningSignalStats(); + mNumSufficientSamples = (int) Math.ceil( + ((double)ORIENTATION_MEASUREMENT_DURATION_MILLIS / SAMPLING_INTERVAL_MILLIS)); + if (DEBUG) Slog.d(TAG, "mNumSufficientSamples = " + mNumSufficientSamples); + } + } + + /** + * If we do not have an accelerometer, we are not going to collect much data. + */ + public boolean hasSensor() { + return mAccelSensor != null; + } + + /* + * Acquire accel data until we determine AnyMotion status. + */ + public void checkForAnyMotion() { + if (DEBUG) { + Slog.d(TAG, "checkForAnyMotion(). mState = " + mState); + } + if (mState != STATE_ACTIVE) { + synchronized (mLock) { + mState = STATE_ACTIVE; + if (DEBUG) { + Slog.d(TAG, "Moved from STATE_INACTIVE to STATE_ACTIVE."); + } + mCurrentGravityVector = null; + mPreviousGravityVector = null; + mWakeLock.acquire(); + Message wakelockTimeoutMsg = Message.obtain(mHandler, mWakelockTimeout); + mHandler.sendMessageDelayed(wakelockTimeoutMsg, WAKELOCK_TIMEOUT_MILLIS); + mWakelockTimeoutIsActive = true; + startOrientationMeasurementLocked(); + } + } + } + + public void stop() { + synchronized (mLock) { + if (mState == STATE_ACTIVE) { + mState = STATE_INACTIVE; + if (DEBUG) Slog.d(TAG, "Moved from STATE_ACTIVE to STATE_INACTIVE."); + } + mHandler.removeCallbacks(mMeasurementTimeout); + mHandler.removeCallbacks(mSensorRestart); + mMeasurementTimeoutIsActive = false; + mSensorRestartIsActive = false; + if (mMeasurementInProgress) { + mMeasurementInProgress = false; + mSensorManager.unregisterListener(mListener); + } + mCurrentGravityVector = null; + mPreviousGravityVector = null; + if (mWakeLock.isHeld()) { + mHandler.removeCallbacks(mWakelockTimeout); + mWakelockTimeoutIsActive = false; + mWakeLock.release(); + } + } + } + + private void startOrientationMeasurementLocked() { + if (DEBUG) Slog.d(TAG, "startOrientationMeasurementLocked: mMeasurementInProgress=" + + mMeasurementInProgress + ", (mAccelSensor != null)=" + (mAccelSensor != null)); + if (!mMeasurementInProgress && mAccelSensor != null) { + if (mSensorManager.registerListener(mListener, mAccelSensor, + SAMPLING_INTERVAL_MILLIS * 1000)) { + mMeasurementInProgress = true; + mRunningStats.reset(); + } + Message measurementTimeoutMsg = Message.obtain(mHandler, mMeasurementTimeout); + mHandler.sendMessageDelayed(measurementTimeoutMsg, ACCELEROMETER_DATA_TIMEOUT_MILLIS); + mMeasurementTimeoutIsActive = true; + } + } + + private int stopOrientationMeasurementLocked() { + if (DEBUG) Slog.d(TAG, "stopOrientationMeasurement. mMeasurementInProgress=" + + mMeasurementInProgress); + int status = RESULT_UNKNOWN; + if (mMeasurementInProgress) { + mHandler.removeCallbacks(mMeasurementTimeout); + mMeasurementTimeoutIsActive = false; + mSensorManager.unregisterListener(mListener); + mMeasurementInProgress = false; + mPreviousGravityVector = mCurrentGravityVector; + mCurrentGravityVector = mRunningStats.getRunningAverage(); + if (mRunningStats.getSampleCount() == 0) { + Slog.w(TAG, "No accelerometer data acquired for orientation measurement."); + } + if (DEBUG) { + Slog.d(TAG, "mRunningStats = " + mRunningStats.toString()); + String currentGravityVectorString = (mCurrentGravityVector == null) ? + "null" : mCurrentGravityVector.toString(); + String previousGravityVectorString = (mPreviousGravityVector == null) ? + "null" : mPreviousGravityVector.toString(); + Slog.d(TAG, "mCurrentGravityVector = " + currentGravityVectorString); + Slog.d(TAG, "mPreviousGravityVector = " + previousGravityVectorString); + } + mRunningStats.reset(); + status = getStationaryStatus(); + if (DEBUG) Slog.d(TAG, "getStationaryStatus() returned " + status); + if (status != RESULT_UNKNOWN) { + if (mWakeLock.isHeld()) { + mHandler.removeCallbacks(mWakelockTimeout); + mWakelockTimeoutIsActive = false; + mWakeLock.release(); + } + if (DEBUG) { + Slog.d(TAG, "Moved from STATE_ACTIVE to STATE_INACTIVE. status = " + status); + } + mState = STATE_INACTIVE; + } else { + /* + * Unknown due to insufficient measurements. Schedule another orientation + * measurement. + */ + if (DEBUG) Slog.d(TAG, "stopOrientationMeasurementLocked(): another measurement" + + " scheduled in " + ORIENTATION_MEASUREMENT_INTERVAL_MILLIS + + " milliseconds."); + Message msg = Message.obtain(mHandler, mSensorRestart); + mHandler.sendMessageDelayed(msg, ORIENTATION_MEASUREMENT_INTERVAL_MILLIS); + mSensorRestartIsActive = true; + } + } + return status; + } + + /* + * Updates mStatus to the current AnyMotion status. + */ + public int getStationaryStatus() { + if ((mPreviousGravityVector == null) || (mCurrentGravityVector == null)) { + return RESULT_UNKNOWN; + } + Vector3 previousGravityVectorNormalized = mPreviousGravityVector.normalized(); + Vector3 currentGravityVectorNormalized = mCurrentGravityVector.normalized(); + float angle = previousGravityVectorNormalized.angleBetween(currentGravityVectorNormalized); + if (DEBUG) Slog.d(TAG, "getStationaryStatus: angle = " + angle + + " energy = " + mRunningStats.getEnergy()); + if ((angle < mThresholdAngle) && (mRunningStats.getEnergy() < THRESHOLD_ENERGY)) { + return RESULT_STATIONARY; + } else if (Float.isNaN(angle)) { + /** + * Floating point rounding errors have caused the angle calcuation's dot product to + * exceed 1.0. In such case, we report RESULT_MOVED to prevent devices from rapidly + * retrying this measurement. + */ + return RESULT_MOVED; + } + long diffTime = mCurrentGravityVector.timeMillisSinceBoot - + mPreviousGravityVector.timeMillisSinceBoot; + if (diffTime > STALE_MEASUREMENT_TIMEOUT_MILLIS) { + if (DEBUG) Slog.d(TAG, "getStationaryStatus: mPreviousGravityVector is too stale at " + + diffTime + " ms ago. Returning RESULT_UNKNOWN."); + return RESULT_UNKNOWN; + } + return RESULT_MOVED; + } + + private final SensorEventListener mListener = new SensorEventListener() { + @Override + public void onSensorChanged(SensorEvent event) { + int status = RESULT_UNKNOWN; + synchronized (mLock) { + Vector3 accelDatum = new Vector3(SystemClock.elapsedRealtime(), event.values[0], + event.values[1], event.values[2]); + mRunningStats.accumulate(accelDatum); + + // If we have enough samples, stop accelerometer data acquisition. + if (mRunningStats.getSampleCount() >= mNumSufficientSamples) { + status = stopOrientationMeasurementLocked(); + } + } + if (status != RESULT_UNKNOWN) { + mHandler.removeCallbacks(mWakelockTimeout); + mWakelockTimeoutIsActive = false; + mCallback.onAnyMotionResult(status); + } + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + } + }; + + private final Runnable mSensorRestart = new Runnable() { + @Override + public void run() { + synchronized (mLock) { + if (mSensorRestartIsActive == true) { + mSensorRestartIsActive = false; + startOrientationMeasurementLocked(); + } + } + } + }; + + private final Runnable mMeasurementTimeout = new Runnable() { + @Override + public void run() { + int status = RESULT_UNKNOWN; + synchronized (mLock) { + if (mMeasurementTimeoutIsActive == true) { + mMeasurementTimeoutIsActive = false; + if (DEBUG) Slog.i(TAG, "mMeasurementTimeout. Failed to collect sufficient accel " + + "data within " + ACCELEROMETER_DATA_TIMEOUT_MILLIS + " ms. Stopping " + + "orientation measurement."); + status = stopOrientationMeasurementLocked(); + if (status != RESULT_UNKNOWN) { + mHandler.removeCallbacks(mWakelockTimeout); + mWakelockTimeoutIsActive = false; + mCallback.onAnyMotionResult(status); + } + } + } + } + }; + + private final Runnable mWakelockTimeout = new Runnable() { + @Override + public void run() { + synchronized (mLock) { + if (mWakelockTimeoutIsActive == true) { + mWakelockTimeoutIsActive = false; + stop(); + } + } + } + }; + + /** + * A timestamped three dimensional vector and some vector operations. + */ + public static final class Vector3 { + public long timeMillisSinceBoot; + public float x; + public float y; + public float z; + + public Vector3(long timeMillisSinceBoot, float x, float y, float z) { + this.timeMillisSinceBoot = timeMillisSinceBoot; + this.x = x; + this.y = y; + this.z = z; + } + + public float norm() { + return (float) Math.sqrt(dotProduct(this)); + } + + public Vector3 normalized() { + float mag = norm(); + return new Vector3(timeMillisSinceBoot, x / mag, y / mag, z / mag); + } + + /** + * Returns the angle between this 3D vector and another given 3D vector. + * Assumes both have already been normalized. + * + * @param other The other Vector3 vector. + * @return angle between this vector and the other given one. + */ + public float angleBetween(Vector3 other) { + Vector3 crossVector = cross(other); + float degrees = Math.abs((float)Math.toDegrees( + Math.atan2(crossVector.norm(), dotProduct(other)))); + Slog.d(TAG, "angleBetween: this = " + this.toString() + + ", other = " + other.toString() + ", degrees = " + degrees); + return degrees; + } + + public Vector3 cross(Vector3 v) { + return new Vector3( + v.timeMillisSinceBoot, + y * v.z - z * v.y, + z * v.x - x * v.z, + x * v.y - y * v.x); + } + + @Override + public String toString() { + String msg = ""; + msg += "timeMillisSinceBoot=" + timeMillisSinceBoot; + msg += " | x=" + x; + msg += ", y=" + y; + msg += ", z=" + z; + return msg; + } + + public float dotProduct(Vector3 v) { + return x * v.x + y * v.y + z * v.z; + } + + public Vector3 times(float val) { + return new Vector3(timeMillisSinceBoot, x * val, y * val, z * val); + } + + public Vector3 plus(Vector3 v) { + return new Vector3(v.timeMillisSinceBoot, x + v.x, y + v.y, z + v.z); + } + + public Vector3 minus(Vector3 v) { + return new Vector3(v.timeMillisSinceBoot, x - v.x, y - v.y, z - v.z); + } + } + + /** + * Maintains running statistics on the signal revelant to AnyMotion detection, including: + * <ul> + * <li>running average. + * <li>running sum-of-squared-errors as the energy of the signal derivative. + * <ul> + */ + private static class RunningSignalStats { + Vector3 previousVector; + Vector3 currentVector; + Vector3 runningSum; + float energy; + int sampleCount; + + public RunningSignalStats() { + reset(); + } + + public void reset() { + previousVector = null; + currentVector = null; + runningSum = new Vector3(0, 0, 0, 0); + energy = 0; + sampleCount = 0; + } + + /** + * Apply a 3D vector v as the next element in the running SSE. + */ + public void accumulate(Vector3 v) { + if (v == null) { + if (DEBUG) Slog.i(TAG, "Cannot accumulate a null vector."); + return; + } + sampleCount++; + runningSum = runningSum.plus(v); + previousVector = currentVector; + currentVector = v; + if (previousVector != null) { + Vector3 dv = currentVector.minus(previousVector); + float incrementalEnergy = dv.x * dv.x + dv.y * dv.y + dv.z * dv.z; + energy += incrementalEnergy; + if (DEBUG) Slog.i(TAG, "Accumulated vector " + currentVector.toString() + + ", runningSum = " + runningSum.toString() + + ", incrementalEnergy = " + incrementalEnergy + + ", energy = " + energy); + } + } + + public Vector3 getRunningAverage() { + if (sampleCount > 0) { + return runningSum.times((float)(1.0f / sampleCount)); + } + return null; + } + + public float getEnergy() { + return energy; + } + + public int getSampleCount() { + return sampleCount; + } + + @Override + public String toString() { + String msg = ""; + String currentVectorString = (currentVector == null) ? + "null" : currentVector.toString(); + String previousVectorString = (previousVector == null) ? + "null" : previousVector.toString(); + msg += "previousVector = " + previousVectorString; + msg += ", currentVector = " + currentVectorString; + msg += ", sampleCount = " + sampleCount; + msg += ", energy = " + energy; + return msg; + } + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java new file mode 100644 index 000000000000..65aaf20ecacb --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java @@ -0,0 +1,4407 @@ +/* + * Copyright (C) 2015 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; + +import android.Manifest; +import android.app.ActivityManager; +import android.app.ActivityManagerInternal; +import android.app.AlarmManager; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.database.ContentObserver; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.hardware.TriggerEvent; +import android.hardware.TriggerEventListener; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.location.LocationRequest; +import android.net.ConnectivityManager; +import android.net.INetworkPolicyManager; +import android.net.NetworkInfo; +import android.net.Uri; +import android.os.BatteryManager; +import android.os.BatteryStats; +import android.os.Binder; +import android.os.Bundle; +import android.os.Environment; +import android.os.FileUtils; +import android.os.Handler; +import android.os.IDeviceIdleController; +import android.os.Looper; +import android.os.Message; +import android.os.PowerManager; +import android.os.PowerManager.ServiceType; +import android.os.PowerManagerInternal; +import android.os.Process; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.os.ServiceManager; +import android.os.ShellCallback; +import android.os.ShellCommand; +import android.os.SystemClock; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.AtomicFile; +import android.util.KeyValueListParser; +import android.util.MutableLong; +import android.util.Pair; +import android.util.Slog; +import android.util.SparseArray; +import android.util.SparseBooleanArray; +import android.util.TimeUtils; +import android.util.Xml; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.app.IBatteryStats; +import com.android.internal.os.BackgroundThread; +import com.android.internal.util.DumpUtils; +import com.android.internal.util.FastXmlSerializer; +import com.android.internal.util.XmlUtils; +import com.android.server.am.BatteryStatsService; +import com.android.server.deviceidle.ConstraintController; +import com.android.server.deviceidle.DeviceIdleConstraintTracker; +import com.android.server.deviceidle.IDeviceIdleConstraint; +import com.android.server.deviceidle.TvConstraintController; +import com.android.server.net.NetworkPolicyManagerInternal; +import com.android.server.wm.ActivityTaskManagerInternal; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.stream.Collectors; + +/** + * Keeps track of device idleness and drives low power mode based on that. + * + * Test: atest com.android.server.DeviceIdleControllerTest + * + * Current idling state machine (as of Android Q). This can be visualized using Graphviz: + <pre> + + digraph { + subgraph deep { + label="deep"; + + STATE_ACTIVE [label="STATE_ACTIVE\nScreen on OR Charging OR Alarm going off soon"] + STATE_INACTIVE [label="STATE_INACTIVE\nScreen off AND Not charging"] + STATE_QUICK_DOZE_DELAY [ + label="STATE_QUICK_DOZE_DELAY\n" + + "Screen off AND Not charging\n" + + "Location, motion detection, and significant motion monitoring turned off" + ] + STATE_IDLE_PENDING [ + label="STATE_IDLE_PENDING\nSignificant motion monitoring turned on" + ] + STATE_SENSING [label="STATE_SENSING\nMonitoring for ANY motion"] + STATE_LOCATING [ + label="STATE_LOCATING\nRequesting location, motion monitoring still on" + ] + STATE_IDLE [ + label="STATE_IDLE\nLocation and motion detection turned off\n" + + "Significant motion monitoring state unchanged" + ] + STATE_IDLE_MAINTENANCE [label="STATE_IDLE_MAINTENANCE\n"] + + STATE_ACTIVE -> STATE_INACTIVE [ + label="becomeInactiveIfAppropriateLocked() AND Quick Doze not enabled" + ] + STATE_ACTIVE -> STATE_QUICK_DOZE_DELAY [ + label="becomeInactiveIfAppropriateLocked() AND Quick Doze enabled" + ] + + STATE_INACTIVE -> STATE_ACTIVE [ + label="handleMotionDetectedLocked(), becomeActiveLocked()" + ] + STATE_INACTIVE -> STATE_IDLE_PENDING [label="stepIdleStateLocked()"] + STATE_INACTIVE -> STATE_QUICK_DOZE_DELAY [ + label="becomeInactiveIfAppropriateLocked() AND Quick Doze enabled" + ] + + STATE_IDLE_PENDING -> STATE_ACTIVE [ + label="handleMotionDetectedLocked(), becomeActiveLocked()" + ] + STATE_IDLE_PENDING -> STATE_SENSING [label="stepIdleStateLocked()"] + STATE_IDLE_PENDING -> STATE_QUICK_DOZE_DELAY [ + label="becomeInactiveIfAppropriateLocked() AND Quick Doze enabled" + ] + + STATE_SENSING -> STATE_ACTIVE [ + label="handleMotionDetectedLocked(), becomeActiveLocked()" + ] + STATE_SENSING -> STATE_LOCATING [label="stepIdleStateLocked()"] + STATE_SENSING -> STATE_QUICK_DOZE_DELAY [ + label="becomeInactiveIfAppropriateLocked() AND Quick Doze enabled" + ] + STATE_SENSING -> STATE_IDLE [ + label="stepIdleStateLocked()\n" + + "No Location Manager OR (no Network provider AND no GPS provider)" + ] + + STATE_LOCATING -> STATE_ACTIVE [ + label="handleMotionDetectedLocked(), becomeActiveLocked()" + ] + STATE_LOCATING -> STATE_QUICK_DOZE_DELAY [ + label="becomeInactiveIfAppropriateLocked() AND Quick Doze enabled" + ] + STATE_LOCATING -> STATE_IDLE [label="stepIdleStateLocked()"] + + STATE_QUICK_DOZE_DELAY -> STATE_ACTIVE [ + label="handleMotionDetectedLocked(), becomeActiveLocked()" + ] + STATE_QUICK_DOZE_DELAY -> STATE_IDLE [label="stepIdleStateLocked()"] + + STATE_IDLE -> STATE_ACTIVE [label="handleMotionDetectedLocked(), becomeActiveLocked()"] + STATE_IDLE -> STATE_IDLE_MAINTENANCE [label="stepIdleStateLocked()"] + + STATE_IDLE_MAINTENANCE -> STATE_ACTIVE [ + label="handleMotionDetectedLocked(), becomeActiveLocked()" + ] + STATE_IDLE_MAINTENANCE -> STATE_IDLE [ + label="stepIdleStateLocked(), exitMaintenanceEarlyIfNeededLocked()" + ] + } + + subgraph light { + label="light" + + LIGHT_STATE_ACTIVE [ + label="LIGHT_STATE_ACTIVE\nScreen on OR Charging OR Alarm going off soon" + ] + LIGHT_STATE_INACTIVE [label="LIGHT_STATE_INACTIVE\nScreen off AND Not charging"] + LIGHT_STATE_PRE_IDLE [ + label="LIGHT_STATE_PRE_IDLE\n" + + "Delay going into LIGHT_STATE_IDLE due to some running jobs or alarms" + ] + LIGHT_STATE_IDLE [label="LIGHT_STATE_IDLE\n"] + LIGHT_STATE_WAITING_FOR_NETWORK [ + label="LIGHT_STATE_WAITING_FOR_NETWORK\n" + + "Coming out of LIGHT_STATE_IDLE, waiting for network" + ] + LIGHT_STATE_IDLE_MAINTENANCE [label="LIGHT_STATE_IDLE_MAINTENANCE\n"] + LIGHT_STATE_OVERRIDE [ + label="LIGHT_STATE_OVERRIDE\nDevice in deep doze, light no longer changing states" + ] + + LIGHT_STATE_ACTIVE -> LIGHT_STATE_INACTIVE [ + label="becomeInactiveIfAppropriateLocked()" + ] + LIGHT_STATE_ACTIVE -> LIGHT_STATE_OVERRIDE [label="deep goes to STATE_IDLE"] + + LIGHT_STATE_INACTIVE -> LIGHT_STATE_ACTIVE [label="becomeActiveLocked()"] + LIGHT_STATE_INACTIVE -> LIGHT_STATE_PRE_IDLE [label="active jobs"] + LIGHT_STATE_INACTIVE -> LIGHT_STATE_IDLE [label="no active jobs"] + LIGHT_STATE_INACTIVE -> LIGHT_STATE_OVERRIDE [label="deep goes to STATE_IDLE"] + + LIGHT_STATE_PRE_IDLE -> LIGHT_STATE_ACTIVE [label="becomeActiveLocked()"] + LIGHT_STATE_PRE_IDLE -> LIGHT_STATE_IDLE [ + label="stepLightIdleStateLocked(), exitMaintenanceEarlyIfNeededLocked()" + ] + LIGHT_STATE_PRE_IDLE -> LIGHT_STATE_OVERRIDE [label="deep goes to STATE_IDLE"] + + LIGHT_STATE_IDLE -> LIGHT_STATE_ACTIVE [label="becomeActiveLocked()"] + LIGHT_STATE_IDLE -> LIGHT_STATE_WAITING_FOR_NETWORK [label="no network"] + LIGHT_STATE_IDLE -> LIGHT_STATE_IDLE_MAINTENANCE + LIGHT_STATE_IDLE -> LIGHT_STATE_OVERRIDE [label="deep goes to STATE_IDLE"] + + LIGHT_STATE_WAITING_FOR_NETWORK -> LIGHT_STATE_ACTIVE [label="becomeActiveLocked()"] + LIGHT_STATE_WAITING_FOR_NETWORK -> LIGHT_STATE_IDLE_MAINTENANCE + LIGHT_STATE_WAITING_FOR_NETWORK -> LIGHT_STATE_OVERRIDE [ + label="deep goes to STATE_IDLE" + ] + + LIGHT_STATE_IDLE_MAINTENANCE -> LIGHT_STATE_ACTIVE [label="becomeActiveLocked()"] + LIGHT_STATE_IDLE_MAINTENANCE -> LIGHT_STATE_IDLE [ + label="stepLightIdleStateLocked(), exitMaintenanceEarlyIfNeededLocked()" + ] + LIGHT_STATE_IDLE_MAINTENANCE -> LIGHT_STATE_OVERRIDE [label="deep goes to STATE_IDLE"] + + LIGHT_STATE_OVERRIDE -> LIGHT_STATE_ACTIVE [ + label="handleMotionDetectedLocked(), becomeActiveLocked()" + ] + } + } + </pre> + */ +public class DeviceIdleController extends SystemService + implements AnyMotionDetector.DeviceIdleCallback { + private static final String TAG = "DeviceIdleController"; + + private static final boolean DEBUG = false; + + private static final boolean COMPRESS_TIME = false; + + private static final int EVENT_BUFFER_SIZE = 100; + + private AlarmManager mAlarmManager; + private AlarmManagerInternal mLocalAlarmManager; + private IBatteryStats mBatteryStats; + private ActivityManagerInternal mLocalActivityManager; + private ActivityTaskManagerInternal mLocalActivityTaskManager; + private PowerManagerInternal mLocalPowerManager; + private PowerManager mPowerManager; + private INetworkPolicyManager mNetworkPolicyManager; + private SensorManager mSensorManager; + private final boolean mUseMotionSensor; + private Sensor mMotionSensor; + private LocationRequest mLocationRequest; + private Intent mIdleIntent; + private Intent mLightIdleIntent; + private AnyMotionDetector mAnyMotionDetector; + private final AppStateTracker mAppStateTracker; + private boolean mLightEnabled; + private boolean mDeepEnabled; + private boolean mQuickDozeActivated; + private boolean mForceIdle; + private boolean mNetworkConnected; + private boolean mScreenOn; + private boolean mCharging; + private boolean mNotMoving; + private boolean mLocating; + private boolean mLocated; + private boolean mHasGps; + private boolean mHasNetworkLocation; + private Location mLastGenericLocation; + private Location mLastGpsLocation; + // Current locked state of the screen + private boolean mScreenLocked; + private int mNumBlockingConstraints = 0; + + /** + * Constraints are the "handbrakes" that stop the device from moving into a lower state until + * every one is released at the same time. + * + * @see #registerDeviceIdleConstraintInternal(IDeviceIdleConstraint, String, int) + */ + private final ArrayMap<IDeviceIdleConstraint, DeviceIdleConstraintTracker> + mConstraints = new ArrayMap<>(); + private ConstraintController mConstraintController; + + /** Device is currently active. */ + @VisibleForTesting + static final int STATE_ACTIVE = 0; + /** Device is inactive (screen off, no motion) and we are waiting to for idle. */ + @VisibleForTesting + static final int STATE_INACTIVE = 1; + /** Device is past the initial inactive period, and waiting for the next idle period. */ + @VisibleForTesting + static final int STATE_IDLE_PENDING = 2; + /** Device is currently sensing motion. */ + @VisibleForTesting + static final int STATE_SENSING = 3; + /** Device is currently finding location (and may still be sensing). */ + @VisibleForTesting + static final int STATE_LOCATING = 4; + /** Device is in the idle state, trying to stay asleep as much as possible. */ + @VisibleForTesting + static final int STATE_IDLE = 5; + /** Device is in the idle state, but temporarily out of idle to do regular maintenance. */ + @VisibleForTesting + static final int STATE_IDLE_MAINTENANCE = 6; + /** + * Device is inactive and should go straight into idle (foregoing motion and location + * monitoring), but allow some time for current work to complete first. + */ + @VisibleForTesting + static final int STATE_QUICK_DOZE_DELAY = 7; + + private static final int ACTIVE_REASON_UNKNOWN = 0; + private static final int ACTIVE_REASON_MOTION = 1; + private static final int ACTIVE_REASON_SCREEN = 2; + private static final int ACTIVE_REASON_CHARGING = 3; + private static final int ACTIVE_REASON_UNLOCKED = 4; + private static final int ACTIVE_REASON_FROM_BINDER_CALL = 5; + private static final int ACTIVE_REASON_FORCED = 6; + private static final int ACTIVE_REASON_ALARM = 7; + @VisibleForTesting + static final int SET_IDLE_FACTOR_RESULT_UNINIT = -1; + @VisibleForTesting + static final int SET_IDLE_FACTOR_RESULT_IGNORED = 0; + @VisibleForTesting + static final int SET_IDLE_FACTOR_RESULT_OK = 1; + @VisibleForTesting + static final int SET_IDLE_FACTOR_RESULT_NOT_SUPPORT = 2; + @VisibleForTesting + static final int SET_IDLE_FACTOR_RESULT_INVALID = 3; + @VisibleForTesting + static final long MIN_STATE_STEP_ALARM_CHANGE = 60 * 1000; + @VisibleForTesting + static final float MIN_PRE_IDLE_FACTOR_CHANGE = 0.05f; + + @VisibleForTesting + static String stateToString(int state) { + switch (state) { + case STATE_ACTIVE: return "ACTIVE"; + case STATE_INACTIVE: return "INACTIVE"; + case STATE_IDLE_PENDING: return "IDLE_PENDING"; + case STATE_SENSING: return "SENSING"; + case STATE_LOCATING: return "LOCATING"; + case STATE_IDLE: return "IDLE"; + case STATE_IDLE_MAINTENANCE: return "IDLE_MAINTENANCE"; + case STATE_QUICK_DOZE_DELAY: return "QUICK_DOZE_DELAY"; + default: return Integer.toString(state); + } + } + + /** Device is currently active. */ + @VisibleForTesting + static final int LIGHT_STATE_ACTIVE = 0; + /** Device is inactive (screen off) and we are waiting to for the first light idle. */ + @VisibleForTesting + static final int LIGHT_STATE_INACTIVE = 1; + /** Device is about to go idle for the first time, wait for current work to complete. */ + @VisibleForTesting + static final int LIGHT_STATE_PRE_IDLE = 3; + /** Device is in the light idle state, trying to stay asleep as much as possible. */ + @VisibleForTesting + static final int LIGHT_STATE_IDLE = 4; + /** Device is in the light idle state, we want to go in to idle maintenance but are + * waiting for network connectivity before doing so. */ + @VisibleForTesting + static final int LIGHT_STATE_WAITING_FOR_NETWORK = 5; + /** Device is in the light idle state, but temporarily out of idle to do regular maintenance. */ + @VisibleForTesting + static final int LIGHT_STATE_IDLE_MAINTENANCE = 6; + /** Device light idle state is overriden, now applying deep doze state. */ + @VisibleForTesting + static final int LIGHT_STATE_OVERRIDE = 7; + + @VisibleForTesting + static String lightStateToString(int state) { + switch (state) { + case LIGHT_STATE_ACTIVE: return "ACTIVE"; + case LIGHT_STATE_INACTIVE: return "INACTIVE"; + case LIGHT_STATE_PRE_IDLE: return "PRE_IDLE"; + case LIGHT_STATE_IDLE: return "IDLE"; + case LIGHT_STATE_WAITING_FOR_NETWORK: return "WAITING_FOR_NETWORK"; + case LIGHT_STATE_IDLE_MAINTENANCE: return "IDLE_MAINTENANCE"; + case LIGHT_STATE_OVERRIDE: return "OVERRIDE"; + default: return Integer.toString(state); + } + } + + private int mState; + private int mLightState; + + private long mInactiveTimeout; + private long mNextAlarmTime; + private long mNextIdlePendingDelay; + private long mNextIdleDelay; + private long mNextLightIdleDelay; + private long mNextLightAlarmTime; + private long mNextSensingTimeoutAlarmTime; + + /** How long a light idle maintenance window should last. */ + private long mCurLightIdleBudget; + + /** + * Start time of the current (light or full) maintenance window, in the elapsed timebase. Valid + * only if {@link #mState} == {@link #STATE_IDLE_MAINTENANCE} or + * {@link #mLightState} == {@link #LIGHT_STATE_IDLE_MAINTENANCE}. + */ + private long mMaintenanceStartTime; + private long mIdleStartTime; + + private int mActiveIdleOpCount; + private PowerManager.WakeLock mActiveIdleWakeLock; // held when there are operations in progress + private PowerManager.WakeLock mGoingIdleWakeLock; // held when we are going idle so hardware + // (especially NetworkPolicyManager) can shut + // down. + private boolean mJobsActive; + private boolean mAlarmsActive; + + /* Factor to apply to INACTIVE_TIMEOUT and IDLE_AFTER_INACTIVE_TIMEOUT in order to enter + * STATE_IDLE faster or slower. Don't apply this to SENSING_TIMEOUT or LOCATING_TIMEOUT because: + * - Both of them are shorter + * - Device sensor might take time be to become be stabilized + * Also don't apply the factor if the device is in motion because device motion provides a + * stronger signal than a prediction algorithm. + */ + private float mPreIdleFactor; + private float mLastPreIdleFactor; + private int mActiveReason; + + public final AtomicFile mConfigFile; + + /** + * Package names the system has white-listed to opt out of power save restrictions, + * except for device idle mode. + */ + private final ArrayMap<String, Integer> mPowerSaveWhitelistAppsExceptIdle = new ArrayMap<>(); + + /** + * Package names the user has white-listed using commandline option to opt out of + * power save restrictions, except for device idle mode. + */ + private final ArraySet<String> mPowerSaveWhitelistUserAppsExceptIdle = new ArraySet<>(); + + /** + * Package names the system has white-listed to opt out of power save restrictions for + * all modes. + */ + private final ArrayMap<String, Integer> mPowerSaveWhitelistApps = new ArrayMap<>(); + + /** + * Package names the user has white-listed to opt out of power save restrictions. + */ + private final ArrayMap<String, Integer> mPowerSaveWhitelistUserApps = new ArrayMap<>(); + + /** + * App IDs of built-in system apps that have been white-listed except for idle modes. + */ + private final SparseBooleanArray mPowerSaveWhitelistSystemAppIdsExceptIdle + = new SparseBooleanArray(); + + /** + * App IDs of built-in system apps that have been white-listed. + */ + private final SparseBooleanArray mPowerSaveWhitelistSystemAppIds = new SparseBooleanArray(); + + /** + * App IDs that have been white-listed to opt out of power save restrictions, except + * for device idle modes. + */ + private final SparseBooleanArray mPowerSaveWhitelistExceptIdleAppIds = new SparseBooleanArray(); + + /** + * Current app IDs that are in the complete power save white list, but shouldn't be + * excluded from idle modes. This array can be shared with others because it will not be + * modified once set. + */ + private int[] mPowerSaveWhitelistExceptIdleAppIdArray = new int[0]; + + /** + * App IDs that have been white-listed to opt out of power save restrictions. + */ + private final SparseBooleanArray mPowerSaveWhitelistAllAppIds = new SparseBooleanArray(); + + /** + * Current app IDs that are in the complete power save white list. This array can + * be shared with others because it will not be modified once set. + */ + private int[] mPowerSaveWhitelistAllAppIdArray = new int[0]; + + /** + * App IDs that have been white-listed by the user to opt out of power save restrictions. + */ + private final SparseBooleanArray mPowerSaveWhitelistUserAppIds = new SparseBooleanArray(); + + /** + * Current app IDs that are in the user power save white list. This array can + * be shared with others because it will not be modified once set. + */ + private int[] mPowerSaveWhitelistUserAppIdArray = new int[0]; + + /** + * List of end times for UIDs that are temporarily marked as being allowed to access + * the network and acquire wakelocks. Times are in milliseconds. + */ + private final SparseArray<Pair<MutableLong, String>> mTempWhitelistAppIdEndTimes + = new SparseArray<>(); + + private NetworkPolicyManagerInternal mNetworkPolicyManagerInternal; + + /** + * Current app IDs of temporarily whitelist apps for high-priority messages. + */ + private int[] mTempWhitelistAppIdArray = new int[0]; + + /** + * Apps in the system whitelist that have been taken out (probably because the user wanted to). + * They can be restored back by calling restoreAppToSystemWhitelist(String). + */ + private ArrayMap<String, Integer> mRemovedFromSystemWhitelistApps = new ArrayMap<>(); + + private static final int EVENT_NULL = 0; + private static final int EVENT_NORMAL = 1; + private static final int EVENT_LIGHT_IDLE = 2; + private static final int EVENT_LIGHT_MAINTENANCE = 3; + private static final int EVENT_DEEP_IDLE = 4; + private static final int EVENT_DEEP_MAINTENANCE = 5; + + private final int[] mEventCmds = new int[EVENT_BUFFER_SIZE]; + private final long[] mEventTimes = new long[EVENT_BUFFER_SIZE]; + private final String[] mEventReasons = new String[EVENT_BUFFER_SIZE]; + + private void addEvent(int cmd, String reason) { + if (mEventCmds[0] != cmd) { + System.arraycopy(mEventCmds, 0, mEventCmds, 1, EVENT_BUFFER_SIZE - 1); + System.arraycopy(mEventTimes, 0, mEventTimes, 1, EVENT_BUFFER_SIZE - 1); + System.arraycopy(mEventReasons, 0, mEventReasons, 1, EVENT_BUFFER_SIZE - 1); + mEventCmds[0] = cmd; + mEventTimes[0] = SystemClock.elapsedRealtime(); + mEventReasons[0] = reason; + } + } + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override public void onReceive(Context context, Intent intent) { + switch (intent.getAction()) { + case ConnectivityManager.CONNECTIVITY_ACTION: { + updateConnectivityState(intent); + } break; + case Intent.ACTION_BATTERY_CHANGED: { + boolean present = intent.getBooleanExtra(BatteryManager.EXTRA_PRESENT, true); + boolean plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0; + synchronized (DeviceIdleController.this) { + updateChargingLocked(present && plugged); + } + } break; + case Intent.ACTION_PACKAGE_REMOVED: { + if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { + Uri data = intent.getData(); + String ssp; + if (data != null && (ssp = data.getSchemeSpecificPart()) != null) { + removePowerSaveWhitelistAppInternal(ssp); + } + } + } break; + } + } + }; + + private final AlarmManager.OnAlarmListener mLightAlarmListener + = new AlarmManager.OnAlarmListener() { + @Override + public void onAlarm() { + synchronized (DeviceIdleController.this) { + stepLightIdleStateLocked("s:alarm"); + } + } + }; + + private final AlarmManager.OnAlarmListener mSensingTimeoutAlarmListener + = new AlarmManager.OnAlarmListener() { + @Override + public void onAlarm() { + if (mState == STATE_SENSING) { + synchronized (DeviceIdleController.this) { + // Restart the device idle progression in case the device moved but the screen + // didn't turn on. + becomeInactiveIfAppropriateLocked(); + } + } + } + }; + + @VisibleForTesting + final AlarmManager.OnAlarmListener mDeepAlarmListener + = new AlarmManager.OnAlarmListener() { + @Override + public void onAlarm() { + synchronized (DeviceIdleController.this) { + stepIdleStateLocked("s:alarm"); + } + } + }; + + private final BroadcastReceiver mIdleStartedDoneReceiver = new BroadcastReceiver() { + @Override public void onReceive(Context context, Intent intent) { + // When coming out of a deep idle, we will add in some delay before we allow + // the system to settle down and finish the maintenance window. This is + // to give a chance for any pending work to be scheduled. + if (PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED.equals(intent.getAction())) { + mHandler.sendEmptyMessageDelayed(MSG_FINISH_IDLE_OP, + mConstants.MIN_DEEP_MAINTENANCE_TIME); + } else { + mHandler.sendEmptyMessageDelayed(MSG_FINISH_IDLE_OP, + mConstants.MIN_LIGHT_MAINTENANCE_TIME); + } + } + }; + + private final BroadcastReceiver mInteractivityReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + synchronized (DeviceIdleController.this) { + updateInteractivityLocked(); + } + } + }; + + @VisibleForTesting + final class MotionListener extends TriggerEventListener + implements SensorEventListener { + + boolean active = false; + + public boolean isActive() { + return active; + } + + @Override + public void onTrigger(TriggerEvent event) { + synchronized (DeviceIdleController.this) { + active = false; + motionLocked(); + } + } + + @Override + public void onSensorChanged(SensorEvent event) { + synchronized (DeviceIdleController.this) { + mSensorManager.unregisterListener(this, mMotionSensor); + active = false; + motionLocked(); + } + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) {} + + public boolean registerLocked() { + boolean success; + if (mMotionSensor.getReportingMode() == Sensor.REPORTING_MODE_ONE_SHOT) { + success = mSensorManager.requestTriggerSensor(mMotionListener, mMotionSensor); + } else { + success = mSensorManager.registerListener( + mMotionListener, mMotionSensor, SensorManager.SENSOR_DELAY_NORMAL); + } + if (success) { + active = true; + } else { + Slog.e(TAG, "Unable to register for " + mMotionSensor); + } + return success; + } + + public void unregisterLocked() { + if (mMotionSensor.getReportingMode() == Sensor.REPORTING_MODE_ONE_SHOT) { + mSensorManager.cancelTriggerSensor(mMotionListener, mMotionSensor); + } else { + mSensorManager.unregisterListener(mMotionListener); + } + active = false; + } + } + @VisibleForTesting final MotionListener mMotionListener = new MotionListener(); + + private final LocationListener mGenericLocationListener = new LocationListener() { + @Override + public void onLocationChanged(Location location) { + synchronized (DeviceIdleController.this) { + receivedGenericLocationLocked(location); + } + } + + @Override + public void onStatusChanged(String provider, int status, Bundle extras) { + } + + @Override + public void onProviderEnabled(String provider) { + } + + @Override + public void onProviderDisabled(String provider) { + } + }; + + private final LocationListener mGpsLocationListener = new LocationListener() { + @Override + public void onLocationChanged(Location location) { + synchronized (DeviceIdleController.this) { + receivedGpsLocationLocked(location); + } + } + + @Override + public void onStatusChanged(String provider, int status, Bundle extras) { + } + + @Override + public void onProviderEnabled(String provider) { + } + + @Override + public void onProviderDisabled(String provider) { + } + }; + + /** + * All times are in milliseconds. These constants are kept synchronized with the system + * global Settings. Any access to this class or its fields should be done while + * holding the DeviceIdleController lock. + */ + public final class Constants extends ContentObserver { + // Key names stored in the settings value. + private static final String KEY_LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT + = "light_after_inactive_to"; + private static final String KEY_LIGHT_PRE_IDLE_TIMEOUT = "light_pre_idle_to"; + private static final String KEY_LIGHT_IDLE_TIMEOUT = "light_idle_to"; + private static final String KEY_LIGHT_IDLE_FACTOR = "light_idle_factor"; + private static final String KEY_LIGHT_MAX_IDLE_TIMEOUT = "light_max_idle_to"; + private static final String KEY_LIGHT_IDLE_MAINTENANCE_MIN_BUDGET + = "light_idle_maintenance_min_budget"; + private static final String KEY_LIGHT_IDLE_MAINTENANCE_MAX_BUDGET + = "light_idle_maintenance_max_budget"; + private static final String KEY_MIN_LIGHT_MAINTENANCE_TIME = "min_light_maintenance_time"; + private static final String KEY_MIN_DEEP_MAINTENANCE_TIME = "min_deep_maintenance_time"; + private static final String KEY_INACTIVE_TIMEOUT = "inactive_to"; + private static final String KEY_SENSING_TIMEOUT = "sensing_to"; + private static final String KEY_LOCATING_TIMEOUT = "locating_to"; + private static final String KEY_LOCATION_ACCURACY = "location_accuracy"; + private static final String KEY_MOTION_INACTIVE_TIMEOUT = "motion_inactive_to"; + private static final String KEY_IDLE_AFTER_INACTIVE_TIMEOUT = "idle_after_inactive_to"; + private static final String KEY_IDLE_PENDING_TIMEOUT = "idle_pending_to"; + private static final String KEY_MAX_IDLE_PENDING_TIMEOUT = "max_idle_pending_to"; + private static final String KEY_IDLE_PENDING_FACTOR = "idle_pending_factor"; + private static final String KEY_QUICK_DOZE_DELAY_TIMEOUT = "quick_doze_delay_to"; + private static final String KEY_IDLE_TIMEOUT = "idle_to"; + private static final String KEY_MAX_IDLE_TIMEOUT = "max_idle_to"; + private static final String KEY_IDLE_FACTOR = "idle_factor"; + private static final String KEY_MIN_TIME_TO_ALARM = "min_time_to_alarm"; + private static final String KEY_MAX_TEMP_APP_WHITELIST_DURATION = + "max_temp_app_whitelist_duration"; + private static final String KEY_MMS_TEMP_APP_WHITELIST_DURATION = + "mms_temp_app_whitelist_duration"; + private static final String KEY_SMS_TEMP_APP_WHITELIST_DURATION = + "sms_temp_app_whitelist_duration"; + private static final String KEY_NOTIFICATION_WHITELIST_DURATION = + "notification_whitelist_duration"; + /** + * Whether to wait for the user to unlock the device before causing screen-on to + * exit doze. Default = true + */ + private static final String KEY_WAIT_FOR_UNLOCK = "wait_for_unlock"; + private static final String KEY_PRE_IDLE_FACTOR_LONG = + "pre_idle_factor_long"; + private static final String KEY_PRE_IDLE_FACTOR_SHORT = + "pre_idle_factor_short"; + + /** + * This is the time, after becoming inactive, that we go in to the first + * light-weight idle mode. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT + */ + public long LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT; + + /** + * This is amount of time we will wait from the point where we decide we would + * like to go idle until we actually do, while waiting for jobs and other current + * activity to finish. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_LIGHT_PRE_IDLE_TIMEOUT + */ + public long LIGHT_PRE_IDLE_TIMEOUT; + + /** + * This is the initial time that we will run in idle maintenance mode. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_LIGHT_IDLE_TIMEOUT + */ + public long LIGHT_IDLE_TIMEOUT; + + /** + * Scaling factor to apply to the light idle mode time each time we complete a cycle. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_LIGHT_IDLE_FACTOR + */ + public float LIGHT_IDLE_FACTOR; + + /** + * This is the maximum time we will run in idle maintenance mode. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_LIGHT_MAX_IDLE_TIMEOUT + */ + public long LIGHT_MAX_IDLE_TIMEOUT; + + /** + * This is the minimum amount of time we want to make available for maintenance mode + * when lightly idling. That is, we will always have at least this amount of time + * available maintenance before timing out and cutting off maintenance mode. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_LIGHT_IDLE_MAINTENANCE_MIN_BUDGET + */ + public long LIGHT_IDLE_MAINTENANCE_MIN_BUDGET; + + /** + * This is the maximum amount of time we want to make available for maintenance mode + * when lightly idling. That is, if the system isn't using up its minimum maintenance + * budget and this time is being added to the budget reserve, this is the maximum + * reserve size we will allow to grow and thus the maximum amount of time we will + * allow for the maintenance window. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_LIGHT_IDLE_MAINTENANCE_MAX_BUDGET + */ + public long LIGHT_IDLE_MAINTENANCE_MAX_BUDGET; + + /** + * This is the minimum amount of time that we will stay in maintenance mode after + * a light doze. We have this minimum to allow various things to respond to switching + * in to maintenance mode and scheduling their work -- otherwise we may + * see there is nothing to do (no jobs pending) and go out of maintenance + * mode immediately. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_MIN_LIGHT_MAINTENANCE_TIME + */ + public long MIN_LIGHT_MAINTENANCE_TIME; + + /** + * This is the minimum amount of time that we will stay in maintenance mode after + * a full doze. We have this minimum to allow various things to respond to switching + * in to maintenance mode and scheduling their work -- otherwise we may + * see there is nothing to do (no jobs pending) and go out of maintenance + * mode immediately. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_MIN_DEEP_MAINTENANCE_TIME + */ + public long MIN_DEEP_MAINTENANCE_TIME; + + /** + * This is the time, after becoming inactive, at which we start looking at the + * motion sensor to determine if the device is being left alone. We don't do this + * immediately after going inactive just because we don't want to be continually running + * the motion sensor whenever the screen is off. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_INACTIVE_TIMEOUT + */ + public long INACTIVE_TIMEOUT; + + /** + * If we don't receive a callback from AnyMotion in this amount of time + + * {@link #LOCATING_TIMEOUT}, we will change from + * STATE_SENSING to STATE_INACTIVE, and any AnyMotion callbacks while not in STATE_SENSING + * will be ignored. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_SENSING_TIMEOUT + */ + public long SENSING_TIMEOUT; + + /** + * This is how long we will wait to try to get a good location fix before going in to + * idle mode. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_LOCATING_TIMEOUT + */ + public long LOCATING_TIMEOUT; + + /** + * The desired maximum accuracy (in meters) we consider the location to be good enough to go + * on to idle. We will be trying to get an accuracy fix at least this good or until + * {@link #LOCATING_TIMEOUT} expires. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_LOCATION_ACCURACY + */ + public float LOCATION_ACCURACY; + + /** + * This is the time, after seeing motion, that we wait after becoming inactive from + * that until we start looking for motion again. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_MOTION_INACTIVE_TIMEOUT + */ + public long MOTION_INACTIVE_TIMEOUT; + + /** + * This is the time, after the inactive timeout elapses, that we will wait looking + * for motion until we truly consider the device to be idle. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_IDLE_AFTER_INACTIVE_TIMEOUT + */ + public long IDLE_AFTER_INACTIVE_TIMEOUT; + + /** + * This is the initial time, after being idle, that we will allow ourself to be back + * in the IDLE_MAINTENANCE state allowing the system to run normally until we return to + * idle. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_IDLE_PENDING_TIMEOUT + */ + public long IDLE_PENDING_TIMEOUT; + + /** + * Maximum pending idle timeout (time spent running) we will be allowed to use. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_MAX_IDLE_PENDING_TIMEOUT + */ + public long MAX_IDLE_PENDING_TIMEOUT; + + /** + * Scaling factor to apply to current pending idle timeout each time we cycle through + * that state. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_IDLE_PENDING_FACTOR + */ + public float IDLE_PENDING_FACTOR; + + /** + * This is amount of time we will wait from the point where we go into + * STATE_QUICK_DOZE_DELAY until we actually go into STATE_IDLE, while waiting for jobs + * and other current activity to finish. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_QUICK_DOZE_DELAY_TIMEOUT + */ + public long QUICK_DOZE_DELAY_TIMEOUT; + + /** + * This is the initial time that we want to sit in the idle state before waking up + * again to return to pending idle and allowing normal work to run. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_IDLE_TIMEOUT + */ + public long IDLE_TIMEOUT; + + /** + * Maximum idle duration we will be allowed to use. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_MAX_IDLE_TIMEOUT + */ + public long MAX_IDLE_TIMEOUT; + + /** + * Scaling factor to apply to current idle timeout each time we cycle through that state. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_IDLE_FACTOR + */ + public float IDLE_FACTOR; + + /** + * This is the minimum time we will allow until the next upcoming alarm for us to + * actually go in to idle mode. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_MIN_TIME_TO_ALARM + */ + public long MIN_TIME_TO_ALARM; + + /** + * Max amount of time to temporarily whitelist an app when it receives a high priority + * tickle. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_MAX_TEMP_APP_WHITELIST_DURATION + */ + public long MAX_TEMP_APP_WHITELIST_DURATION; + + /** + * Amount of time we would like to whitelist an app that is receiving an MMS. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_MMS_TEMP_APP_WHITELIST_DURATION + */ + public long MMS_TEMP_APP_WHITELIST_DURATION; + + /** + * Amount of time we would like to whitelist an app that is receiving an SMS. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_SMS_TEMP_APP_WHITELIST_DURATION + */ + public long SMS_TEMP_APP_WHITELIST_DURATION; + + /** + * Amount of time we would like to whitelist an app that is handling a + * {@link android.app.PendingIntent} triggered by a {@link android.app.Notification}. + * @see Settings.Global#DEVICE_IDLE_CONSTANTS + * @see #KEY_NOTIFICATION_WHITELIST_DURATION + */ + public long NOTIFICATION_WHITELIST_DURATION; + + /** + * Pre idle time factor use to make idle delay longer + */ + public float PRE_IDLE_FACTOR_LONG; + + /** + * Pre idle time factor use to make idle delay shorter + */ + public float PRE_IDLE_FACTOR_SHORT; + + public boolean WAIT_FOR_UNLOCK; + + private final ContentResolver mResolver; + private final boolean mSmallBatteryDevice; + private final KeyValueListParser mParser = new KeyValueListParser(','); + + public Constants(Handler handler, ContentResolver resolver) { + super(handler); + mResolver = resolver; + mSmallBatteryDevice = ActivityManager.isSmallBatteryDevice(); + mResolver.registerContentObserver( + Settings.Global.getUriFor(Settings.Global.DEVICE_IDLE_CONSTANTS), + false, this); + updateConstants(); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + updateConstants(); + } + + private void updateConstants() { + synchronized (DeviceIdleController.this) { + try { + mParser.setString(Settings.Global.getString(mResolver, + Settings.Global.DEVICE_IDLE_CONSTANTS)); + } catch (IllegalArgumentException e) { + // Failed to parse the settings string, log this and move on + // with defaults. + Slog.e(TAG, "Bad device idle settings", e); + } + + LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT = mParser.getDurationMillis( + KEY_LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT, + !COMPRESS_TIME ? 3 * 60 * 1000L : 15 * 1000L); + LIGHT_PRE_IDLE_TIMEOUT = mParser.getDurationMillis(KEY_LIGHT_PRE_IDLE_TIMEOUT, + !COMPRESS_TIME ? 3 * 60 * 1000L : 30 * 1000L); + LIGHT_IDLE_TIMEOUT = mParser.getDurationMillis(KEY_LIGHT_IDLE_TIMEOUT, + !COMPRESS_TIME ? 5 * 60 * 1000L : 15 * 1000L); + LIGHT_IDLE_FACTOR = mParser.getFloat(KEY_LIGHT_IDLE_FACTOR, + 2f); + LIGHT_MAX_IDLE_TIMEOUT = mParser.getDurationMillis(KEY_LIGHT_MAX_IDLE_TIMEOUT, + !COMPRESS_TIME ? 15 * 60 * 1000L : 60 * 1000L); + LIGHT_IDLE_MAINTENANCE_MIN_BUDGET = mParser.getDurationMillis( + KEY_LIGHT_IDLE_MAINTENANCE_MIN_BUDGET, + !COMPRESS_TIME ? 1 * 60 * 1000L : 15 * 1000L); + LIGHT_IDLE_MAINTENANCE_MAX_BUDGET = mParser.getDurationMillis( + KEY_LIGHT_IDLE_MAINTENANCE_MAX_BUDGET, + !COMPRESS_TIME ? 5 * 60 * 1000L : 30 * 1000L); + MIN_LIGHT_MAINTENANCE_TIME = mParser.getDurationMillis( + KEY_MIN_LIGHT_MAINTENANCE_TIME, + !COMPRESS_TIME ? 5 * 1000L : 1 * 1000L); + MIN_DEEP_MAINTENANCE_TIME = mParser.getDurationMillis( + KEY_MIN_DEEP_MAINTENANCE_TIME, + !COMPRESS_TIME ? 30 * 1000L : 5 * 1000L); + long inactiveTimeoutDefault = (mSmallBatteryDevice ? 15 : 30) * 60 * 1000L; + INACTIVE_TIMEOUT = mParser.getDurationMillis(KEY_INACTIVE_TIMEOUT, + !COMPRESS_TIME ? inactiveTimeoutDefault : (inactiveTimeoutDefault / 10)); + SENSING_TIMEOUT = mParser.getDurationMillis(KEY_SENSING_TIMEOUT, + !COMPRESS_TIME ? 4 * 60 * 1000L : 60 * 1000L); + LOCATING_TIMEOUT = mParser.getDurationMillis(KEY_LOCATING_TIMEOUT, + !COMPRESS_TIME ? 30 * 1000L : 15 * 1000L); + LOCATION_ACCURACY = mParser.getFloat(KEY_LOCATION_ACCURACY, 20); + MOTION_INACTIVE_TIMEOUT = mParser.getDurationMillis(KEY_MOTION_INACTIVE_TIMEOUT, + !COMPRESS_TIME ? 10 * 60 * 1000L : 60 * 1000L); + long idleAfterInactiveTimeout = (mSmallBatteryDevice ? 15 : 30) * 60 * 1000L; + IDLE_AFTER_INACTIVE_TIMEOUT = mParser.getDurationMillis( + KEY_IDLE_AFTER_INACTIVE_TIMEOUT, + !COMPRESS_TIME ? idleAfterInactiveTimeout + : (idleAfterInactiveTimeout / 10)); + IDLE_PENDING_TIMEOUT = mParser.getDurationMillis(KEY_IDLE_PENDING_TIMEOUT, + !COMPRESS_TIME ? 5 * 60 * 1000L : 30 * 1000L); + MAX_IDLE_PENDING_TIMEOUT = mParser.getDurationMillis(KEY_MAX_IDLE_PENDING_TIMEOUT, + !COMPRESS_TIME ? 10 * 60 * 1000L : 60 * 1000L); + IDLE_PENDING_FACTOR = mParser.getFloat(KEY_IDLE_PENDING_FACTOR, + 2f); + QUICK_DOZE_DELAY_TIMEOUT = mParser.getDurationMillis( + KEY_QUICK_DOZE_DELAY_TIMEOUT, !COMPRESS_TIME ? 60 * 1000L : 15 * 1000L); + IDLE_TIMEOUT = mParser.getDurationMillis(KEY_IDLE_TIMEOUT, + !COMPRESS_TIME ? 60 * 60 * 1000L : 6 * 60 * 1000L); + MAX_IDLE_TIMEOUT = mParser.getDurationMillis(KEY_MAX_IDLE_TIMEOUT, + !COMPRESS_TIME ? 6 * 60 * 60 * 1000L : 30 * 60 * 1000L); + IDLE_FACTOR = mParser.getFloat(KEY_IDLE_FACTOR, + 2f); + MIN_TIME_TO_ALARM = mParser.getDurationMillis(KEY_MIN_TIME_TO_ALARM, + !COMPRESS_TIME ? 60 * 60 * 1000L : 6 * 60 * 1000L); + MAX_TEMP_APP_WHITELIST_DURATION = mParser.getDurationMillis( + KEY_MAX_TEMP_APP_WHITELIST_DURATION, 5 * 60 * 1000L); + MMS_TEMP_APP_WHITELIST_DURATION = mParser.getDurationMillis( + KEY_MMS_TEMP_APP_WHITELIST_DURATION, 60 * 1000L); + SMS_TEMP_APP_WHITELIST_DURATION = mParser.getDurationMillis( + KEY_SMS_TEMP_APP_WHITELIST_DURATION, 20 * 1000L); + NOTIFICATION_WHITELIST_DURATION = mParser.getDurationMillis( + KEY_NOTIFICATION_WHITELIST_DURATION, 30 * 1000L); + WAIT_FOR_UNLOCK = mParser.getBoolean(KEY_WAIT_FOR_UNLOCK, true); + PRE_IDLE_FACTOR_LONG = mParser.getFloat(KEY_PRE_IDLE_FACTOR_LONG, 1.67f); + PRE_IDLE_FACTOR_SHORT = mParser.getFloat(KEY_PRE_IDLE_FACTOR_SHORT, 0.33f); + } + } + + void dump(PrintWriter pw) { + pw.println(" Settings:"); + + pw.print(" "); pw.print(KEY_LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT); pw.print("="); + TimeUtils.formatDuration(LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_LIGHT_PRE_IDLE_TIMEOUT); pw.print("="); + TimeUtils.formatDuration(LIGHT_PRE_IDLE_TIMEOUT, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_LIGHT_IDLE_TIMEOUT); pw.print("="); + TimeUtils.formatDuration(LIGHT_IDLE_TIMEOUT, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_LIGHT_IDLE_FACTOR); pw.print("="); + pw.print(LIGHT_IDLE_FACTOR); + pw.println(); + + pw.print(" "); pw.print(KEY_LIGHT_MAX_IDLE_TIMEOUT); pw.print("="); + TimeUtils.formatDuration(LIGHT_MAX_IDLE_TIMEOUT, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_LIGHT_IDLE_MAINTENANCE_MIN_BUDGET); pw.print("="); + TimeUtils.formatDuration(LIGHT_IDLE_MAINTENANCE_MIN_BUDGET, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_LIGHT_IDLE_MAINTENANCE_MAX_BUDGET); pw.print("="); + TimeUtils.formatDuration(LIGHT_IDLE_MAINTENANCE_MAX_BUDGET, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_MIN_LIGHT_MAINTENANCE_TIME); pw.print("="); + TimeUtils.formatDuration(MIN_LIGHT_MAINTENANCE_TIME, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_MIN_DEEP_MAINTENANCE_TIME); pw.print("="); + TimeUtils.formatDuration(MIN_DEEP_MAINTENANCE_TIME, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_INACTIVE_TIMEOUT); pw.print("="); + TimeUtils.formatDuration(INACTIVE_TIMEOUT, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_SENSING_TIMEOUT); pw.print("="); + TimeUtils.formatDuration(SENSING_TIMEOUT, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_LOCATING_TIMEOUT); pw.print("="); + TimeUtils.formatDuration(LOCATING_TIMEOUT, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_LOCATION_ACCURACY); pw.print("="); + pw.print(LOCATION_ACCURACY); pw.print("m"); + pw.println(); + + pw.print(" "); pw.print(KEY_MOTION_INACTIVE_TIMEOUT); pw.print("="); + TimeUtils.formatDuration(MOTION_INACTIVE_TIMEOUT, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_IDLE_AFTER_INACTIVE_TIMEOUT); pw.print("="); + TimeUtils.formatDuration(IDLE_AFTER_INACTIVE_TIMEOUT, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_IDLE_PENDING_TIMEOUT); pw.print("="); + TimeUtils.formatDuration(IDLE_PENDING_TIMEOUT, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_MAX_IDLE_PENDING_TIMEOUT); pw.print("="); + TimeUtils.formatDuration(MAX_IDLE_PENDING_TIMEOUT, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_IDLE_PENDING_FACTOR); pw.print("="); + pw.println(IDLE_PENDING_FACTOR); + + pw.print(" "); pw.print(KEY_QUICK_DOZE_DELAY_TIMEOUT); pw.print("="); + TimeUtils.formatDuration(QUICK_DOZE_DELAY_TIMEOUT, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_IDLE_TIMEOUT); pw.print("="); + TimeUtils.formatDuration(IDLE_TIMEOUT, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_MAX_IDLE_TIMEOUT); pw.print("="); + TimeUtils.formatDuration(MAX_IDLE_TIMEOUT, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_IDLE_FACTOR); pw.print("="); + pw.println(IDLE_FACTOR); + + pw.print(" "); pw.print(KEY_MIN_TIME_TO_ALARM); pw.print("="); + TimeUtils.formatDuration(MIN_TIME_TO_ALARM, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_MAX_TEMP_APP_WHITELIST_DURATION); pw.print("="); + TimeUtils.formatDuration(MAX_TEMP_APP_WHITELIST_DURATION, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_MMS_TEMP_APP_WHITELIST_DURATION); pw.print("="); + TimeUtils.formatDuration(MMS_TEMP_APP_WHITELIST_DURATION, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_SMS_TEMP_APP_WHITELIST_DURATION); pw.print("="); + TimeUtils.formatDuration(SMS_TEMP_APP_WHITELIST_DURATION, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_NOTIFICATION_WHITELIST_DURATION); pw.print("="); + TimeUtils.formatDuration(NOTIFICATION_WHITELIST_DURATION, pw); + pw.println(); + + pw.print(" "); pw.print(KEY_WAIT_FOR_UNLOCK); pw.print("="); + pw.println(WAIT_FOR_UNLOCK); + + pw.print(" "); pw.print(KEY_PRE_IDLE_FACTOR_LONG); pw.print("="); + pw.println(PRE_IDLE_FACTOR_LONG); + + pw.print(" "); pw.print(KEY_PRE_IDLE_FACTOR_SHORT); pw.print("="); + pw.println(PRE_IDLE_FACTOR_SHORT); + } + } + + private Constants mConstants; + + @Override + public void onAnyMotionResult(int result) { + if (DEBUG) Slog.d(TAG, "onAnyMotionResult(" + result + ")"); + if (result != AnyMotionDetector.RESULT_UNKNOWN) { + synchronized (this) { + cancelSensingTimeoutAlarmLocked(); + } + } + if ((result == AnyMotionDetector.RESULT_MOVED) || + (result == AnyMotionDetector.RESULT_UNKNOWN)) { + synchronized (this) { + handleMotionDetectedLocked(mConstants.INACTIVE_TIMEOUT, "non_stationary"); + } + } else if (result == AnyMotionDetector.RESULT_STATIONARY) { + if (mState == STATE_SENSING) { + // If we are currently sensing, it is time to move to locating. + synchronized (this) { + mNotMoving = true; + stepIdleStateLocked("s:stationary"); + } + } else if (mState == STATE_LOCATING) { + // If we are currently locating, note that we are not moving and step + // if we have located the position. + synchronized (this) { + mNotMoving = true; + if (mLocated) { + stepIdleStateLocked("s:stationary"); + } + } + } + } + } + + private static final int MSG_WRITE_CONFIG = 1; + private static final int MSG_REPORT_IDLE_ON = 2; + private static final int MSG_REPORT_IDLE_ON_LIGHT = 3; + private static final int MSG_REPORT_IDLE_OFF = 4; + private static final int MSG_REPORT_ACTIVE = 5; + private static final int MSG_TEMP_APP_WHITELIST_TIMEOUT = 6; + private static final int MSG_FINISH_IDLE_OP = 8; + private static final int MSG_REPORT_TEMP_APP_WHITELIST_CHANGED = 9; + private static final int MSG_SEND_CONSTRAINT_MONITORING = 10; + private static final int MSG_UPDATE_PRE_IDLE_TIMEOUT_FACTOR = 11; + private static final int MSG_RESET_PRE_IDLE_TIMEOUT_FACTOR = 12; + + final class MyHandler extends Handler { + MyHandler(Looper looper) { + super(looper); + } + + @Override public void handleMessage(Message msg) { + if (DEBUG) Slog.d(TAG, "handleMessage(" + msg.what + ")"); + switch (msg.what) { + case MSG_WRITE_CONFIG: { + // Does not hold a wakelock. Just let this happen whenever. + handleWriteConfigFile(); + } break; + case MSG_REPORT_IDLE_ON: + case MSG_REPORT_IDLE_ON_LIGHT: { + // mGoingIdleWakeLock is held at this point + EventLogTags.writeDeviceIdleOnStart(); + final boolean deepChanged; + final boolean lightChanged; + if (msg.what == MSG_REPORT_IDLE_ON) { + deepChanged = mLocalPowerManager.setDeviceIdleMode(true); + lightChanged = mLocalPowerManager.setLightDeviceIdleMode(false); + } else { + deepChanged = mLocalPowerManager.setDeviceIdleMode(false); + lightChanged = mLocalPowerManager.setLightDeviceIdleMode(true); + } + try { + mNetworkPolicyManager.setDeviceIdleMode(true); + mBatteryStats.noteDeviceIdleMode(msg.what == MSG_REPORT_IDLE_ON + ? BatteryStats.DEVICE_IDLE_MODE_DEEP + : BatteryStats.DEVICE_IDLE_MODE_LIGHT, null, Process.myUid()); + } catch (RemoteException e) { + } + if (deepChanged) { + getContext().sendBroadcastAsUser(mIdleIntent, UserHandle.ALL); + } + if (lightChanged) { + getContext().sendBroadcastAsUser(mLightIdleIntent, UserHandle.ALL); + } + EventLogTags.writeDeviceIdleOnComplete(); + mGoingIdleWakeLock.release(); + } break; + case MSG_REPORT_IDLE_OFF: { + // mActiveIdleWakeLock is held at this point + EventLogTags.writeDeviceIdleOffStart("unknown"); + final boolean deepChanged = mLocalPowerManager.setDeviceIdleMode(false); + final boolean lightChanged = mLocalPowerManager.setLightDeviceIdleMode(false); + try { + mNetworkPolicyManager.setDeviceIdleMode(false); + mBatteryStats.noteDeviceIdleMode(BatteryStats.DEVICE_IDLE_MODE_OFF, + null, Process.myUid()); + } catch (RemoteException e) { + } + if (deepChanged) { + incActiveIdleOps(); + getContext().sendOrderedBroadcastAsUser(mIdleIntent, UserHandle.ALL, + null, mIdleStartedDoneReceiver, null, 0, null, null); + } + if (lightChanged) { + incActiveIdleOps(); + getContext().sendOrderedBroadcastAsUser(mLightIdleIntent, UserHandle.ALL, + null, mIdleStartedDoneReceiver, null, 0, null, null); + } + // Always start with one active op for the message being sent here. + // Now we are done! + decActiveIdleOps(); + EventLogTags.writeDeviceIdleOffComplete(); + } break; + case MSG_REPORT_ACTIVE: { + // The device is awake at this point, so no wakelock necessary. + String activeReason = (String)msg.obj; + int activeUid = msg.arg1; + EventLogTags.writeDeviceIdleOffStart( + activeReason != null ? activeReason : "unknown"); + final boolean deepChanged = mLocalPowerManager.setDeviceIdleMode(false); + final boolean lightChanged = mLocalPowerManager.setLightDeviceIdleMode(false); + try { + mNetworkPolicyManager.setDeviceIdleMode(false); + mBatteryStats.noteDeviceIdleMode(BatteryStats.DEVICE_IDLE_MODE_OFF, + activeReason, activeUid); + } catch (RemoteException e) { + } + if (deepChanged) { + getContext().sendBroadcastAsUser(mIdleIntent, UserHandle.ALL); + } + if (lightChanged) { + getContext().sendBroadcastAsUser(mLightIdleIntent, UserHandle.ALL); + } + EventLogTags.writeDeviceIdleOffComplete(); + } break; + case MSG_TEMP_APP_WHITELIST_TIMEOUT: { + // TODO: What is keeping the device awake at this point? Does it need to be? + int appId = msg.arg1; + checkTempAppWhitelistTimeout(appId); + } break; + case MSG_FINISH_IDLE_OP: { + // mActiveIdleWakeLock is held at this point + decActiveIdleOps(); + } break; + case MSG_REPORT_TEMP_APP_WHITELIST_CHANGED: { + final int appId = msg.arg1; + final boolean added = (msg.arg2 == 1); + mNetworkPolicyManagerInternal.onTempPowerSaveWhitelistChange(appId, added); + } break; + case MSG_SEND_CONSTRAINT_MONITORING: { + final IDeviceIdleConstraint constraint = (IDeviceIdleConstraint) msg.obj; + final boolean monitoring = (msg.arg1 == 1); + if (monitoring) { + constraint.startMonitoring(); + } else { + constraint.stopMonitoring(); + } + } break; + case MSG_UPDATE_PRE_IDLE_TIMEOUT_FACTOR: { + updatePreIdleFactor(); + } break; + case MSG_RESET_PRE_IDLE_TIMEOUT_FACTOR: { + updatePreIdleFactor(); + maybeDoImmediateMaintenance(); + } break; + } + } + } + + final MyHandler mHandler; + + BinderService mBinderService; + + private final class BinderService extends IDeviceIdleController.Stub { + @Override public void addPowerSaveWhitelistApp(String name) { + if (DEBUG) { + Slog.i(TAG, "addPowerSaveWhitelistApp(name = " + name + ")"); + } + getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, + null); + long ident = Binder.clearCallingIdentity(); + try { + addPowerSaveWhitelistAppInternal(name); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override public void removePowerSaveWhitelistApp(String name) { + if (DEBUG) { + Slog.i(TAG, "removePowerSaveWhitelistApp(name = " + name + ")"); + } + getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, + null); + long ident = Binder.clearCallingIdentity(); + try { + removePowerSaveWhitelistAppInternal(name); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override public void removeSystemPowerWhitelistApp(String name) { + if (DEBUG) { + Slog.d(TAG, "removeAppFromSystemWhitelist(name = " + name + ")"); + } + getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, + null); + long ident = Binder.clearCallingIdentity(); + try { + removeSystemPowerWhitelistAppInternal(name); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override public void restoreSystemPowerWhitelistApp(String name) { + if (DEBUG) { + Slog.d(TAG, "restoreAppToSystemWhitelist(name = " + name + ")"); + } + getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, + null); + long ident = Binder.clearCallingIdentity(); + try { + restoreSystemPowerWhitelistAppInternal(name); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + public String[] getRemovedSystemPowerWhitelistApps() { + return getRemovedSystemPowerWhitelistAppsInternal(); + } + + @Override public String[] getSystemPowerWhitelistExceptIdle() { + return getSystemPowerWhitelistExceptIdleInternal(); + } + + @Override public String[] getSystemPowerWhitelist() { + return getSystemPowerWhitelistInternal(); + } + + @Override public String[] getUserPowerWhitelist() { + return getUserPowerWhitelistInternal(); + } + + @Override public String[] getFullPowerWhitelistExceptIdle() { + return getFullPowerWhitelistExceptIdleInternal(); + } + + @Override public String[] getFullPowerWhitelist() { + return getFullPowerWhitelistInternal(); + } + + @Override public int[] getAppIdWhitelistExceptIdle() { + return getAppIdWhitelistExceptIdleInternal(); + } + + @Override public int[] getAppIdWhitelist() { + return getAppIdWhitelistInternal(); + } + + @Override public int[] getAppIdUserWhitelist() { + return getAppIdUserWhitelistInternal(); + } + + @Override public int[] getAppIdTempWhitelist() { + return getAppIdTempWhitelistInternal(); + } + + @Override public boolean isPowerSaveWhitelistExceptIdleApp(String name) { + return isPowerSaveWhitelistExceptIdleAppInternal(name); + } + + @Override public boolean isPowerSaveWhitelistApp(String name) { + return isPowerSaveWhitelistAppInternal(name); + } + + @Override public void addPowerSaveTempWhitelistApp(String packageName, long duration, + int userId, String reason) throws RemoteException { + addPowerSaveTempWhitelistAppChecked(packageName, duration, userId, reason); + } + + @Override public long addPowerSaveTempWhitelistAppForMms(String packageName, + int userId, String reason) throws RemoteException { + long duration = mConstants.MMS_TEMP_APP_WHITELIST_DURATION; + addPowerSaveTempWhitelistAppChecked(packageName, duration, userId, reason); + return duration; + } + + @Override public long addPowerSaveTempWhitelistAppForSms(String packageName, + int userId, String reason) throws RemoteException { + long duration = mConstants.SMS_TEMP_APP_WHITELIST_DURATION; + addPowerSaveTempWhitelistAppChecked(packageName, duration, userId, reason); + return duration; + } + + @Override public void exitIdle(String reason) { + getContext().enforceCallingOrSelfPermission(Manifest.permission.DEVICE_POWER, + null); + long ident = Binder.clearCallingIdentity(); + try { + exitIdleInternal(reason); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override public int setPreIdleTimeoutMode(int mode) { + getContext().enforceCallingOrSelfPermission(Manifest.permission.DEVICE_POWER, + null); + long ident = Binder.clearCallingIdentity(); + try { + return DeviceIdleController.this.setPreIdleTimeoutMode(mode); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override public void resetPreIdleTimeoutMode() { + getContext().enforceCallingOrSelfPermission(Manifest.permission.DEVICE_POWER, + null); + long ident = Binder.clearCallingIdentity(); + try { + DeviceIdleController.this.resetPreIdleTimeoutMode(); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + DeviceIdleController.this.dump(fd, pw, args); + } + + @Override public void onShellCommand(FileDescriptor in, FileDescriptor out, + FileDescriptor err, String[] args, ShellCallback callback, ResultReceiver resultReceiver) { + (new Shell()).exec(this, in, out, err, args, callback, resultReceiver); + } + } + + private class LocalService implements DeviceIdleInternal { + @Override + public void onConstraintStateChanged(IDeviceIdleConstraint constraint, boolean active) { + synchronized (DeviceIdleController.this) { + onConstraintStateChangedLocked(constraint, active); + } + } + + @Override + public void registerDeviceIdleConstraint(IDeviceIdleConstraint constraint, String name, + @IDeviceIdleConstraint.MinimumState int minState) { + registerDeviceIdleConstraintInternal(constraint, name, minState); + } + + @Override + public void unregisterDeviceIdleConstraint(IDeviceIdleConstraint constraint) { + unregisterDeviceIdleConstraintInternal(constraint); + } + + @Override + public void exitIdle(String reason) { + exitIdleInternal(reason); + } + + // duration in milliseconds + @Override + public void addPowerSaveTempWhitelistApp(int callingUid, String packageName, + long duration, int userId, boolean sync, String reason) { + addPowerSaveTempWhitelistAppInternal(callingUid, packageName, duration, + userId, sync, reason); + } + + // duration in milliseconds + @Override + public void addPowerSaveTempWhitelistAppDirect(int uid, long duration, boolean sync, + String reason) { + addPowerSaveTempWhitelistAppDirectInternal(0, uid, duration, sync, reason); + } + + // duration in milliseconds + @Override + public long getNotificationWhitelistDuration() { + return mConstants.NOTIFICATION_WHITELIST_DURATION; + } + + @Override + public void setJobsActive(boolean active) { + DeviceIdleController.this.setJobsActive(active); + } + + // Up-call from alarm manager. + @Override + public void setAlarmsActive(boolean active) { + DeviceIdleController.this.setAlarmsActive(active); + } + + /** Is the app on any of the power save whitelists, whether system or user? */ + @Override + public boolean isAppOnWhitelist(int appid) { + return DeviceIdleController.this.isAppOnWhitelistInternal(appid); + } + + /** + * Returns the array of app ids whitelisted by user. Take care not to + * modify this, as it is a reference to the original copy. But the reference + * can change when the list changes, so it needs to be re-acquired when + * {@link PowerManager#ACTION_POWER_SAVE_WHITELIST_CHANGED} is sent. + */ + @Override + public int[] getPowerSaveWhitelistUserAppIds() { + return DeviceIdleController.this.getPowerSaveWhitelistUserAppIds(); + } + + @Override + public int[] getPowerSaveTempWhitelistAppIds() { + return DeviceIdleController.this.getAppIdTempWhitelistInternal(); + } + } + + static class Injector { + private final Context mContext; + private ConnectivityService mConnectivityService; + private Constants mConstants; + private LocationManager mLocationManager; + + Injector(Context ctx) { + mContext = ctx; + } + + AlarmManager getAlarmManager() { + return mContext.getSystemService(AlarmManager.class); + } + + AnyMotionDetector getAnyMotionDetector(Handler handler, SensorManager sm, + AnyMotionDetector.DeviceIdleCallback callback, float angleThreshold) { + return new AnyMotionDetector(getPowerManager(), handler, sm, callback, angleThreshold); + } + + AppStateTracker getAppStateTracker(Context ctx, Looper looper) { + return new AppStateTracker(ctx, looper); + } + + ConnectivityService getConnectivityService() { + if (mConnectivityService == null) { + mConnectivityService = (ConnectivityService) ServiceManager.getService( + Context.CONNECTIVITY_SERVICE); + } + return mConnectivityService; + } + + Constants getConstants(DeviceIdleController controller, Handler handler, + ContentResolver resolver) { + if (mConstants == null) { + mConstants = controller.new Constants(handler, resolver); + } + return mConstants; + } + + + /** Returns the current elapsed realtime in milliseconds. */ + long getElapsedRealtime() { + return SystemClock.elapsedRealtime(); + } + + LocationManager getLocationManager() { + if (mLocationManager == null) { + mLocationManager = mContext.getSystemService(LocationManager.class); + } + return mLocationManager; + } + + MyHandler getHandler(DeviceIdleController controller) { + return controller.new MyHandler(BackgroundThread.getHandler().getLooper()); + } + + PowerManager getPowerManager() { + return mContext.getSystemService(PowerManager.class); + } + + SensorManager getSensorManager() { + return mContext.getSystemService(SensorManager.class); + } + + ConstraintController getConstraintController(Handler handler, + DeviceIdleInternal localService) { + if (mContext.getPackageManager() + .hasSystemFeature(PackageManager.FEATURE_LEANBACK_ONLY)) { + return new TvConstraintController(mContext, handler); + } + return null; + } + + boolean useMotionSensor() { + return mContext.getResources().getBoolean( + com.android.internal.R.bool.config_autoPowerModeUseMotionSensor); + } + } + + private final Injector mInjector; + + private ActivityTaskManagerInternal.ScreenObserver mScreenObserver = + new ActivityTaskManagerInternal.ScreenObserver() { + @Override + public void onAwakeStateChanged(boolean isAwake) { } + + @Override + public void onKeyguardStateChanged(boolean isShowing) { + synchronized (DeviceIdleController.this) { + DeviceIdleController.this.keyguardShowingLocked(isShowing); + } + } + }; + + @VisibleForTesting DeviceIdleController(Context context, Injector injector) { + super(context); + mInjector = injector; + mConfigFile = new AtomicFile(new File(getSystemDir(), "deviceidle.xml")); + mHandler = mInjector.getHandler(this); + mAppStateTracker = mInjector.getAppStateTracker(context, FgThread.get().getLooper()); + LocalServices.addService(AppStateTracker.class, mAppStateTracker); + mUseMotionSensor = mInjector.useMotionSensor(); + } + + public DeviceIdleController(Context context) { + this(context, new Injector(context)); + } + + boolean isAppOnWhitelistInternal(int appid) { + synchronized (this) { + return Arrays.binarySearch(mPowerSaveWhitelistAllAppIdArray, appid) >= 0; + } + } + + int[] getPowerSaveWhitelistUserAppIds() { + synchronized (this) { + return mPowerSaveWhitelistUserAppIdArray; + } + } + + private static File getSystemDir() { + return new File(Environment.getDataDirectory(), "system"); + } + + @Override + public void onStart() { + final PackageManager pm = getContext().getPackageManager(); + + synchronized (this) { + mLightEnabled = mDeepEnabled = getContext().getResources().getBoolean( + com.android.internal.R.bool.config_enableAutoPowerModes); + SystemConfig sysConfig = SystemConfig.getInstance(); + ArraySet<String> allowPowerExceptIdle = sysConfig.getAllowInPowerSaveExceptIdle(); + for (int i=0; i<allowPowerExceptIdle.size(); i++) { + String pkg = allowPowerExceptIdle.valueAt(i); + try { + ApplicationInfo ai = pm.getApplicationInfo(pkg, + PackageManager.MATCH_SYSTEM_ONLY); + int appid = UserHandle.getAppId(ai.uid); + mPowerSaveWhitelistAppsExceptIdle.put(ai.packageName, appid); + mPowerSaveWhitelistSystemAppIdsExceptIdle.put(appid, true); + } catch (PackageManager.NameNotFoundException e) { + } + } + ArraySet<String> allowPower = sysConfig.getAllowInPowerSave(); + for (int i=0; i<allowPower.size(); i++) { + String pkg = allowPower.valueAt(i); + try { + ApplicationInfo ai = pm.getApplicationInfo(pkg, + PackageManager.MATCH_SYSTEM_ONLY); + int appid = UserHandle.getAppId(ai.uid); + // These apps are on both the whitelist-except-idle as well + // as the full whitelist, so they apply in all cases. + mPowerSaveWhitelistAppsExceptIdle.put(ai.packageName, appid); + mPowerSaveWhitelistSystemAppIdsExceptIdle.put(appid, true); + mPowerSaveWhitelistApps.put(ai.packageName, appid); + mPowerSaveWhitelistSystemAppIds.put(appid, true); + } catch (PackageManager.NameNotFoundException e) { + } + } + + mConstants = mInjector.getConstants(this, mHandler, getContext().getContentResolver()); + + readConfigFileLocked(); + updateWhitelistAppIdsLocked(); + + mNetworkConnected = true; + mScreenOn = true; + mScreenLocked = false; + // Start out assuming we are charging. If we aren't, we will at least get + // a battery update the next time the level drops. + mCharging = true; + mActiveReason = ACTIVE_REASON_UNKNOWN; + mState = STATE_ACTIVE; + mLightState = LIGHT_STATE_ACTIVE; + mInactiveTimeout = mConstants.INACTIVE_TIMEOUT; + mPreIdleFactor = 1.0f; + mLastPreIdleFactor = 1.0f; + } + + mBinderService = new BinderService(); + publishBinderService(Context.DEVICE_IDLE_CONTROLLER, mBinderService); + publishLocalService(DeviceIdleInternal.class, new LocalService()); + } + + @Override + public void onBootPhase(int phase) { + if (phase == PHASE_SYSTEM_SERVICES_READY) { + synchronized (this) { + mAlarmManager = mInjector.getAlarmManager(); + mLocalAlarmManager = getLocalService(AlarmManagerInternal.class); + mBatteryStats = BatteryStatsService.getService(); + mLocalActivityManager = getLocalService(ActivityManagerInternal.class); + mLocalActivityTaskManager = getLocalService(ActivityTaskManagerInternal.class); + mLocalPowerManager = getLocalService(PowerManagerInternal.class); + mPowerManager = mInjector.getPowerManager(); + mActiveIdleWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, + "deviceidle_maint"); + mActiveIdleWakeLock.setReferenceCounted(false); + mGoingIdleWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, + "deviceidle_going_idle"); + mGoingIdleWakeLock.setReferenceCounted(true); + mNetworkPolicyManager = INetworkPolicyManager.Stub.asInterface( + ServiceManager.getService(Context.NETWORK_POLICY_SERVICE)); + mNetworkPolicyManagerInternal = getLocalService(NetworkPolicyManagerInternal.class); + mSensorManager = mInjector.getSensorManager(); + + if (mUseMotionSensor) { + int sigMotionSensorId = getContext().getResources().getInteger( + com.android.internal.R.integer.config_autoPowerModeAnyMotionSensor); + if (sigMotionSensorId > 0) { + mMotionSensor = mSensorManager.getDefaultSensor(sigMotionSensorId, true); + } + if (mMotionSensor == null && getContext().getResources().getBoolean( + com.android.internal.R.bool.config_autoPowerModePreferWristTilt)) { + mMotionSensor = mSensorManager.getDefaultSensor( + Sensor.TYPE_WRIST_TILT_GESTURE, true); + } + if (mMotionSensor == null) { + // As a last ditch, fall back to SMD. + mMotionSensor = mSensorManager.getDefaultSensor( + Sensor.TYPE_SIGNIFICANT_MOTION, true); + } + } + + if (getContext().getResources().getBoolean( + com.android.internal.R.bool.config_autoPowerModePrefetchLocation)) { + mLocationRequest = new LocationRequest() + .setQuality(LocationRequest.ACCURACY_FINE) + .setInterval(0) + .setFastestInterval(0) + .setNumUpdates(1); + } + + mConstraintController = mInjector.getConstraintController( + mHandler, getLocalService(LocalService.class)); + if (mConstraintController != null) { + mConstraintController.start(); + } + + float angleThreshold = getContext().getResources().getInteger( + com.android.internal.R.integer.config_autoPowerModeThresholdAngle) / 100f; + mAnyMotionDetector = mInjector.getAnyMotionDetector(mHandler, mSensorManager, this, + angleThreshold); + + mAppStateTracker.onSystemServicesReady(); + + mIdleIntent = new Intent(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED); + mIdleIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY + | Intent.FLAG_RECEIVER_FOREGROUND); + mLightIdleIntent = new Intent(PowerManager.ACTION_LIGHT_DEVICE_IDLE_MODE_CHANGED); + mLightIdleIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY + | Intent.FLAG_RECEIVER_FOREGROUND); + + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_BATTERY_CHANGED); + getContext().registerReceiver(mReceiver, filter); + + filter = new IntentFilter(); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addDataScheme("package"); + getContext().registerReceiver(mReceiver, filter); + + filter = new IntentFilter(); + filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); + getContext().registerReceiver(mReceiver, filter); + + filter = new IntentFilter(); + filter.addAction(Intent.ACTION_SCREEN_OFF); + filter.addAction(Intent.ACTION_SCREEN_ON); + getContext().registerReceiver(mInteractivityReceiver, filter); + + mLocalActivityManager.setDeviceIdleWhitelist( + mPowerSaveWhitelistAllAppIdArray, mPowerSaveWhitelistExceptIdleAppIdArray); + mLocalPowerManager.setDeviceIdleWhitelist(mPowerSaveWhitelistAllAppIdArray); + + mLocalPowerManager.registerLowPowerModeObserver(ServiceType.QUICK_DOZE, + state -> { + synchronized (DeviceIdleController.this) { + updateQuickDozeFlagLocked(state.batterySaverEnabled); + } + }); + updateQuickDozeFlagLocked( + mLocalPowerManager.getLowPowerState( + ServiceType.QUICK_DOZE).batterySaverEnabled); + + mLocalActivityTaskManager.registerScreenObserver(mScreenObserver); + + passWhiteListsToForceAppStandbyTrackerLocked(); + updateInteractivityLocked(); + } + updateConnectivityState(null); + } + } + + @VisibleForTesting + boolean hasMotionSensor() { + return mUseMotionSensor && mMotionSensor != null; + } + + private void registerDeviceIdleConstraintInternal(IDeviceIdleConstraint constraint, + final String name, final int type) { + final int minState; + switch (type) { + case IDeviceIdleConstraint.ACTIVE: + minState = STATE_ACTIVE; + break; + case IDeviceIdleConstraint.SENSING_OR_ABOVE: + minState = STATE_SENSING; + break; + default: + Slog.wtf(TAG, "Registering device-idle constraint with invalid type: " + type); + return; + } + synchronized (this) { + if (mConstraints.containsKey(constraint)) { + Slog.e(TAG, "Re-registering device-idle constraint: " + constraint + "."); + return; + } + DeviceIdleConstraintTracker tracker = new DeviceIdleConstraintTracker(name, minState); + mConstraints.put(constraint, tracker); + updateActiveConstraintsLocked(); + } + } + + private void unregisterDeviceIdleConstraintInternal(IDeviceIdleConstraint constraint) { + synchronized (this) { + // Artificially force the constraint to inactive to unblock anything waiting for it. + onConstraintStateChangedLocked(constraint, /* active= */ false); + + // Let the constraint know that we are not listening to it any more. + setConstraintMonitoringLocked(constraint, /* monitoring= */ false); + mConstraints.remove(constraint); + } + } + + @GuardedBy("this") + private void onConstraintStateChangedLocked(IDeviceIdleConstraint constraint, boolean active) { + DeviceIdleConstraintTracker tracker = mConstraints.get(constraint); + if (tracker == null) { + Slog.e(TAG, "device-idle constraint " + constraint + " has not been registered."); + return; + } + if (active != tracker.active && tracker.monitoring) { + tracker.active = active; + mNumBlockingConstraints += (tracker.active ? +1 : -1); + if (mNumBlockingConstraints == 0) { + if (mState == STATE_ACTIVE) { + becomeInactiveIfAppropriateLocked(); + } else if (mNextAlarmTime == 0 || mNextAlarmTime < SystemClock.elapsedRealtime()) { + stepIdleStateLocked("s:" + tracker.name); + } + } + } + } + + @GuardedBy("this") + private void setConstraintMonitoringLocked(IDeviceIdleConstraint constraint, boolean monitor) { + DeviceIdleConstraintTracker tracker = mConstraints.get(constraint); + if (tracker.monitoring != monitor) { + tracker.monitoring = monitor; + updateActiveConstraintsLocked(); + // We send the callback on a separate thread instead of just relying on oneway as + // the client could be in the system server with us and cause re-entry problems. + mHandler.obtainMessage(MSG_SEND_CONSTRAINT_MONITORING, + /* monitoring= */ monitor ? 1 : 0, + /* <not used>= */ -1, + /* constraint= */ constraint).sendToTarget(); + } + } + + @GuardedBy("this") + private void updateActiveConstraintsLocked() { + mNumBlockingConstraints = 0; + for (int i = 0; i < mConstraints.size(); i++) { + final IDeviceIdleConstraint constraint = mConstraints.keyAt(i); + final DeviceIdleConstraintTracker tracker = mConstraints.valueAt(i); + final boolean monitoring = (tracker.minState == mState); + if (monitoring != tracker.monitoring) { + setConstraintMonitoringLocked(constraint, monitoring); + tracker.active = monitoring; + } + if (tracker.monitoring && tracker.active) { + mNumBlockingConstraints++; + } + } + } + + public boolean addPowerSaveWhitelistAppInternal(String name) { + synchronized (this) { + try { + ApplicationInfo ai = getContext().getPackageManager().getApplicationInfo(name, + PackageManager.MATCH_ANY_USER); + if (mPowerSaveWhitelistUserApps.put(name, UserHandle.getAppId(ai.uid)) == null) { + reportPowerSaveWhitelistChangedLocked(); + updateWhitelistAppIdsLocked(); + writeConfigFileLocked(); + } + return true; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + } + + public boolean removePowerSaveWhitelistAppInternal(String name) { + synchronized (this) { + if (mPowerSaveWhitelistUserApps.remove(name) != null) { + reportPowerSaveWhitelistChangedLocked(); + updateWhitelistAppIdsLocked(); + writeConfigFileLocked(); + return true; + } + } + return false; + } + + public boolean getPowerSaveWhitelistAppInternal(String name) { + synchronized (this) { + return mPowerSaveWhitelistUserApps.containsKey(name); + } + } + + void resetSystemPowerWhitelistInternal() { + synchronized (this) { + mPowerSaveWhitelistApps.putAll(mRemovedFromSystemWhitelistApps); + mRemovedFromSystemWhitelistApps.clear(); + reportPowerSaveWhitelistChangedLocked(); + updateWhitelistAppIdsLocked(); + writeConfigFileLocked(); + } + } + + public boolean restoreSystemPowerWhitelistAppInternal(String name) { + synchronized (this) { + if (!mRemovedFromSystemWhitelistApps.containsKey(name)) { + return false; + } + mPowerSaveWhitelistApps.put(name, mRemovedFromSystemWhitelistApps.remove(name)); + reportPowerSaveWhitelistChangedLocked(); + updateWhitelistAppIdsLocked(); + writeConfigFileLocked(); + return true; + } + } + + public boolean removeSystemPowerWhitelistAppInternal(String name) { + synchronized (this) { + if (!mPowerSaveWhitelistApps.containsKey(name)) { + return false; + } + mRemovedFromSystemWhitelistApps.put(name, mPowerSaveWhitelistApps.remove(name)); + reportPowerSaveWhitelistChangedLocked(); + updateWhitelistAppIdsLocked(); + writeConfigFileLocked(); + return true; + } + } + + public boolean addPowerSaveWhitelistExceptIdleInternal(String name) { + synchronized (this) { + try { + final ApplicationInfo ai = getContext().getPackageManager().getApplicationInfo(name, + PackageManager.MATCH_ANY_USER); + if (mPowerSaveWhitelistAppsExceptIdle.put(name, UserHandle.getAppId(ai.uid)) + == null) { + mPowerSaveWhitelistUserAppsExceptIdle.add(name); + reportPowerSaveWhitelistChangedLocked(); + mPowerSaveWhitelistExceptIdleAppIdArray = buildAppIdArray( + mPowerSaveWhitelistAppsExceptIdle, mPowerSaveWhitelistUserApps, + mPowerSaveWhitelistExceptIdleAppIds); + + passWhiteListsToForceAppStandbyTrackerLocked(); + } + return true; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + } + + public void resetPowerSaveWhitelistExceptIdleInternal() { + synchronized (this) { + if (mPowerSaveWhitelistAppsExceptIdle.removeAll( + mPowerSaveWhitelistUserAppsExceptIdle)) { + reportPowerSaveWhitelistChangedLocked(); + mPowerSaveWhitelistExceptIdleAppIdArray = buildAppIdArray( + mPowerSaveWhitelistAppsExceptIdle, mPowerSaveWhitelistUserApps, + mPowerSaveWhitelistExceptIdleAppIds); + mPowerSaveWhitelistUserAppsExceptIdle.clear(); + + passWhiteListsToForceAppStandbyTrackerLocked(); + } + } + } + + public boolean getPowerSaveWhitelistExceptIdleInternal(String name) { + synchronized (this) { + return mPowerSaveWhitelistAppsExceptIdle.containsKey(name); + } + } + + public String[] getSystemPowerWhitelistExceptIdleInternal() { + synchronized (this) { + int size = mPowerSaveWhitelistAppsExceptIdle.size(); + String[] apps = new String[size]; + for (int i = 0; i < size; i++) { + apps[i] = mPowerSaveWhitelistAppsExceptIdle.keyAt(i); + } + return apps; + } + } + + public String[] getSystemPowerWhitelistInternal() { + synchronized (this) { + int size = mPowerSaveWhitelistApps.size(); + String[] apps = new String[size]; + for (int i = 0; i < size; i++) { + apps[i] = mPowerSaveWhitelistApps.keyAt(i); + } + return apps; + } + } + + public String[] getRemovedSystemPowerWhitelistAppsInternal() { + synchronized (this) { + int size = mRemovedFromSystemWhitelistApps.size(); + final String[] apps = new String[size]; + for (int i = 0; i < size; i++) { + apps[i] = mRemovedFromSystemWhitelistApps.keyAt(i); + } + return apps; + } + } + + public String[] getUserPowerWhitelistInternal() { + synchronized (this) { + int size = mPowerSaveWhitelistUserApps.size(); + String[] apps = new String[size]; + for (int i = 0; i < mPowerSaveWhitelistUserApps.size(); i++) { + apps[i] = mPowerSaveWhitelistUserApps.keyAt(i); + } + return apps; + } + } + + public String[] getFullPowerWhitelistExceptIdleInternal() { + synchronized (this) { + int size = mPowerSaveWhitelistAppsExceptIdle.size() + mPowerSaveWhitelistUserApps.size(); + String[] apps = new String[size]; + int cur = 0; + for (int i = 0; i < mPowerSaveWhitelistAppsExceptIdle.size(); i++) { + apps[cur] = mPowerSaveWhitelistAppsExceptIdle.keyAt(i); + cur++; + } + for (int i = 0; i < mPowerSaveWhitelistUserApps.size(); i++) { + apps[cur] = mPowerSaveWhitelistUserApps.keyAt(i); + cur++; + } + return apps; + } + } + + public String[] getFullPowerWhitelistInternal() { + synchronized (this) { + int size = mPowerSaveWhitelistApps.size() + mPowerSaveWhitelistUserApps.size(); + String[] apps = new String[size]; + int cur = 0; + for (int i = 0; i < mPowerSaveWhitelistApps.size(); i++) { + apps[cur] = mPowerSaveWhitelistApps.keyAt(i); + cur++; + } + for (int i = 0; i < mPowerSaveWhitelistUserApps.size(); i++) { + apps[cur] = mPowerSaveWhitelistUserApps.keyAt(i); + cur++; + } + return apps; + } + } + + public boolean isPowerSaveWhitelistExceptIdleAppInternal(String packageName) { + synchronized (this) { + return mPowerSaveWhitelistAppsExceptIdle.containsKey(packageName) + || mPowerSaveWhitelistUserApps.containsKey(packageName); + } + } + + public boolean isPowerSaveWhitelistAppInternal(String packageName) { + synchronized (this) { + return mPowerSaveWhitelistApps.containsKey(packageName) + || mPowerSaveWhitelistUserApps.containsKey(packageName); + } + } + + public int[] getAppIdWhitelistExceptIdleInternal() { + synchronized (this) { + return mPowerSaveWhitelistExceptIdleAppIdArray; + } + } + + public int[] getAppIdWhitelistInternal() { + synchronized (this) { + return mPowerSaveWhitelistAllAppIdArray; + } + } + + public int[] getAppIdUserWhitelistInternal() { + synchronized (this) { + return mPowerSaveWhitelistUserAppIdArray; + } + } + + public int[] getAppIdTempWhitelistInternal() { + synchronized (this) { + return mTempWhitelistAppIdArray; + } + } + + void addPowerSaveTempWhitelistAppChecked(String packageName, long duration, + int userId, String reason) throws RemoteException { + getContext().enforceCallingPermission( + Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST, + "No permission to change device idle whitelist"); + final int callingUid = Binder.getCallingUid(); + userId = ActivityManager.getService().handleIncomingUser( + Binder.getCallingPid(), + callingUid, + userId, + /*allowAll=*/ false, + /*requireFull=*/ false, + "addPowerSaveTempWhitelistApp", null); + final long token = Binder.clearCallingIdentity(); + try { + addPowerSaveTempWhitelistAppInternal(callingUid, + packageName, duration, userId, true, reason); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + void removePowerSaveTempWhitelistAppChecked(String packageName, int userId) + throws RemoteException { + getContext().enforceCallingPermission( + Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST, + "No permission to change device idle whitelist"); + final int callingUid = Binder.getCallingUid(); + userId = ActivityManager.getService().handleIncomingUser( + Binder.getCallingPid(), + callingUid, + userId, + /*allowAll=*/ false, + /*requireFull=*/ false, + "removePowerSaveTempWhitelistApp", null); + final long token = Binder.clearCallingIdentity(); + try { + removePowerSaveTempWhitelistAppInternal(packageName, userId); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + /** + * Adds an app to the temporary whitelist and resets the endTime for granting the + * app an exemption to access network and acquire wakelocks. + */ + void addPowerSaveTempWhitelistAppInternal(int callingUid, String packageName, + long duration, int userId, boolean sync, String reason) { + try { + int uid = getContext().getPackageManager().getPackageUidAsUser(packageName, userId); + addPowerSaveTempWhitelistAppDirectInternal(callingUid, uid, duration, sync, reason); + } catch (NameNotFoundException e) { + } + } + + /** + * Adds an app to the temporary whitelist and resets the endTime for granting the + * app an exemption to access network and acquire wakelocks. + */ + void addPowerSaveTempWhitelistAppDirectInternal(int callingUid, int uid, + long duration, boolean sync, String reason) { + final long timeNow = SystemClock.elapsedRealtime(); + boolean informWhitelistChanged = false; + int appId = UserHandle.getAppId(uid); + synchronized (this) { + int callingAppId = UserHandle.getAppId(callingUid); + if (callingAppId >= Process.FIRST_APPLICATION_UID) { + if (!mPowerSaveWhitelistSystemAppIds.get(callingAppId)) { + throw new SecurityException("Calling app " + UserHandle.formatUid(callingUid) + + " is not on whitelist"); + } + } + duration = Math.min(duration, mConstants.MAX_TEMP_APP_WHITELIST_DURATION); + Pair<MutableLong, String> entry = mTempWhitelistAppIdEndTimes.get(appId); + final boolean newEntry = entry == null; + // Set the new end time + if (newEntry) { + entry = new Pair<>(new MutableLong(0), reason); + mTempWhitelistAppIdEndTimes.put(appId, entry); + } + entry.first.value = timeNow + duration; + if (DEBUG) { + Slog.d(TAG, "Adding AppId " + appId + " to temp whitelist. New entry: " + newEntry); + } + if (newEntry) { + // No pending timeout for the app id, post a delayed message + try { + mBatteryStats.noteEvent(BatteryStats.HistoryItem.EVENT_TEMP_WHITELIST_START, + reason, uid); + } catch (RemoteException e) { + } + postTempActiveTimeoutMessage(appId, duration); + updateTempWhitelistAppIdsLocked(appId, true); + if (sync) { + informWhitelistChanged = true; + } else { + mHandler.obtainMessage(MSG_REPORT_TEMP_APP_WHITELIST_CHANGED, appId, 1) + .sendToTarget(); + } + reportTempWhitelistChangedLocked(); + } + } + if (informWhitelistChanged) { + mNetworkPolicyManagerInternal.onTempPowerSaveWhitelistChange(appId, true); + } + } + + /** + * Removes an app from the temporary whitelist and notifies the observers. + */ + private void removePowerSaveTempWhitelistAppInternal(String packageName, int userId) { + try { + final int uid = getContext().getPackageManager().getPackageUidAsUser( + packageName, userId); + final int appId = UserHandle.getAppId(uid); + removePowerSaveTempWhitelistAppDirectInternal(appId); + } catch (NameNotFoundException e) { + } + } + + private void removePowerSaveTempWhitelistAppDirectInternal(int appId) { + synchronized (this) { + final int idx = mTempWhitelistAppIdEndTimes.indexOfKey(appId); + if (idx < 0) { + // Nothing else to do + return; + } + final String reason = mTempWhitelistAppIdEndTimes.valueAt(idx).second; + mTempWhitelistAppIdEndTimes.removeAt(idx); + onAppRemovedFromTempWhitelistLocked(appId, reason); + } + } + + private void postTempActiveTimeoutMessage(int appId, long delay) { + if (DEBUG) { + Slog.d(TAG, "postTempActiveTimeoutMessage: appId=" + appId + ", delay=" + delay); + } + mHandler.sendMessageDelayed( + mHandler.obtainMessage(MSG_TEMP_APP_WHITELIST_TIMEOUT, appId, 0), delay); + } + + void checkTempAppWhitelistTimeout(int appId) { + final long timeNow = SystemClock.elapsedRealtime(); + if (DEBUG) { + Slog.d(TAG, "checkTempAppWhitelistTimeout: appId=" + appId + ", timeNow=" + timeNow); + } + synchronized (this) { + Pair<MutableLong, String> entry = mTempWhitelistAppIdEndTimes.get(appId); + if (entry == null) { + // Nothing to do + return; + } + if (timeNow >= entry.first.value) { + mTempWhitelistAppIdEndTimes.delete(appId); + onAppRemovedFromTempWhitelistLocked(appId, entry.second); + } else { + // Need more time + if (DEBUG) { + Slog.d(TAG, "Time to remove AppId " + appId + ": " + entry.first.value); + } + postTempActiveTimeoutMessage(appId, entry.first.value - timeNow); + } + } + } + + @GuardedBy("this") + private void onAppRemovedFromTempWhitelistLocked(int appId, String reason) { + if (DEBUG) { + Slog.d(TAG, "Removing appId " + appId + " from temp whitelist"); + } + updateTempWhitelistAppIdsLocked(appId, false); + mHandler.obtainMessage(MSG_REPORT_TEMP_APP_WHITELIST_CHANGED, appId, 0) + .sendToTarget(); + reportTempWhitelistChangedLocked(); + try { + mBatteryStats.noteEvent(BatteryStats.HistoryItem.EVENT_TEMP_WHITELIST_FINISH, + reason, appId); + } catch (RemoteException e) { + } + } + + public void exitIdleInternal(String reason) { + synchronized (this) { + mActiveReason = ACTIVE_REASON_FROM_BINDER_CALL; + becomeActiveLocked(reason, Binder.getCallingUid()); + } + } + + @VisibleForTesting + boolean isNetworkConnected() { + synchronized (this) { + return mNetworkConnected; + } + } + + void updateConnectivityState(Intent connIntent) { + ConnectivityService cm; + synchronized (this) { + cm = mInjector.getConnectivityService(); + } + if (cm == null) { + return; + } + // Note: can't call out to ConnectivityService with our lock held. + NetworkInfo ni = cm.getActiveNetworkInfo(); + synchronized (this) { + boolean conn; + if (ni == null) { + conn = false; + } else { + if (connIntent == null) { + conn = ni.isConnected(); + } else { + final int networkType = + connIntent.getIntExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, + ConnectivityManager.TYPE_NONE); + if (ni.getType() != networkType) { + return; + } + conn = !connIntent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, + false); + } + } + if (conn != mNetworkConnected) { + mNetworkConnected = conn; + if (conn && mLightState == LIGHT_STATE_WAITING_FOR_NETWORK) { + stepLightIdleStateLocked("network"); + } + } + } + } + + @VisibleForTesting + boolean isScreenOn() { + synchronized (this) { + return mScreenOn; + } + } + + void updateInteractivityLocked() { + // The interactivity state from the power manager tells us whether the display is + // in a state that we need to keep things running so they will update at a normal + // frequency. + boolean screenOn = mPowerManager.isInteractive(); + if (DEBUG) Slog.d(TAG, "updateInteractivityLocked: screenOn=" + screenOn); + if (!screenOn && mScreenOn) { + mScreenOn = false; + if (!mForceIdle) { + becomeInactiveIfAppropriateLocked(); + } + } else if (screenOn) { + mScreenOn = true; + if (!mForceIdle && (!mScreenLocked || !mConstants.WAIT_FOR_UNLOCK)) { + mActiveReason = ACTIVE_REASON_SCREEN; + becomeActiveLocked("screen", Process.myUid()); + } + } + } + + @VisibleForTesting + boolean isCharging() { + synchronized (this) { + return mCharging; + } + } + + void updateChargingLocked(boolean charging) { + if (DEBUG) Slog.i(TAG, "updateChargingLocked: charging=" + charging); + if (!charging && mCharging) { + mCharging = false; + if (!mForceIdle) { + becomeInactiveIfAppropriateLocked(); + } + } else if (charging) { + mCharging = charging; + if (!mForceIdle) { + mActiveReason = ACTIVE_REASON_CHARGING; + becomeActiveLocked("charging", Process.myUid()); + } + } + } + + @VisibleForTesting + boolean isQuickDozeEnabled() { + synchronized (this) { + return mQuickDozeActivated; + } + } + + /** Updates the quick doze flag and enters deep doze if appropriate. */ + @VisibleForTesting + void updateQuickDozeFlagLocked(boolean enabled) { + if (DEBUG) Slog.i(TAG, "updateQuickDozeFlagLocked: enabled=" + enabled); + mQuickDozeActivated = enabled; + if (enabled) { + // If Quick Doze is enabled, see if we should go straight into it. + becomeInactiveIfAppropriateLocked(); + } + // Going from Deep Doze to Light Idle (if quick doze becomes disabled) is tricky and + // probably not worth the overhead, so leave in deep doze if that's the case until the + // next natural time to come out of it. + } + + + /** Returns true if the screen is locked. */ + @VisibleForTesting + boolean isKeyguardShowing() { + synchronized (this) { + return mScreenLocked; + } + } + + @VisibleForTesting + void keyguardShowingLocked(boolean showing) { + if (DEBUG) Slog.i(TAG, "keyguardShowing=" + showing); + if (mScreenLocked != showing) { + mScreenLocked = showing; + if (mScreenOn && !mForceIdle && !mScreenLocked) { + mActiveReason = ACTIVE_REASON_UNLOCKED; + becomeActiveLocked("unlocked", Process.myUid()); + } + } + } + + @VisibleForTesting + void scheduleReportActiveLocked(String activeReason, int activeUid) { + Message msg = mHandler.obtainMessage(MSG_REPORT_ACTIVE, activeUid, 0, activeReason); + mHandler.sendMessage(msg); + } + + void becomeActiveLocked(String activeReason, int activeUid) { + becomeActiveLocked(activeReason, activeUid, mConstants.INACTIVE_TIMEOUT, true); + } + + private void becomeActiveLocked(String activeReason, int activeUid, + long newInactiveTimeout, boolean changeLightIdle) { + if (DEBUG) { + Slog.i(TAG, "becomeActiveLocked, reason=" + activeReason + + ", changeLightIdle=" + changeLightIdle); + } + if (mState != STATE_ACTIVE || mLightState != STATE_ACTIVE) { + EventLogTags.writeDeviceIdle(STATE_ACTIVE, activeReason); + mState = STATE_ACTIVE; + mInactiveTimeout = newInactiveTimeout; + resetIdleManagementLocked(); + // Don't reset maintenance window start time if we're in a light idle maintenance window + // because its used in the light idle budget calculation. + if (mLightState != LIGHT_STATE_IDLE_MAINTENANCE) { + mMaintenanceStartTime = 0; + } + + if (changeLightIdle) { + EventLogTags.writeDeviceIdleLight(LIGHT_STATE_ACTIVE, activeReason); + mLightState = LIGHT_STATE_ACTIVE; + resetLightIdleManagementLocked(); + // Only report active if light is also ACTIVE. + scheduleReportActiveLocked(activeReason, activeUid); + addEvent(EVENT_NORMAL, activeReason); + } + } + } + + /** Must only be used in tests. */ + @VisibleForTesting + void setDeepEnabledForTest(boolean enabled) { + synchronized (this) { + mDeepEnabled = enabled; + } + } + + /** Must only be used in tests. */ + @VisibleForTesting + void setLightEnabledForTest(boolean enabled) { + synchronized (this) { + mLightEnabled = enabled; + } + } + + /** Sanity check to make sure DeviceIdleController and AlarmManager are on the same page. */ + private void verifyAlarmStateLocked() { + if (mState == STATE_ACTIVE && mNextAlarmTime != 0) { + Slog.wtf(TAG, "mState=ACTIVE but mNextAlarmTime=" + mNextAlarmTime); + } + if (mState != STATE_IDLE && mLocalAlarmManager.isIdling()) { + Slog.wtf(TAG, "mState=" + stateToString(mState) + " but AlarmManager is idling"); + } + if (mState == STATE_IDLE && !mLocalAlarmManager.isIdling()) { + Slog.wtf(TAG, "mState=IDLE but AlarmManager is not idling"); + } + if (mLightState == LIGHT_STATE_ACTIVE && mNextLightAlarmTime != 0) { + Slog.wtf(TAG, "mLightState=ACTIVE but mNextLightAlarmTime is " + + TimeUtils.formatDuration(mNextLightAlarmTime - SystemClock.elapsedRealtime()) + + " from now"); + } + } + + void becomeInactiveIfAppropriateLocked() { + verifyAlarmStateLocked(); + + final boolean isScreenBlockingInactive = + mScreenOn && (!mConstants.WAIT_FOR_UNLOCK || !mScreenLocked); + if (DEBUG) { + Slog.d(TAG, "becomeInactiveIfAppropriateLocked():" + + " isScreenBlockingInactive=" + isScreenBlockingInactive + + " (mScreenOn=" + mScreenOn + + ", WAIT_FOR_UNLOCK=" + mConstants.WAIT_FOR_UNLOCK + + ", mScreenLocked=" + mScreenLocked + ")" + + " mCharging=" + mCharging + + " mForceIdle=" + mForceIdle + ); + } + if (!mForceIdle && (mCharging || isScreenBlockingInactive)) { + return; + } + // Become inactive and determine if we will ultimately go idle. + if (mDeepEnabled) { + if (mQuickDozeActivated) { + if (mState == STATE_QUICK_DOZE_DELAY || mState == STATE_IDLE + || mState == STATE_IDLE_MAINTENANCE) { + // Already "idling". Don't want to restart the process. + // mLightState can't be LIGHT_STATE_ACTIVE if mState is any of these 3 + // values, so returning here is safe. + return; + } + if (DEBUG) { + Slog.d(TAG, "Moved from " + + stateToString(mState) + " to STATE_QUICK_DOZE_DELAY"); + } + mState = STATE_QUICK_DOZE_DELAY; + // Make sure any motion sensing or locating is stopped. + resetIdleManagementLocked(); + if (isUpcomingAlarmClock()) { + // If there's an upcoming AlarmClock alarm, we won't go into idle, so + // setting a wakeup alarm before the upcoming alarm is futile. Set the quick + // doze alarm to after the upcoming AlarmClock alarm. + scheduleAlarmLocked( + mAlarmManager.getNextWakeFromIdleTime() - mInjector.getElapsedRealtime() + + mConstants.QUICK_DOZE_DELAY_TIMEOUT, false); + } else { + // Wait a small amount of time in case something (eg: background service from + // recently closed app) needs to finish running. + scheduleAlarmLocked(mConstants.QUICK_DOZE_DELAY_TIMEOUT, false); + } + EventLogTags.writeDeviceIdle(mState, "no activity"); + } else if (mState == STATE_ACTIVE) { + mState = STATE_INACTIVE; + if (DEBUG) Slog.d(TAG, "Moved from STATE_ACTIVE to STATE_INACTIVE"); + resetIdleManagementLocked(); + long delay = mInactiveTimeout; + if (shouldUseIdleTimeoutFactorLocked()) { + delay = (long) (mPreIdleFactor * delay); + } + if (isUpcomingAlarmClock()) { + // If there's an upcoming AlarmClock alarm, we won't go into idle, so + // setting a wakeup alarm before the upcoming alarm is futile. Set the idle + // alarm to after the upcoming AlarmClock alarm. + scheduleAlarmLocked( + mAlarmManager.getNextWakeFromIdleTime() - mInjector.getElapsedRealtime() + + delay, false); + } else { + scheduleAlarmLocked(delay, false); + } + EventLogTags.writeDeviceIdle(mState, "no activity"); + } + } + if (mLightState == LIGHT_STATE_ACTIVE && mLightEnabled) { + mLightState = LIGHT_STATE_INACTIVE; + if (DEBUG) Slog.d(TAG, "Moved from LIGHT_STATE_ACTIVE to LIGHT_STATE_INACTIVE"); + resetLightIdleManagementLocked(); + scheduleLightAlarmLocked(mConstants.LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT); + EventLogTags.writeDeviceIdleLight(mLightState, "no activity"); + } + } + + private void resetIdleManagementLocked() { + mNextIdlePendingDelay = 0; + mNextIdleDelay = 0; + mIdleStartTime = 0; + cancelAlarmLocked(); + cancelSensingTimeoutAlarmLocked(); + cancelLocatingLocked(); + stopMonitoringMotionLocked(); + mAnyMotionDetector.stop(); + updateActiveConstraintsLocked(); + } + + private void resetLightIdleManagementLocked() { + mNextLightIdleDelay = 0; + mCurLightIdleBudget = 0; + cancelLightAlarmLocked(); + } + + void exitForceIdleLocked() { + if (mForceIdle) { + mForceIdle = false; + if (mScreenOn || mCharging) { + mActiveReason = ACTIVE_REASON_FORCED; + becomeActiveLocked("exit-force", Process.myUid()); + } + } + } + + /** + * Must only be used in tests. + * + * This sets the state value directly and thus doesn't trigger any behavioral changes. + */ + @VisibleForTesting + void setLightStateForTest(int lightState) { + synchronized (this) { + mLightState = lightState; + } + } + + @VisibleForTesting + int getLightState() { + return mLightState; + } + + void stepLightIdleStateLocked(String reason) { + if (mLightState == LIGHT_STATE_OVERRIDE) { + // If we are already in deep device idle mode, then + // there is nothing left to do for light mode. + return; + } + + if (DEBUG) Slog.d(TAG, "stepLightIdleStateLocked: mLightState=" + mLightState); + EventLogTags.writeDeviceIdleLightStep(); + + switch (mLightState) { + case LIGHT_STATE_INACTIVE: + mCurLightIdleBudget = mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET; + // Reset the upcoming idle delays. + mNextLightIdleDelay = mConstants.LIGHT_IDLE_TIMEOUT; + mMaintenanceStartTime = 0; + if (!isOpsInactiveLocked()) { + // We have some active ops going on... give them a chance to finish + // before going in to our first idle. + mLightState = LIGHT_STATE_PRE_IDLE; + EventLogTags.writeDeviceIdleLight(mLightState, reason); + scheduleLightAlarmLocked(mConstants.LIGHT_PRE_IDLE_TIMEOUT); + break; + } + // Nothing active, fall through to immediately idle. + case LIGHT_STATE_PRE_IDLE: + case LIGHT_STATE_IDLE_MAINTENANCE: + if (mMaintenanceStartTime != 0) { + long duration = SystemClock.elapsedRealtime() - mMaintenanceStartTime; + if (duration < mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET) { + // We didn't use up all of our minimum budget; add this to the reserve. + mCurLightIdleBudget += + (mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET - duration); + } else { + // We used more than our minimum budget; this comes out of the reserve. + mCurLightIdleBudget -= + (duration - mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET); + } + } + mMaintenanceStartTime = 0; + scheduleLightAlarmLocked(mNextLightIdleDelay); + mNextLightIdleDelay = Math.min(mConstants.LIGHT_MAX_IDLE_TIMEOUT, + (long)(mNextLightIdleDelay * mConstants.LIGHT_IDLE_FACTOR)); + if (mNextLightIdleDelay < mConstants.LIGHT_IDLE_TIMEOUT) { + mNextLightIdleDelay = mConstants.LIGHT_IDLE_TIMEOUT; + } + if (DEBUG) Slog.d(TAG, "Moved to LIGHT_STATE_IDLE."); + mLightState = LIGHT_STATE_IDLE; + EventLogTags.writeDeviceIdleLight(mLightState, reason); + addEvent(EVENT_LIGHT_IDLE, null); + mGoingIdleWakeLock.acquire(); + mHandler.sendEmptyMessage(MSG_REPORT_IDLE_ON_LIGHT); + break; + case LIGHT_STATE_IDLE: + case LIGHT_STATE_WAITING_FOR_NETWORK: + if (mNetworkConnected || mLightState == LIGHT_STATE_WAITING_FOR_NETWORK) { + // We have been idling long enough, now it is time to do some work. + mActiveIdleOpCount = 1; + mActiveIdleWakeLock.acquire(); + mMaintenanceStartTime = SystemClock.elapsedRealtime(); + if (mCurLightIdleBudget < mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET) { + mCurLightIdleBudget = mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET; + } else if (mCurLightIdleBudget > mConstants.LIGHT_IDLE_MAINTENANCE_MAX_BUDGET) { + mCurLightIdleBudget = mConstants.LIGHT_IDLE_MAINTENANCE_MAX_BUDGET; + } + scheduleLightAlarmLocked(mCurLightIdleBudget); + if (DEBUG) Slog.d(TAG, + "Moved from LIGHT_STATE_IDLE to LIGHT_STATE_IDLE_MAINTENANCE."); + mLightState = LIGHT_STATE_IDLE_MAINTENANCE; + EventLogTags.writeDeviceIdleLight(mLightState, reason); + addEvent(EVENT_LIGHT_MAINTENANCE, null); + mHandler.sendEmptyMessage(MSG_REPORT_IDLE_OFF); + } else { + // We'd like to do maintenance, but currently don't have network + // connectivity... let's try to wait until the network comes back. + // We'll only wait for another full idle period, however, and then give up. + scheduleLightAlarmLocked(mNextLightIdleDelay); + if (DEBUG) Slog.d(TAG, "Moved to LIGHT_WAITING_FOR_NETWORK."); + mLightState = LIGHT_STATE_WAITING_FOR_NETWORK; + EventLogTags.writeDeviceIdleLight(mLightState, reason); + } + break; + } + } + + @VisibleForTesting + int getState() { + return mState; + } + + /** + * Returns true if there's an upcoming AlarmClock alarm that is soon enough to prevent the + * device from going into idle. + */ + private boolean isUpcomingAlarmClock() { + return mInjector.getElapsedRealtime() + mConstants.MIN_TIME_TO_ALARM + >= mAlarmManager.getNextWakeFromIdleTime(); + } + + @VisibleForTesting + void stepIdleStateLocked(String reason) { + if (DEBUG) Slog.d(TAG, "stepIdleStateLocked: mState=" + mState); + EventLogTags.writeDeviceIdleStep(); + + if (isUpcomingAlarmClock()) { + // Whoops, there is an upcoming alarm. We don't actually want to go idle. + if (mState != STATE_ACTIVE) { + mActiveReason = ACTIVE_REASON_ALARM; + becomeActiveLocked("alarm", Process.myUid()); + becomeInactiveIfAppropriateLocked(); + } + return; + } + + if (mNumBlockingConstraints != 0 && !mForceIdle) { + // We have some constraints from other parts of the system server preventing + // us from moving to the next state. + if (DEBUG) { + Slog.i(TAG, "Cannot step idle state. Blocked by: " + mConstraints.values().stream() + .filter(x -> x.active) + .map(x -> x.name) + .collect(Collectors.joining(","))); + } + return; + } + + switch (mState) { + case STATE_INACTIVE: + // We have now been inactive long enough, it is time to start looking + // for motion and sleep some more while doing so. + startMonitoringMotionLocked(); + long delay = mConstants.IDLE_AFTER_INACTIVE_TIMEOUT; + if (shouldUseIdleTimeoutFactorLocked()) { + delay = (long) (mPreIdleFactor * delay); + } + scheduleAlarmLocked(delay, false); + moveToStateLocked(STATE_IDLE_PENDING, reason); + break; + case STATE_IDLE_PENDING: + moveToStateLocked(STATE_SENSING, reason); + cancelLocatingLocked(); + mLocated = false; + mLastGenericLocation = null; + mLastGpsLocation = null; + updateActiveConstraintsLocked(); + + // Wait for open constraints and an accelerometer reading before moving on. + if (mUseMotionSensor && mAnyMotionDetector.hasSensor()) { + scheduleSensingTimeoutAlarmLocked(mConstants.SENSING_TIMEOUT); + mNotMoving = false; + mAnyMotionDetector.checkForAnyMotion(); + break; + } else if (mNumBlockingConstraints != 0) { + cancelAlarmLocked(); + break; + } + + mNotMoving = true; + // Otherwise, fall through and check this off the list of requirements. + case STATE_SENSING: + cancelSensingTimeoutAlarmLocked(); + moveToStateLocked(STATE_LOCATING, reason); + scheduleAlarmLocked(mConstants.LOCATING_TIMEOUT, false); + LocationManager locationManager = mInjector.getLocationManager(); + if (locationManager != null + && locationManager.getProvider(LocationManager.NETWORK_PROVIDER) != null) { + locationManager.requestLocationUpdates(mLocationRequest, + mGenericLocationListener, mHandler.getLooper()); + mLocating = true; + } else { + mHasNetworkLocation = false; + } + if (locationManager != null + && locationManager.getProvider(LocationManager.GPS_PROVIDER) != null) { + mHasGps = true; + locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000, 5, + mGpsLocationListener, mHandler.getLooper()); + mLocating = true; + } else { + mHasGps = false; + } + // If we have a location provider, we're all set, the listeners will move state + // forward. + if (mLocating) { + break; + } + + // Otherwise, we have to move from locating into idle maintenance. + case STATE_LOCATING: + cancelAlarmLocked(); + cancelLocatingLocked(); + mAnyMotionDetector.stop(); + + // Intentional fallthrough -- time to go into IDLE state. + case STATE_QUICK_DOZE_DELAY: + // Reset the upcoming idle delays. + mNextIdlePendingDelay = mConstants.IDLE_PENDING_TIMEOUT; + mNextIdleDelay = mConstants.IDLE_TIMEOUT; + + // Everything is in place to go into IDLE state. + case STATE_IDLE_MAINTENANCE: + scheduleAlarmLocked(mNextIdleDelay, true); + if (DEBUG) Slog.d(TAG, "Moved to STATE_IDLE. Next alarm in " + mNextIdleDelay + + " ms."); + mNextIdleDelay = (long)(mNextIdleDelay * mConstants.IDLE_FACTOR); + if (DEBUG) Slog.d(TAG, "Setting mNextIdleDelay = " + mNextIdleDelay); + mIdleStartTime = SystemClock.elapsedRealtime(); + mNextIdleDelay = Math.min(mNextIdleDelay, mConstants.MAX_IDLE_TIMEOUT); + if (mNextIdleDelay < mConstants.IDLE_TIMEOUT) { + mNextIdleDelay = mConstants.IDLE_TIMEOUT; + } + moveToStateLocked(STATE_IDLE, reason); + if (mLightState != LIGHT_STATE_OVERRIDE) { + mLightState = LIGHT_STATE_OVERRIDE; + cancelLightAlarmLocked(); + } + addEvent(EVENT_DEEP_IDLE, null); + mGoingIdleWakeLock.acquire(); + mHandler.sendEmptyMessage(MSG_REPORT_IDLE_ON); + break; + case STATE_IDLE: + // We have been idling long enough, now it is time to do some work. + mActiveIdleOpCount = 1; + mActiveIdleWakeLock.acquire(); + scheduleAlarmLocked(mNextIdlePendingDelay, false); + if (DEBUG) Slog.d(TAG, "Moved from STATE_IDLE to STATE_IDLE_MAINTENANCE. " + + "Next alarm in " + mNextIdlePendingDelay + " ms."); + mMaintenanceStartTime = SystemClock.elapsedRealtime(); + mNextIdlePendingDelay = Math.min(mConstants.MAX_IDLE_PENDING_TIMEOUT, + (long)(mNextIdlePendingDelay * mConstants.IDLE_PENDING_FACTOR)); + if (mNextIdlePendingDelay < mConstants.IDLE_PENDING_TIMEOUT) { + mNextIdlePendingDelay = mConstants.IDLE_PENDING_TIMEOUT; + } + moveToStateLocked(STATE_IDLE_MAINTENANCE, reason); + addEvent(EVENT_DEEP_MAINTENANCE, null); + mHandler.sendEmptyMessage(MSG_REPORT_IDLE_OFF); + break; + } + } + + private void moveToStateLocked(int state, String reason) { + final int oldState = mState; + mState = state; + if (DEBUG) { + Slog.d(TAG, String.format("Moved from STATE_%s to STATE_%s.", + stateToString(oldState), stateToString(mState))); + } + EventLogTags.writeDeviceIdle(mState, reason); + updateActiveConstraintsLocked(); + } + + void incActiveIdleOps() { + synchronized (this) { + mActiveIdleOpCount++; + } + } + + void decActiveIdleOps() { + synchronized (this) { + mActiveIdleOpCount--; + if (mActiveIdleOpCount <= 0) { + exitMaintenanceEarlyIfNeededLocked(); + mActiveIdleWakeLock.release(); + } + } + } + + /** Must only be used in tests. */ + @VisibleForTesting + void setActiveIdleOpsForTest(int count) { + synchronized (this) { + mActiveIdleOpCount = count; + } + } + + void setJobsActive(boolean active) { + synchronized (this) { + mJobsActive = active; + if (!active) { + exitMaintenanceEarlyIfNeededLocked(); + } + } + } + + void setAlarmsActive(boolean active) { + synchronized (this) { + mAlarmsActive = active; + if (!active) { + exitMaintenanceEarlyIfNeededLocked(); + } + } + } + + @VisibleForTesting + int setPreIdleTimeoutMode(int mode) { + return setPreIdleTimeoutFactor(getPreIdleTimeoutByMode(mode)); + } + + @VisibleForTesting + float getPreIdleTimeoutByMode(int mode) { + switch (mode) { + case PowerManager.PRE_IDLE_TIMEOUT_MODE_LONG: { + return mConstants.PRE_IDLE_FACTOR_LONG; + } + case PowerManager.PRE_IDLE_TIMEOUT_MODE_SHORT: { + return mConstants.PRE_IDLE_FACTOR_SHORT; + } + case PowerManager.PRE_IDLE_TIMEOUT_MODE_NORMAL: { + return 1.0f; + } + default: { + Slog.w(TAG, "Invalid time out factor mode: " + mode); + return 1.0f; + } + } + } + + @VisibleForTesting + float getPreIdleTimeoutFactor() { + return mPreIdleFactor; + } + + @VisibleForTesting + int setPreIdleTimeoutFactor(float ratio) { + if (!mDeepEnabled) { + if (DEBUG) Slog.d(TAG, "setPreIdleTimeoutFactor: Deep Idle disable"); + return SET_IDLE_FACTOR_RESULT_NOT_SUPPORT; + } else if (ratio <= MIN_PRE_IDLE_FACTOR_CHANGE) { + if (DEBUG) Slog.d(TAG, "setPreIdleTimeoutFactor: Invalid input"); + return SET_IDLE_FACTOR_RESULT_INVALID; + } else if (Math.abs(ratio - mPreIdleFactor) < MIN_PRE_IDLE_FACTOR_CHANGE) { + if (DEBUG) Slog.d(TAG, "setPreIdleTimeoutFactor: New factor same as previous factor"); + return SET_IDLE_FACTOR_RESULT_IGNORED; + } + synchronized (this) { + mLastPreIdleFactor = mPreIdleFactor; + mPreIdleFactor = ratio; + } + if (DEBUG) Slog.d(TAG, "setPreIdleTimeoutFactor: " + ratio); + postUpdatePreIdleFactor(); + return SET_IDLE_FACTOR_RESULT_OK; + } + + @VisibleForTesting + void resetPreIdleTimeoutMode() { + synchronized (this) { + mLastPreIdleFactor = mPreIdleFactor; + mPreIdleFactor = 1.0f; + } + if (DEBUG) Slog.d(TAG, "resetPreIdleTimeoutMode to 1.0"); + postResetPreIdleTimeoutFactor(); + } + + private void postUpdatePreIdleFactor() { + mHandler.sendEmptyMessage(MSG_UPDATE_PRE_IDLE_TIMEOUT_FACTOR); + } + + private void postResetPreIdleTimeoutFactor() { + mHandler.sendEmptyMessage(MSG_RESET_PRE_IDLE_TIMEOUT_FACTOR); + } + + @VisibleForTesting + void updatePreIdleFactor() { + synchronized (this) { + if (!shouldUseIdleTimeoutFactorLocked()) { + return; + } + if (mState == STATE_INACTIVE || mState == STATE_IDLE_PENDING) { + if (mNextAlarmTime == 0) { + return; + } + long delay = mNextAlarmTime - SystemClock.elapsedRealtime(); + if (delay < MIN_STATE_STEP_ALARM_CHANGE) { + return; + } + long newDelay = (long) (delay / mLastPreIdleFactor * mPreIdleFactor); + if (Math.abs(delay - newDelay) < MIN_STATE_STEP_ALARM_CHANGE) { + return; + } + scheduleAlarmLocked(newDelay, false); + } + } + } + + @VisibleForTesting + void maybeDoImmediateMaintenance() { + synchronized (this) { + if (mState == STATE_IDLE) { + long duration = SystemClock.elapsedRealtime() - mIdleStartTime; + /* Let's trgger a immediate maintenance, + * if it has been idle for a long time */ + if (duration > mConstants.IDLE_TIMEOUT) { + scheduleAlarmLocked(0, false); + } + } + } + } + + private boolean shouldUseIdleTimeoutFactorLocked() { + // exclude ACTIVE_REASON_MOTION, for exclude device in pocket case + if (mActiveReason == ACTIVE_REASON_MOTION) { + return false; + } + return true; + } + + /** Must only be used in tests. */ + @VisibleForTesting + void setIdleStartTimeForTest(long idleStartTime) { + synchronized (this) { + mIdleStartTime = idleStartTime; + } + } + + @VisibleForTesting + long getNextAlarmTime() { + return mNextAlarmTime; + } + + boolean isOpsInactiveLocked() { + return mActiveIdleOpCount <= 0 && !mJobsActive && !mAlarmsActive; + } + + void exitMaintenanceEarlyIfNeededLocked() { + if (mState == STATE_IDLE_MAINTENANCE || mLightState == LIGHT_STATE_IDLE_MAINTENANCE + || mLightState == LIGHT_STATE_PRE_IDLE) { + if (isOpsInactiveLocked()) { + final long now = SystemClock.elapsedRealtime(); + if (DEBUG) { + StringBuilder sb = new StringBuilder(); + sb.append("Exit: start="); + TimeUtils.formatDuration(mMaintenanceStartTime, sb); + sb.append(" now="); + TimeUtils.formatDuration(now, sb); + Slog.d(TAG, sb.toString()); + } + if (mState == STATE_IDLE_MAINTENANCE) { + stepIdleStateLocked("s:early"); + } else if (mLightState == LIGHT_STATE_PRE_IDLE) { + stepLightIdleStateLocked("s:predone"); + } else { + stepLightIdleStateLocked("s:early"); + } + } + } + } + + void motionLocked() { + if (DEBUG) Slog.d(TAG, "motionLocked()"); + // The motion sensor will have been disabled at this point + handleMotionDetectedLocked(mConstants.MOTION_INACTIVE_TIMEOUT, "motion"); + } + + void handleMotionDetectedLocked(long timeout, String type) { + // The device is not yet active, so we want to go back to the pending idle + // state to wait again for no motion. Note that we only monitor for motion + // after moving out of the inactive state, so no need to worry about that. + final boolean becomeInactive = mState != STATE_ACTIVE + || mLightState == LIGHT_STATE_OVERRIDE; + // We only want to change the IDLE state if it's OVERRIDE. + becomeActiveLocked(type, Process.myUid(), timeout, mLightState == LIGHT_STATE_OVERRIDE); + if (becomeInactive) { + becomeInactiveIfAppropriateLocked(); + } + } + + void receivedGenericLocationLocked(Location location) { + if (mState != STATE_LOCATING) { + cancelLocatingLocked(); + return; + } + if (DEBUG) Slog.d(TAG, "Generic location: " + location); + mLastGenericLocation = new Location(location); + if (location.getAccuracy() > mConstants.LOCATION_ACCURACY && mHasGps) { + return; + } + mLocated = true; + if (mNotMoving) { + stepIdleStateLocked("s:location"); + } + } + + void receivedGpsLocationLocked(Location location) { + if (mState != STATE_LOCATING) { + cancelLocatingLocked(); + return; + } + if (DEBUG) Slog.d(TAG, "GPS location: " + location); + mLastGpsLocation = new Location(location); + if (location.getAccuracy() > mConstants.LOCATION_ACCURACY) { + return; + } + mLocated = true; + if (mNotMoving) { + stepIdleStateLocked("s:gps"); + } + } + + void startMonitoringMotionLocked() { + if (DEBUG) Slog.d(TAG, "startMonitoringMotionLocked()"); + if (mMotionSensor != null && !mMotionListener.active) { + mMotionListener.registerLocked(); + } + } + + void stopMonitoringMotionLocked() { + if (DEBUG) Slog.d(TAG, "stopMonitoringMotionLocked()"); + if (mMotionSensor != null && mMotionListener.active) { + mMotionListener.unregisterLocked(); + } + } + + void cancelAlarmLocked() { + if (mNextAlarmTime != 0) { + mNextAlarmTime = 0; + mAlarmManager.cancel(mDeepAlarmListener); + } + } + + void cancelLightAlarmLocked() { + if (mNextLightAlarmTime != 0) { + mNextLightAlarmTime = 0; + mAlarmManager.cancel(mLightAlarmListener); + } + } + + void cancelLocatingLocked() { + if (mLocating) { + LocationManager locationManager = mInjector.getLocationManager(); + locationManager.removeUpdates(mGenericLocationListener); + locationManager.removeUpdates(mGpsLocationListener); + mLocating = false; + } + } + + void cancelSensingTimeoutAlarmLocked() { + if (mNextSensingTimeoutAlarmTime != 0) { + mNextSensingTimeoutAlarmTime = 0; + mAlarmManager.cancel(mSensingTimeoutAlarmListener); + } + } + + void scheduleAlarmLocked(long delay, boolean idleUntil) { + if (DEBUG) Slog.d(TAG, "scheduleAlarmLocked(" + delay + ", " + idleUntil + ")"); + + if (mUseMotionSensor && mMotionSensor == null + && mState != STATE_QUICK_DOZE_DELAY + && mState != STATE_IDLE + && mState != STATE_IDLE_MAINTENANCE) { + // If there is no motion sensor on this device, but we need one, then we won't schedule + // alarms, because we can't determine if the device is not moving. This effectively + // turns off normal execution of device idling, although it is still possible to + // manually poke it by pretending like the alarm is going off. + // STATE_QUICK_DOZE_DELAY skips the motion sensing so if the state is past the motion + // sensing stage (ie, is QUICK_DOZE_DELAY, IDLE, or IDLE_MAINTENANCE), then idling + // can continue until the user interacts with the device. + return; + } + mNextAlarmTime = SystemClock.elapsedRealtime() + delay; + if (idleUntil) { + mAlarmManager.setIdleUntil(AlarmManager.ELAPSED_REALTIME_WAKEUP, + mNextAlarmTime, "DeviceIdleController.deep", mDeepAlarmListener, mHandler); + } else { + mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, + mNextAlarmTime, "DeviceIdleController.deep", mDeepAlarmListener, mHandler); + } + } + + void scheduleLightAlarmLocked(long delay) { + if (DEBUG) Slog.d(TAG, "scheduleLightAlarmLocked(" + delay + ")"); + mNextLightAlarmTime = SystemClock.elapsedRealtime() + delay; + mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, + mNextLightAlarmTime, "DeviceIdleController.light", mLightAlarmListener, mHandler); + } + + void scheduleSensingTimeoutAlarmLocked(long delay) { + if (DEBUG) Slog.d(TAG, "scheduleSensingAlarmLocked(" + delay + ")"); + mNextSensingTimeoutAlarmTime = SystemClock.elapsedRealtime() + delay; + mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, mNextSensingTimeoutAlarmTime, + "DeviceIdleController.sensing", mSensingTimeoutAlarmListener, mHandler); + } + + private static int[] buildAppIdArray(ArrayMap<String, Integer> systemApps, + ArrayMap<String, Integer> userApps, SparseBooleanArray outAppIds) { + outAppIds.clear(); + if (systemApps != null) { + for (int i = 0; i < systemApps.size(); i++) { + outAppIds.put(systemApps.valueAt(i), true); + } + } + if (userApps != null) { + for (int i = 0; i < userApps.size(); i++) { + outAppIds.put(userApps.valueAt(i), true); + } + } + int size = outAppIds.size(); + int[] appids = new int[size]; + for (int i = 0; i < size; i++) { + appids[i] = outAppIds.keyAt(i); + } + return appids; + } + + private void updateWhitelistAppIdsLocked() { + mPowerSaveWhitelistExceptIdleAppIdArray = buildAppIdArray(mPowerSaveWhitelistAppsExceptIdle, + mPowerSaveWhitelistUserApps, mPowerSaveWhitelistExceptIdleAppIds); + mPowerSaveWhitelistAllAppIdArray = buildAppIdArray(mPowerSaveWhitelistApps, + mPowerSaveWhitelistUserApps, mPowerSaveWhitelistAllAppIds); + mPowerSaveWhitelistUserAppIdArray = buildAppIdArray(null, + mPowerSaveWhitelistUserApps, mPowerSaveWhitelistUserAppIds); + if (mLocalActivityManager != null) { + mLocalActivityManager.setDeviceIdleWhitelist( + mPowerSaveWhitelistAllAppIdArray, mPowerSaveWhitelistExceptIdleAppIdArray); + } + if (mLocalPowerManager != null) { + if (DEBUG) { + Slog.d(TAG, "Setting wakelock whitelist to " + + Arrays.toString(mPowerSaveWhitelistAllAppIdArray)); + } + mLocalPowerManager.setDeviceIdleWhitelist(mPowerSaveWhitelistAllAppIdArray); + } + passWhiteListsToForceAppStandbyTrackerLocked(); + } + + private void updateTempWhitelistAppIdsLocked(int appId, boolean adding) { + final int size = mTempWhitelistAppIdEndTimes.size(); + if (mTempWhitelistAppIdArray.length != size) { + mTempWhitelistAppIdArray = new int[size]; + } + for (int i = 0; i < size; i++) { + mTempWhitelistAppIdArray[i] = mTempWhitelistAppIdEndTimes.keyAt(i); + } + if (mLocalActivityManager != null) { + if (DEBUG) { + Slog.d(TAG, "Setting activity manager temp whitelist to " + + Arrays.toString(mTempWhitelistAppIdArray)); + } + mLocalActivityManager.updateDeviceIdleTempWhitelist(mTempWhitelistAppIdArray, appId, + adding); + } + if (mLocalPowerManager != null) { + if (DEBUG) { + Slog.d(TAG, "Setting wakelock temp whitelist to " + + Arrays.toString(mTempWhitelistAppIdArray)); + } + mLocalPowerManager.setDeviceIdleTempWhitelist(mTempWhitelistAppIdArray); + } + passWhiteListsToForceAppStandbyTrackerLocked(); + } + + private void reportPowerSaveWhitelistChangedLocked() { + Intent intent = new Intent(PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED); + intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); + getContext().sendBroadcastAsUser(intent, UserHandle.SYSTEM); + } + + private void reportTempWhitelistChangedLocked() { + Intent intent = new Intent(PowerManager.ACTION_POWER_SAVE_TEMP_WHITELIST_CHANGED); + intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); + getContext().sendBroadcastAsUser(intent, UserHandle.SYSTEM); + } + + private void passWhiteListsToForceAppStandbyTrackerLocked() { + mAppStateTracker.setPowerSaveWhitelistAppIds( + mPowerSaveWhitelistExceptIdleAppIdArray, + mPowerSaveWhitelistUserAppIdArray, + mTempWhitelistAppIdArray); + } + + void readConfigFileLocked() { + if (DEBUG) Slog.d(TAG, "Reading config from " + mConfigFile.getBaseFile()); + mPowerSaveWhitelistUserApps.clear(); + FileInputStream stream; + try { + stream = mConfigFile.openRead(); + } catch (FileNotFoundException e) { + return; + } + try { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(stream, StandardCharsets.UTF_8.name()); + readConfigFileLocked(parser); + } catch (XmlPullParserException e) { + } finally { + try { + stream.close(); + } catch (IOException e) { + } + } + } + + private void readConfigFileLocked(XmlPullParser parser) { + final PackageManager pm = getContext().getPackageManager(); + + try { + int type; + while ((type = parser.next()) != XmlPullParser.START_TAG + && type != XmlPullParser.END_DOCUMENT) { + ; + } + + if (type != XmlPullParser.START_TAG) { + throw new IllegalStateException("no start tag found"); + } + + int outerDepth = parser.getDepth(); + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + String tagName = parser.getName(); + switch (tagName) { + case "wl": + String name = parser.getAttributeValue(null, "n"); + if (name != null) { + try { + ApplicationInfo ai = pm.getApplicationInfo(name, + PackageManager.MATCH_ANY_USER); + mPowerSaveWhitelistUserApps.put(ai.packageName, + UserHandle.getAppId(ai.uid)); + } catch (PackageManager.NameNotFoundException e) { + } + } + break; + case "un-wl": + final String packageName = parser.getAttributeValue(null, "n"); + if (mPowerSaveWhitelistApps.containsKey(packageName)) { + mRemovedFromSystemWhitelistApps.put(packageName, + mPowerSaveWhitelistApps.remove(packageName)); + } + break; + default: + Slog.w(TAG, "Unknown element under <config>: " + + parser.getName()); + XmlUtils.skipCurrentTag(parser); + break; + } + } + + } catch (IllegalStateException e) { + Slog.w(TAG, "Failed parsing config " + e); + } catch (NullPointerException e) { + Slog.w(TAG, "Failed parsing config " + e); + } catch (NumberFormatException e) { + Slog.w(TAG, "Failed parsing config " + e); + } catch (XmlPullParserException e) { + Slog.w(TAG, "Failed parsing config " + e); + } catch (IOException e) { + Slog.w(TAG, "Failed parsing config " + e); + } catch (IndexOutOfBoundsException e) { + Slog.w(TAG, "Failed parsing config " + e); + } + } + + void writeConfigFileLocked() { + mHandler.removeMessages(MSG_WRITE_CONFIG); + mHandler.sendEmptyMessageDelayed(MSG_WRITE_CONFIG, 5000); + } + + void handleWriteConfigFile() { + final ByteArrayOutputStream memStream = new ByteArrayOutputStream(); + + try { + synchronized (this) { + XmlSerializer out = new FastXmlSerializer(); + out.setOutput(memStream, StandardCharsets.UTF_8.name()); + writeConfigFileLocked(out); + } + } catch (IOException e) { + } + + synchronized (mConfigFile) { + FileOutputStream stream = null; + try { + stream = mConfigFile.startWrite(); + memStream.writeTo(stream); + stream.flush(); + FileUtils.sync(stream); + stream.close(); + mConfigFile.finishWrite(stream); + } catch (IOException e) { + Slog.w(TAG, "Error writing config file", e); + mConfigFile.failWrite(stream); + } + } + } + + void writeConfigFileLocked(XmlSerializer out) throws IOException { + out.startDocument(null, true); + out.startTag(null, "config"); + for (int i=0; i<mPowerSaveWhitelistUserApps.size(); i++) { + String name = mPowerSaveWhitelistUserApps.keyAt(i); + out.startTag(null, "wl"); + out.attribute(null, "n", name); + out.endTag(null, "wl"); + } + for (int i = 0; i < mRemovedFromSystemWhitelistApps.size(); i++) { + out.startTag(null, "un-wl"); + out.attribute(null, "n", mRemovedFromSystemWhitelistApps.keyAt(i)); + out.endTag(null, "un-wl"); + } + out.endTag(null, "config"); + out.endDocument(); + } + + static void dumpHelp(PrintWriter pw) { + pw.println("Device idle controller (deviceidle) commands:"); + pw.println(" help"); + pw.println(" Print this help text."); + pw.println(" step [light|deep]"); + pw.println(" Immediately step to next state, without waiting for alarm."); + pw.println(" force-idle [light|deep]"); + pw.println(" Force directly into idle mode, regardless of other device state."); + pw.println(" force-inactive"); + pw.println(" Force to be inactive, ready to freely step idle states."); + pw.println(" unforce"); + pw.println(" Resume normal functioning after force-idle or force-inactive."); + pw.println(" get [light|deep|force|screen|charging|network]"); + pw.println(" Retrieve the current given state."); + pw.println(" disable [light|deep|all]"); + pw.println(" Completely disable device idle mode."); + pw.println(" enable [light|deep|all]"); + pw.println(" Re-enable device idle mode after it had previously been disabled."); + pw.println(" enabled [light|deep|all]"); + pw.println(" Print 1 if device idle mode is currently enabled, else 0."); + pw.println(" whitelist"); + pw.println(" Print currently whitelisted apps."); + pw.println(" whitelist [package ...]"); + pw.println(" Add (prefix with +) or remove (prefix with -) packages."); + pw.println(" sys-whitelist [package ...|reset]"); + pw.println(" Prefix the package with '-' to remove it from the system whitelist or '+'" + + " to put it back in the system whitelist."); + pw.println(" Note that only packages that were" + + " earlier removed from the system whitelist can be added back."); + pw.println(" reset will reset the whitelist to the original state"); + pw.println(" Prints the system whitelist if no arguments are specified"); + pw.println(" except-idle-whitelist [package ...|reset]"); + pw.println(" Prefix the package with '+' to add it to whitelist or " + + "'=' to check if it is already whitelisted"); + pw.println(" [reset] will reset the whitelist to it's original state"); + pw.println(" Note that unlike <whitelist> cmd, " + + "changes made using this won't be persisted across boots"); + pw.println(" tempwhitelist"); + pw.println(" Print packages that are temporarily whitelisted."); + pw.println(" tempwhitelist [-u USER] [-d DURATION] [-r] [package]"); + pw.println(" Temporarily place package in whitelist for DURATION milliseconds."); + pw.println(" If no DURATION is specified, 10 seconds is used"); + pw.println(" If [-r] option is used, then the package is removed from temp whitelist " + + "and any [-d] is ignored"); + pw.println(" motion"); + pw.println(" Simulate a motion event to bring the device out of deep doze"); + pw.println(" pre-idle-factor [0|1|2]"); + pw.println(" Set a new factor to idle time before step to idle" + + "(inactive_to and idle_after_inactive_to)"); + pw.println(" reset-pre-idle-factor"); + pw.println(" Reset factor to idle time to default"); + } + + class Shell extends ShellCommand { + int userId = UserHandle.USER_SYSTEM; + + @Override + public int onCommand(String cmd) { + return onShellCommand(this, cmd); + } + + @Override + public void onHelp() { + PrintWriter pw = getOutPrintWriter(); + dumpHelp(pw); + } + } + + int onShellCommand(Shell shell, String cmd) { + PrintWriter pw = shell.getOutPrintWriter(); + if ("step".equals(cmd)) { + getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, + null); + synchronized (this) { + long token = Binder.clearCallingIdentity(); + String arg = shell.getNextArg(); + try { + if (arg == null || "deep".equals(arg)) { + stepIdleStateLocked("s:shell"); + pw.print("Stepped to deep: "); + pw.println(stateToString(mState)); + } else if ("light".equals(arg)) { + stepLightIdleStateLocked("s:shell"); + pw.print("Stepped to light: "); pw.println(lightStateToString(mLightState)); + } else { + pw.println("Unknown idle mode: " + arg); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + } else if ("force-idle".equals(cmd)) { + getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, + null); + synchronized (this) { + long token = Binder.clearCallingIdentity(); + String arg = shell.getNextArg(); + try { + if (arg == null || "deep".equals(arg)) { + if (!mDeepEnabled) { + pw.println("Unable to go deep idle; not enabled"); + return -1; + } + mForceIdle = true; + becomeInactiveIfAppropriateLocked(); + int curState = mState; + while (curState != STATE_IDLE) { + stepIdleStateLocked("s:shell"); + if (curState == mState) { + pw.print("Unable to go deep idle; stopped at "); + pw.println(stateToString(mState)); + exitForceIdleLocked(); + return -1; + } + curState = mState; + } + pw.println("Now forced in to deep idle mode"); + } else if ("light".equals(arg)) { + mForceIdle = true; + becomeInactiveIfAppropriateLocked(); + int curLightState = mLightState; + while (curLightState != LIGHT_STATE_IDLE) { + stepLightIdleStateLocked("s:shell"); + if (curLightState == mLightState) { + pw.print("Unable to go light idle; stopped at "); + pw.println(lightStateToString(mLightState)); + exitForceIdleLocked(); + return -1; + } + curLightState = mLightState; + } + pw.println("Now forced in to light idle mode"); + } else { + pw.println("Unknown idle mode: " + arg); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + } else if ("force-inactive".equals(cmd)) { + getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, + null); + synchronized (this) { + long token = Binder.clearCallingIdentity(); + try { + mForceIdle = true; + becomeInactiveIfAppropriateLocked(); + pw.print("Light state: "); + pw.print(lightStateToString(mLightState)); + pw.print(", deep state: "); + pw.println(stateToString(mState)); + } finally { + Binder.restoreCallingIdentity(token); + } + } + } else if ("unforce".equals(cmd)) { + getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, + null); + synchronized (this) { + long token = Binder.clearCallingIdentity(); + try { + exitForceIdleLocked(); + pw.print("Light state: "); + pw.print(lightStateToString(mLightState)); + pw.print(", deep state: "); + pw.println(stateToString(mState)); + } finally { + Binder.restoreCallingIdentity(token); + } + } + } else if ("get".equals(cmd)) { + getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, + null); + synchronized (this) { + String arg = shell.getNextArg(); + if (arg != null) { + long token = Binder.clearCallingIdentity(); + try { + switch (arg) { + case "light": pw.println(lightStateToString(mLightState)); break; + case "deep": pw.println(stateToString(mState)); break; + case "force": pw.println(mForceIdle); break; + case "quick": pw.println(mQuickDozeActivated); break; + case "screen": pw.println(mScreenOn); break; + case "charging": pw.println(mCharging); break; + case "network": pw.println(mNetworkConnected); break; + default: pw.println("Unknown get option: " + arg); break; + } + } finally { + Binder.restoreCallingIdentity(token); + } + } else { + pw.println("Argument required"); + } + } + } else if ("disable".equals(cmd)) { + getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, + null); + synchronized (this) { + long token = Binder.clearCallingIdentity(); + String arg = shell.getNextArg(); + try { + boolean becomeActive = false; + boolean valid = false; + if (arg == null || "deep".equals(arg) || "all".equals(arg)) { + valid = true; + if (mDeepEnabled) { + mDeepEnabled = false; + becomeActive = true; + pw.println("Deep idle mode disabled"); + } + } + if (arg == null || "light".equals(arg) || "all".equals(arg)) { + valid = true; + if (mLightEnabled) { + mLightEnabled = false; + becomeActive = true; + pw.println("Light idle mode disabled"); + } + } + if (becomeActive) { + mActiveReason = ACTIVE_REASON_FORCED; + becomeActiveLocked((arg == null ? "all" : arg) + "-disabled", + Process.myUid()); + } + if (!valid) { + pw.println("Unknown idle mode: " + arg); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + } else if ("enable".equals(cmd)) { + getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, + null); + synchronized (this) { + long token = Binder.clearCallingIdentity(); + String arg = shell.getNextArg(); + try { + boolean becomeInactive = false; + boolean valid = false; + if (arg == null || "deep".equals(arg) || "all".equals(arg)) { + valid = true; + if (!mDeepEnabled) { + mDeepEnabled = true; + becomeInactive = true; + pw.println("Deep idle mode enabled"); + } + } + if (arg == null || "light".equals(arg) || "all".equals(arg)) { + valid = true; + if (!mLightEnabled) { + mLightEnabled = true; + becomeInactive = true; + pw.println("Light idle mode enable"); + } + } + if (becomeInactive) { + becomeInactiveIfAppropriateLocked(); + } + if (!valid) { + pw.println("Unknown idle mode: " + arg); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + } else if ("enabled".equals(cmd)) { + synchronized (this) { + String arg = shell.getNextArg(); + if (arg == null || "all".equals(arg)) { + pw.println(mDeepEnabled && mLightEnabled ? "1" : 0); + } else if ("deep".equals(arg)) { + pw.println(mDeepEnabled ? "1" : 0); + } else if ("light".equals(arg)) { + pw.println(mLightEnabled ? "1" : 0); + } else { + pw.println("Unknown idle mode: " + arg); + } + } + } else if ("whitelist".equals(cmd)) { + String arg = shell.getNextArg(); + if (arg != null) { + getContext().enforceCallingOrSelfPermission( + android.Manifest.permission.DEVICE_POWER, null); + long token = Binder.clearCallingIdentity(); + try { + do { + if (arg.length() < 1 || (arg.charAt(0) != '-' + && arg.charAt(0) != '+' && arg.charAt(0) != '=')) { + pw.println("Package must be prefixed with +, -, or =: " + arg); + return -1; + } + char op = arg.charAt(0); + String pkg = arg.substring(1); + if (op == '+') { + if (addPowerSaveWhitelistAppInternal(pkg)) { + pw.println("Added: " + pkg); + } else { + pw.println("Unknown package: " + pkg); + } + } else if (op == '-') { + if (removePowerSaveWhitelistAppInternal(pkg)) { + pw.println("Removed: " + pkg); + } + } else { + pw.println(getPowerSaveWhitelistAppInternal(pkg)); + } + } while ((arg=shell.getNextArg()) != null); + } finally { + Binder.restoreCallingIdentity(token); + } + } else { + synchronized (this) { + for (int j=0; j<mPowerSaveWhitelistAppsExceptIdle.size(); j++) { + pw.print("system-excidle,"); + pw.print(mPowerSaveWhitelistAppsExceptIdle.keyAt(j)); + pw.print(","); + pw.println(mPowerSaveWhitelistAppsExceptIdle.valueAt(j)); + } + for (int j=0; j<mPowerSaveWhitelistApps.size(); j++) { + pw.print("system,"); + pw.print(mPowerSaveWhitelistApps.keyAt(j)); + pw.print(","); + pw.println(mPowerSaveWhitelistApps.valueAt(j)); + } + for (int j=0; j<mPowerSaveWhitelistUserApps.size(); j++) { + pw.print("user,"); + pw.print(mPowerSaveWhitelistUserApps.keyAt(j)); + pw.print(","); + pw.println(mPowerSaveWhitelistUserApps.valueAt(j)); + } + } + } + } else if ("tempwhitelist".equals(cmd)) { + long duration = 10000; + boolean removePkg = false; + String opt; + while ((opt=shell.getNextOption()) != null) { + if ("-u".equals(opt)) { + opt = shell.getNextArg(); + if (opt == null) { + pw.println("-u requires a user number"); + return -1; + } + shell.userId = Integer.parseInt(opt); + } else if ("-d".equals(opt)) { + opt = shell.getNextArg(); + if (opt == null) { + pw.println("-d requires a duration"); + return -1; + } + duration = Long.parseLong(opt); + } else if ("-r".equals(opt)) { + removePkg = true; + } + } + String arg = shell.getNextArg(); + if (arg != null) { + try { + if (removePkg) { + removePowerSaveTempWhitelistAppChecked(arg, shell.userId); + } else { + addPowerSaveTempWhitelistAppChecked(arg, duration, shell.userId, "shell"); + } + } catch (Exception e) { + pw.println("Failed: " + e); + return -1; + } + } else if (removePkg) { + pw.println("[-r] requires a package name"); + return -1; + } else { + dumpTempWhitelistSchedule(pw, false); + } + } else if ("except-idle-whitelist".equals(cmd)) { + getContext().enforceCallingOrSelfPermission( + android.Manifest.permission.DEVICE_POWER, null); + final long token = Binder.clearCallingIdentity(); + try { + String arg = shell.getNextArg(); + if (arg == null) { + pw.println("No arguments given"); + return -1; + } else if ("reset".equals(arg)) { + resetPowerSaveWhitelistExceptIdleInternal(); + } else { + do { + if (arg.length() < 1 || (arg.charAt(0) != '-' + && arg.charAt(0) != '+' && arg.charAt(0) != '=')) { + pw.println("Package must be prefixed with +, -, or =: " + arg); + return -1; + } + char op = arg.charAt(0); + String pkg = arg.substring(1); + if (op == '+') { + if (addPowerSaveWhitelistExceptIdleInternal(pkg)) { + pw.println("Added: " + pkg); + } else { + pw.println("Unknown package: " + pkg); + } + } else if (op == '=') { + pw.println(getPowerSaveWhitelistExceptIdleInternal(pkg)); + } else { + pw.println("Unknown argument: " + arg); + return -1; + } + } while ((arg = shell.getNextArg()) != null); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } else if ("sys-whitelist".equals(cmd)) { + String arg = shell.getNextArg(); + if (arg != null) { + getContext().enforceCallingOrSelfPermission( + android.Manifest.permission.DEVICE_POWER, null); + final long token = Binder.clearCallingIdentity(); + try { + if ("reset".equals(arg)) { + resetSystemPowerWhitelistInternal(); + } else { + do { + if (arg.length() < 1 + || (arg.charAt(0) != '-' && arg.charAt(0) != '+')) { + pw.println("Package must be prefixed with + or - " + arg); + return -1; + } + final char op = arg.charAt(0); + final String pkg = arg.substring(1); + switch (op) { + case '+': + if (restoreSystemPowerWhitelistAppInternal(pkg)) { + pw.println("Restored " + pkg); + } + break; + case '-': + if (removeSystemPowerWhitelistAppInternal(pkg)) { + pw.println("Removed " + pkg); + } + break; + } + } while ((arg = shell.getNextArg()) != null); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } else { + synchronized (this) { + for (int j = 0; j < mPowerSaveWhitelistApps.size(); j++) { + pw.print(mPowerSaveWhitelistApps.keyAt(j)); + pw.print(","); + pw.println(mPowerSaveWhitelistApps.valueAt(j)); + } + } + } + } else if ("motion".equals(cmd)) { + getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, + null); + synchronized (this) { + long token = Binder.clearCallingIdentity(); + try { + motionLocked(); + pw.print("Light state: "); + pw.print(lightStateToString(mLightState)); + pw.print(", deep state: "); + pw.println(stateToString(mState)); + } finally { + Binder.restoreCallingIdentity(token); + } + } + } else if ("pre-idle-factor".equals(cmd)) { + getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, + null); + synchronized (this) { + long token = Binder.clearCallingIdentity(); + int ret = SET_IDLE_FACTOR_RESULT_UNINIT; + try { + String arg = shell.getNextArg(); + boolean valid = false; + int mode = 0; + if (arg != null) { + mode = Integer.parseInt(arg); + ret = setPreIdleTimeoutMode(mode); + if (ret == SET_IDLE_FACTOR_RESULT_OK) { + pw.println("pre-idle-factor: " + mode); + valid = true; + } else if (ret == SET_IDLE_FACTOR_RESULT_NOT_SUPPORT) { + valid = true; + pw.println("Deep idle not supported"); + } else if (ret == SET_IDLE_FACTOR_RESULT_IGNORED) { + valid = true; + pw.println("Idle timeout factor not changed"); + } + } + if (!valid) { + pw.println("Unknown idle timeout factor: " + arg + + ",(error code: " + ret + ")"); + } + } catch (NumberFormatException e) { + pw.println("Unknown idle timeout factor" + + ",(error code: " + ret + ")"); + } finally { + Binder.restoreCallingIdentity(token); + } + } + } else if ("reset-pre-idle-factor".equals(cmd)) { + getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, + null); + synchronized (this) { + long token = Binder.clearCallingIdentity(); + try { + resetPreIdleTimeoutMode(); + } finally { + Binder.restoreCallingIdentity(token); + } + } + } else { + return shell.handleDefaultCommands(cmd); + } + return 0; + } + + void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + if (!DumpUtils.checkDumpPermission(getContext(), TAG, pw)) return; + + if (args != null) { + int userId = UserHandle.USER_SYSTEM; + for (int i=0; i<args.length; i++) { + String arg = args[i]; + if ("-h".equals(arg)) { + dumpHelp(pw); + return; + } else if ("-u".equals(arg)) { + i++; + if (i < args.length) { + arg = args[i]; + userId = Integer.parseInt(arg); + } + } else if ("-a".equals(arg)) { + // Ignore, we always dump all. + } else if (arg.length() > 0 && arg.charAt(0) == '-'){ + pw.println("Unknown option: " + arg); + return; + } else { + Shell shell = new Shell(); + shell.userId = userId; + String[] newArgs = new String[args.length-i]; + System.arraycopy(args, i, newArgs, 0, args.length-i); + shell.exec(mBinderService, null, fd, null, newArgs, null, + new ResultReceiver(null)); + return; + } + } + } + + synchronized (this) { + mConstants.dump(pw); + + if (mEventCmds[0] != EVENT_NULL) { + pw.println(" Idling history:"); + long now = SystemClock.elapsedRealtime(); + for (int i=EVENT_BUFFER_SIZE-1; i>=0; i--) { + int cmd = mEventCmds[i]; + if (cmd == EVENT_NULL) { + continue; + } + String label; + switch (mEventCmds[i]) { + case EVENT_NORMAL: label = " normal"; break; + case EVENT_LIGHT_IDLE: label = " light-idle"; break; + case EVENT_LIGHT_MAINTENANCE: label = "light-maint"; break; + case EVENT_DEEP_IDLE: label = " deep-idle"; break; + case EVENT_DEEP_MAINTENANCE: label = " deep-maint"; break; + default: label = " ??"; break; + } + pw.print(" "); + pw.print(label); + pw.print(": "); + TimeUtils.formatDuration(mEventTimes[i], now, pw); + if (mEventReasons[i] != null) { + pw.print(" ("); + pw.print(mEventReasons[i]); + pw.print(")"); + } + pw.println(); + + } + } + + int size = mPowerSaveWhitelistAppsExceptIdle.size(); + if (size > 0) { + pw.println(" Whitelist (except idle) system apps:"); + for (int i = 0; i < size; i++) { + pw.print(" "); + pw.println(mPowerSaveWhitelistAppsExceptIdle.keyAt(i)); + } + } + size = mPowerSaveWhitelistApps.size(); + if (size > 0) { + pw.println(" Whitelist system apps:"); + for (int i = 0; i < size; i++) { + pw.print(" "); + pw.println(mPowerSaveWhitelistApps.keyAt(i)); + } + } + size = mRemovedFromSystemWhitelistApps.size(); + if (size > 0) { + pw.println(" Removed from whitelist system apps:"); + for (int i = 0; i < size; i++) { + pw.print(" "); + pw.println(mRemovedFromSystemWhitelistApps.keyAt(i)); + } + } + size = mPowerSaveWhitelistUserApps.size(); + if (size > 0) { + pw.println(" Whitelist user apps:"); + for (int i = 0; i < size; i++) { + pw.print(" "); + pw.println(mPowerSaveWhitelistUserApps.keyAt(i)); + } + } + size = mPowerSaveWhitelistExceptIdleAppIds.size(); + if (size > 0) { + pw.println(" Whitelist (except idle) all app ids:"); + for (int i = 0; i < size; i++) { + pw.print(" "); + pw.print(mPowerSaveWhitelistExceptIdleAppIds.keyAt(i)); + pw.println(); + } + } + size = mPowerSaveWhitelistUserAppIds.size(); + if (size > 0) { + pw.println(" Whitelist user app ids:"); + for (int i = 0; i < size; i++) { + pw.print(" "); + pw.print(mPowerSaveWhitelistUserAppIds.keyAt(i)); + pw.println(); + } + } + size = mPowerSaveWhitelistAllAppIds.size(); + if (size > 0) { + pw.println(" Whitelist all app ids:"); + for (int i = 0; i < size; i++) { + pw.print(" "); + pw.print(mPowerSaveWhitelistAllAppIds.keyAt(i)); + pw.println(); + } + } + dumpTempWhitelistSchedule(pw, true); + + size = mTempWhitelistAppIdArray != null ? mTempWhitelistAppIdArray.length : 0; + if (size > 0) { + pw.println(" Temp whitelist app ids:"); + for (int i = 0; i < size; i++) { + pw.print(" "); + pw.print(mTempWhitelistAppIdArray[i]); + pw.println(); + } + } + + pw.print(" mLightEnabled="); pw.print(mLightEnabled); + pw.print(" mDeepEnabled="); pw.println(mDeepEnabled); + pw.print(" mForceIdle="); pw.println(mForceIdle); + pw.print(" mUseMotionSensor="); pw.print(mUseMotionSensor); + if (mUseMotionSensor) { + pw.print(" mMotionSensor="); pw.println(mMotionSensor); + } else { + pw.println(); + } + pw.print(" mScreenOn="); pw.println(mScreenOn); + pw.print(" mScreenLocked="); pw.println(mScreenLocked); + pw.print(" mNetworkConnected="); pw.println(mNetworkConnected); + pw.print(" mCharging="); pw.println(mCharging); + if (mConstraints.size() != 0) { + pw.println(" mConstraints={"); + for (int i = 0; i < mConstraints.size(); i++) { + final DeviceIdleConstraintTracker tracker = mConstraints.valueAt(i); + pw.print(" \""); pw.print(tracker.name); pw.print("\"="); + if (tracker.minState == mState) { + pw.println(tracker.active); + } else { + pw.print("ignored <mMinState="); pw.print(stateToString(tracker.minState)); + pw.println(">"); + } + } + pw.println(" }"); + } + if (mUseMotionSensor) { + pw.print(" mMotionActive="); pw.println(mMotionListener.active); + pw.print(" mNotMoving="); pw.println(mNotMoving); + } + pw.print(" mLocating="); pw.print(mLocating); pw.print(" mHasGps="); + pw.print(mHasGps); pw.print(" mHasNetwork="); + pw.print(mHasNetworkLocation); pw.print(" mLocated="); pw.println(mLocated); + if (mLastGenericLocation != null) { + pw.print(" mLastGenericLocation="); pw.println(mLastGenericLocation); + } + if (mLastGpsLocation != null) { + pw.print(" mLastGpsLocation="); pw.println(mLastGpsLocation); + } + pw.print(" mState="); pw.print(stateToString(mState)); + pw.print(" mLightState="); + pw.println(lightStateToString(mLightState)); + pw.print(" mInactiveTimeout="); TimeUtils.formatDuration(mInactiveTimeout, pw); + pw.println(); + if (mActiveIdleOpCount != 0) { + pw.print(" mActiveIdleOpCount="); pw.println(mActiveIdleOpCount); + } + if (mNextAlarmTime != 0) { + pw.print(" mNextAlarmTime="); + TimeUtils.formatDuration(mNextAlarmTime, SystemClock.elapsedRealtime(), pw); + pw.println(); + } + if (mNextIdlePendingDelay != 0) { + pw.print(" mNextIdlePendingDelay="); + TimeUtils.formatDuration(mNextIdlePendingDelay, pw); + pw.println(); + } + if (mNextIdleDelay != 0) { + pw.print(" mNextIdleDelay="); + TimeUtils.formatDuration(mNextIdleDelay, pw); + pw.println(); + } + if (mNextLightIdleDelay != 0) { + pw.print(" mNextIdleDelay="); + TimeUtils.formatDuration(mNextLightIdleDelay, pw); + pw.println(); + } + if (mNextLightAlarmTime != 0) { + pw.print(" mNextLightAlarmTime="); + TimeUtils.formatDuration(mNextLightAlarmTime, SystemClock.elapsedRealtime(), pw); + pw.println(); + } + if (mCurLightIdleBudget != 0) { + pw.print(" mCurLightIdleBudget="); + TimeUtils.formatDuration(mCurLightIdleBudget, pw); + pw.println(); + } + if (mMaintenanceStartTime != 0) { + pw.print(" mMaintenanceStartTime="); + TimeUtils.formatDuration(mMaintenanceStartTime, SystemClock.elapsedRealtime(), pw); + pw.println(); + } + if (mJobsActive) { + pw.print(" mJobsActive="); pw.println(mJobsActive); + } + if (mAlarmsActive) { + pw.print(" mAlarmsActive="); pw.println(mAlarmsActive); + } + if (Math.abs(mPreIdleFactor - 1.0f) > MIN_PRE_IDLE_FACTOR_CHANGE) { + pw.print(" mPreIdleFactor="); pw.println(mPreIdleFactor); + } + } + } + + void dumpTempWhitelistSchedule(PrintWriter pw, boolean printTitle) { + final int size = mTempWhitelistAppIdEndTimes.size(); + if (size > 0) { + String prefix = ""; + if (printTitle) { + pw.println(" Temp whitelist schedule:"); + prefix = " "; + } + final long timeNow = SystemClock.elapsedRealtime(); + for (int i = 0; i < size; i++) { + pw.print(prefix); + pw.print("UID="); + pw.print(mTempWhitelistAppIdEndTimes.keyAt(i)); + pw.print(": "); + Pair<MutableLong, String> entry = mTempWhitelistAppIdEndTimes.valueAt(i); + TimeUtils.formatDuration(entry.first.value, timeNow, pw); + pw.print(" - "); + pw.println(entry.second); + } + } + } + } diff --git a/apex/jobscheduler/service/java/com/android/server/deviceidle/BluetoothConstraint.java b/apex/jobscheduler/service/java/com/android/server/deviceidle/BluetoothConstraint.java new file mode 100644 index 000000000000..cf181a99f3db --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/deviceidle/BluetoothConstraint.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2018 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.deviceidle; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothManager; +import android.bluetooth.BluetoothProfile; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Handler; +import android.os.Message; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.DeviceIdleInternal; + +// TODO: Should we part of the apex, or the platform?? + +/** + * Track whether there are any active Bluetooth devices connected. + */ +public class BluetoothConstraint implements IDeviceIdleConstraint { + private static final String TAG = BluetoothConstraint.class.getSimpleName(); + private static final long INACTIVITY_TIMEOUT_MS = 20 * 60 * 1000L; + + private final Context mContext; + private final Handler mHandler; + private final DeviceIdleInternal mLocalService; + private final BluetoothManager mBluetoothManager; + + private volatile boolean mConnected = true; + private volatile boolean mMonitoring = false; + + public BluetoothConstraint( + Context context, Handler handler, DeviceIdleInternal localService) { + mContext = context; + mHandler = handler; + mLocalService = localService; + mBluetoothManager = mContext.getSystemService(BluetoothManager.class); + } + + @Override + public synchronized void startMonitoring() { + // Start by assuming we have a connected bluetooth device. + mConnected = true; + mMonitoring = true; + + // Register a receiver to get updates on bluetooth devices disconnecting or the + // adapter state changing. + IntentFilter filter = new IntentFilter(); + filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); + filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED); + filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); + mContext.registerReceiver(mReceiver, filter); + + // Some devices will try to stay connected indefinitely. Set a timeout to ignore them. + mHandler.sendMessageDelayed( + Message.obtain(mHandler, mTimeoutCallback), INACTIVITY_TIMEOUT_MS); + + // Now we have the receiver registered, make a direct check for connected devices. + updateAndReportActiveLocked(); + } + + @Override + public synchronized void stopMonitoring() { + mContext.unregisterReceiver(mReceiver); + mHandler.removeCallbacks(mTimeoutCallback); + mMonitoring = false; + } + + private synchronized void cancelMonitoringDueToTimeout() { + if (mMonitoring) { + mMonitoring = false; + mLocalService.onConstraintStateChanged(this, /* active= */ false); + } + } + + /** + * Check the latest data from BluetoothManager and let DeviceIdleController know whether we + * have connected devices (for example TV remotes / gamepads) and thus want to stay awake. + */ + @GuardedBy("this") + private void updateAndReportActiveLocked() { + final boolean connected = isBluetoothConnected(mBluetoothManager); + if (connected != mConnected) { + mConnected = connected; + // If we lost all of our connections, we are on track to going into idle state. + mLocalService.onConstraintStateChanged(this, /* active= */ mConnected); + } + } + + /** + * True if the bluetooth adapter exists, is enabled, and has at least one GATT device connected. + */ + @VisibleForTesting + static boolean isBluetoothConnected(BluetoothManager bluetoothManager) { + BluetoothAdapter adapter = bluetoothManager.getAdapter(); + if (adapter != null && adapter.isEnabled()) { + return bluetoothManager.getConnectedDevices(BluetoothProfile.GATT).size() > 0; + } + return false; + } + + /** + * Registered in {@link #startMonitoring()}, unregistered in {@link #stopMonitoring()}. + */ + @VisibleForTesting + final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (BluetoothDevice.ACTION_ACL_CONNECTED.equals(intent.getAction())) { + mLocalService.exitIdle("bluetooth"); + } else { + updateAndReportActiveLocked(); + } + } + }; + + private final Runnable mTimeoutCallback = () -> cancelMonitoringDueToTimeout(); +} diff --git a/apex/jobscheduler/service/java/com/android/server/deviceidle/DeviceIdleConstraintTracker.java b/apex/jobscheduler/service/java/com/android/server/deviceidle/DeviceIdleConstraintTracker.java new file mode 100644 index 000000000000..4d5760ef9c86 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/deviceidle/DeviceIdleConstraintTracker.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2018 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.deviceidle; + +/** + * Current state of an {@link IDeviceIdleConstraint}. + * + * If the current doze state is between leastActive and mostActive, then startMonitoring() will + * be the most recent call. Otherwise, stopMonitoring() is the most recent call. + */ +public class DeviceIdleConstraintTracker { + + /** + * Appears in "dumpsys deviceidle". + */ + public final String name; + + /** + * Whenever a constraint is active, it will keep the device at or above + * minState (provided the rule is currently in effect). + * + */ + public final int minState; + + /** + * Whether this constraint currently prevents going below {@link #minState}. + * + * When the state is set to exactly minState, active is automatically + * overwritten with {@code true}. + */ + public boolean active = false; + + /** + * Internal tracking for whether the {@link IDeviceIdleConstraint} on the other + * side has been told it needs to send updates. + */ + public boolean monitoring = false; + + public DeviceIdleConstraintTracker(final String name, int minState) { + this.name = name; + this.minState = minState; + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/deviceidle/TvConstraintController.java b/apex/jobscheduler/service/java/com/android/server/deviceidle/TvConstraintController.java new file mode 100644 index 000000000000..7f0a2717ed4a --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/deviceidle/TvConstraintController.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2018 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.deviceidle; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Handler; + +import com.android.server.DeviceIdleInternal; +import com.android.server.LocalServices; + +/** + * Device idle constraints for television devices. + * + * <p>Televisions are devices with {@code FEATURE_LEANBACK_ONLY}. Other devices might support + * some kind of leanback mode but they should not follow the same rules for idle state. + */ +public class TvConstraintController implements ConstraintController { + private final Context mContext; + private final Handler mHandler; + private final DeviceIdleInternal mDeviceIdleService; + + @Nullable + private final BluetoothConstraint mBluetoothConstraint; + + public TvConstraintController(Context context, Handler handler) { + mContext = context; + mHandler = handler; + mDeviceIdleService = LocalServices.getService(DeviceIdleInternal.class); + + final PackageManager pm = context.getPackageManager(); + mBluetoothConstraint = pm.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH) + ? new BluetoothConstraint(mContext, mHandler, mDeviceIdleService) + : null; + } + + @Override + public void start() { + if (mBluetoothConstraint != null) { + mDeviceIdleService.registerDeviceIdleConstraint( + mBluetoothConstraint, "bluetooth", IDeviceIdleConstraint.SENSING_OR_ABOVE); + } + } + + @Override + public void stop() { + if (mBluetoothConstraint != null) { + mDeviceIdleService.unregisterDeviceIdleConstraint(mBluetoothConstraint); + } + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/GrantedUriPermissions.java b/apex/jobscheduler/service/java/com/android/server/job/GrantedUriPermissions.java new file mode 100644 index 000000000000..b7e8cf6e3fc8 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/GrantedUriPermissions.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2017 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; + +import android.app.UriGrantsManager; +import android.content.ClipData; +import android.content.ContentProvider; +import android.content.Intent; +import android.net.Uri; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.Slog; +import android.util.proto.ProtoOutputStream; +import com.android.server.LocalServices; +import com.android.server.uri.UriGrantsManagerInternal; + +import java.io.PrintWriter; +import java.util.ArrayList; + +public final class GrantedUriPermissions { + private final int mGrantFlags; + private final int mSourceUserId; + private final String mTag; + private final IBinder mPermissionOwner; + private final ArrayList<Uri> mUris = new ArrayList<>(); + + private GrantedUriPermissions(int grantFlags, int uid, String tag) + throws RemoteException { + mGrantFlags = grantFlags; + mSourceUserId = UserHandle.getUserId(uid); + mTag = tag; + mPermissionOwner = LocalServices + .getService(UriGrantsManagerInternal.class).newUriPermissionOwner("job: " + tag); + } + + public void revoke() { + for (int i = mUris.size()-1; i >= 0; i--) { + LocalServices.getService(UriGrantsManagerInternal.class).revokeUriPermissionFromOwner( + mPermissionOwner, mUris.get(i), mGrantFlags, mSourceUserId); + } + mUris.clear(); + } + + public static boolean checkGrantFlags(int grantFlags) { + return (grantFlags & (Intent.FLAG_GRANT_WRITE_URI_PERMISSION + |Intent.FLAG_GRANT_READ_URI_PERMISSION)) != 0; + } + + public static GrantedUriPermissions createFromIntent(Intent intent, + int sourceUid, String targetPackage, int targetUserId, String tag) { + int grantFlags = intent.getFlags(); + if (!checkGrantFlags(grantFlags)) { + return null; + } + + GrantedUriPermissions perms = null; + + Uri data = intent.getData(); + if (data != null) { + perms = grantUri(data, sourceUid, targetPackage, targetUserId, grantFlags, tag, + perms); + } + + ClipData clip = intent.getClipData(); + if (clip != null) { + perms = grantClip(clip, sourceUid, targetPackage, targetUserId, grantFlags, tag, + perms); + } + + return perms; + } + + public static GrantedUriPermissions createFromClip(ClipData clip, + int sourceUid, String targetPackage, int targetUserId, int grantFlags, String tag) { + if (!checkGrantFlags(grantFlags)) { + return null; + } + GrantedUriPermissions perms = null; + if (clip != null) { + perms = grantClip(clip, sourceUid, targetPackage, targetUserId, grantFlags, + tag, perms); + } + return perms; + } + + private static GrantedUriPermissions grantClip(ClipData clip, + int sourceUid, String targetPackage, int targetUserId, int grantFlags, String tag, + GrantedUriPermissions curPerms) { + final int N = clip.getItemCount(); + for (int i = 0; i < N; i++) { + curPerms = grantItem(clip.getItemAt(i), sourceUid, targetPackage, targetUserId, + grantFlags, tag, curPerms); + } + return curPerms; + } + + private static GrantedUriPermissions grantUri(Uri uri, + int sourceUid, String targetPackage, int targetUserId, int grantFlags, String tag, + GrantedUriPermissions curPerms) { + try { + int sourceUserId = ContentProvider.getUserIdFromUri(uri, + UserHandle.getUserId(sourceUid)); + uri = ContentProvider.getUriWithoutUserId(uri); + if (curPerms == null) { + curPerms = new GrantedUriPermissions(grantFlags, sourceUid, tag); + } + UriGrantsManager.getService().grantUriPermissionFromOwner(curPerms.mPermissionOwner, + sourceUid, targetPackage, uri, grantFlags, sourceUserId, targetUserId); + curPerms.mUris.add(uri); + } catch (RemoteException e) { + Slog.e("JobScheduler", "AM dead"); + } + return curPerms; + } + + private static GrantedUriPermissions grantItem(ClipData.Item item, + int sourceUid, String targetPackage, int targetUserId, int grantFlags, String tag, + GrantedUriPermissions curPerms) { + if (item.getUri() != null) { + curPerms = grantUri(item.getUri(), sourceUid, targetPackage, targetUserId, + grantFlags, tag, curPerms); + } + Intent intent = item.getIntent(); + if (intent != null && intent.getData() != null) { + curPerms = grantUri(intent.getData(), sourceUid, targetPackage, targetUserId, + grantFlags, tag, curPerms); + } + return curPerms; + } + + // Dumpsys infrastructure + public void dump(PrintWriter pw, String prefix) { + pw.print(prefix); pw.print("mGrantFlags=0x"); pw.print(Integer.toHexString(mGrantFlags)); + pw.print(" mSourceUserId="); pw.println(mSourceUserId); + pw.print(prefix); pw.print("mTag="); pw.println(mTag); + pw.print(prefix); pw.print("mPermissionOwner="); pw.println(mPermissionOwner); + for (int i = 0; i < mUris.size(); i++) { + pw.print(prefix); pw.print("#"); pw.print(i); pw.print(": "); + pw.println(mUris.get(i)); + } + } + + public void dump(ProtoOutputStream proto, long fieldId) { + final long token = proto.start(fieldId); + + proto.write(GrantedUriPermissionsDumpProto.FLAGS, mGrantFlags); + proto.write(GrantedUriPermissionsDumpProto.SOURCE_USER_ID, mSourceUserId); + proto.write(GrantedUriPermissionsDumpProto.TAG, mTag); + proto.write(GrantedUriPermissionsDumpProto.PERMISSION_OWNER, mPermissionOwner.toString()); + for (int i = 0; i < mUris.size(); i++) { + Uri u = mUris.get(i); + if (u != null) { + proto.write(GrantedUriPermissionsDumpProto.URIS, u.toString()); + } + } + + proto.end(token); + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobCompletedListener.java b/apex/jobscheduler/service/java/com/android/server/job/JobCompletedListener.java new file mode 100644 index 000000000000..34ba753b3daa --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/JobCompletedListener.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2014 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; + +import com.android.server.job.controllers.JobStatus; + +/** + * Used for communication between {@link com.android.server.job.JobServiceContext} and the + * {@link com.android.server.job.JobSchedulerService}. + */ +public interface JobCompletedListener { + /** + * Callback for when a job is completed. + * @param needsReschedule Whether the implementing class should reschedule this job. + */ + void onJobCompletedLocked(JobStatus jobStatus, boolean needsReschedule); +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java new file mode 100644 index 000000000000..525fbaedc140 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java @@ -0,0 +1,725 @@ +/* + * Copyright (C) 2018 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; + +import android.app.ActivityManager; +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.PowerManager; +import android.os.RemoteException; +import android.util.Slog; +import android.util.TimeUtils; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.app.procstats.ProcessStats; +import com.android.internal.os.BackgroundThread; +import com.android.internal.util.IndentingPrintWriter; +import com.android.internal.util.StatLogger; +import com.android.server.job.JobSchedulerService.Constants; +import com.android.server.job.JobSchedulerService.MaxJobCountsPerMemoryTrimLevel; +import com.android.server.job.controllers.JobStatus; +import com.android.server.job.controllers.StateController; + +import java.util.Iterator; +import java.util.List; + +/** + * This class decides, given the various configuration and the system status, how many more jobs + * can start. + */ +class JobConcurrencyManager { + private static final String TAG = JobSchedulerService.TAG; + private static final boolean DEBUG = JobSchedulerService.DEBUG; + + private final Object mLock; + private final JobSchedulerService mService; + private final JobSchedulerService.Constants mConstants; + private final Context mContext; + private final Handler mHandler; + + private PowerManager mPowerManager; + + private boolean mCurrentInteractiveState; + private boolean mEffectiveInteractiveState; + + private long mLastScreenOnRealtime; + private long mLastScreenOffRealtime; + + private static final int MAX_JOB_CONTEXTS_COUNT = JobSchedulerService.MAX_JOB_CONTEXTS_COUNT; + + /** + * This array essentially stores the state of mActiveServices array. + * The ith index stores the job present on the ith JobServiceContext. + * We manipulate this array until we arrive at what jobs should be running on + * what JobServiceContext. + */ + JobStatus[] mRecycledAssignContextIdToJobMap = new JobStatus[MAX_JOB_CONTEXTS_COUNT]; + + boolean[] mRecycledSlotChanged = new boolean[MAX_JOB_CONTEXTS_COUNT]; + + int[] mRecycledPreferredUidForContext = new int[MAX_JOB_CONTEXTS_COUNT]; + + /** Max job counts according to the current system state. */ + private JobSchedulerService.MaxJobCounts mMaxJobCounts; + + private final JobCountTracker mJobCountTracker = new JobCountTracker(); + + /** Current memory trim level. */ + private int mLastMemoryTrimLevel; + + /** Used to throttle heavy API calls. */ + private long mNextSystemStateRefreshTime; + private static final int SYSTEM_STATE_REFRESH_MIN_INTERVAL = 1000; + + private final StatLogger mStatLogger = new StatLogger(new String[]{ + "assignJobsToContexts", + "refreshSystemState", + }); + + interface Stats { + int ASSIGN_JOBS_TO_CONTEXTS = 0; + int REFRESH_SYSTEM_STATE = 1; + + int COUNT = REFRESH_SYSTEM_STATE + 1; + } + + JobConcurrencyManager(JobSchedulerService service) { + mService = service; + mLock = mService.mLock; + mConstants = service.mConstants; + mContext = service.getContext(); + + mHandler = BackgroundThread.getHandler(); + } + + public void onSystemReady() { + mPowerManager = mContext.getSystemService(PowerManager.class); + + final IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON); + filter.addAction(Intent.ACTION_SCREEN_OFF); + mContext.registerReceiver(mReceiver, filter); + + onInteractiveStateChanged(mPowerManager.isInteractive()); + } + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + switch (intent.getAction()) { + case Intent.ACTION_SCREEN_ON: + onInteractiveStateChanged(true); + break; + case Intent.ACTION_SCREEN_OFF: + onInteractiveStateChanged(false); + break; + } + } + }; + + /** + * Called when the screen turns on / off. + */ + private void onInteractiveStateChanged(boolean interactive) { + synchronized (mLock) { + if (mCurrentInteractiveState == interactive) { + return; + } + mCurrentInteractiveState = interactive; + if (DEBUG) { + Slog.d(TAG, "Interactive: " + interactive); + } + + final long nowRealtime = JobSchedulerService.sElapsedRealtimeClock.millis(); + if (interactive) { + mLastScreenOnRealtime = nowRealtime; + mEffectiveInteractiveState = true; + + mHandler.removeCallbacks(mRampUpForScreenOff); + } else { + mLastScreenOffRealtime = nowRealtime; + + // Set mEffectiveInteractiveState to false after the delay, when we may increase + // the concurrency. + // We don't need a wakeup alarm here. When there's a pending job, there should + // also be jobs running too, meaning the device should be awake. + + // Note: we can't directly do postDelayed(this::rampUpForScreenOn), because + // we need the exact same instance for removeCallbacks(). + mHandler.postDelayed(mRampUpForScreenOff, + mConstants.SCREEN_OFF_JOB_CONCURRENCY_INCREASE_DELAY_MS.getValue()); + } + } + } + + private final Runnable mRampUpForScreenOff = this::rampUpForScreenOff; + + /** + * Called in {@link Constants#SCREEN_OFF_JOB_CONCURRENCY_INCREASE_DELAY_MS} after + * the screen turns off, in order to increase concurrency. + */ + private void rampUpForScreenOff() { + synchronized (mLock) { + // Make sure the screen has really been off for the configured duration. + // (There could be a race.) + if (!mEffectiveInteractiveState) { + return; + } + if (mLastScreenOnRealtime > mLastScreenOffRealtime) { + return; + } + final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); + if ((mLastScreenOffRealtime + + mConstants.SCREEN_OFF_JOB_CONCURRENCY_INCREASE_DELAY_MS.getValue()) + > now) { + return; + } + + mEffectiveInteractiveState = false; + + if (DEBUG) { + Slog.d(TAG, "Ramping up concurrency"); + } + + mService.maybeRunPendingJobsLocked(); + } + } + + private boolean isFgJob(JobStatus job) { + return job.lastEvaluatedPriority >= JobInfo.PRIORITY_TOP_APP; + } + + @GuardedBy("mLock") + private void refreshSystemStateLocked() { + final long nowUptime = JobSchedulerService.sUptimeMillisClock.millis(); + + // Only refresh the information every so often. + if (nowUptime < mNextSystemStateRefreshTime) { + return; + } + + final long start = mStatLogger.getTime(); + mNextSystemStateRefreshTime = nowUptime + SYSTEM_STATE_REFRESH_MIN_INTERVAL; + + mLastMemoryTrimLevel = ProcessStats.ADJ_MEM_FACTOR_NORMAL; + try { + mLastMemoryTrimLevel = ActivityManager.getService().getMemoryTrimLevel(); + } catch (RemoteException e) { + } + + mStatLogger.logDurationStat(Stats.REFRESH_SYSTEM_STATE, start); + } + + @GuardedBy("mLock") + private void updateMaxCountsLocked() { + refreshSystemStateLocked(); + + final MaxJobCountsPerMemoryTrimLevel jobCounts = mEffectiveInteractiveState + ? mConstants.MAX_JOB_COUNTS_SCREEN_ON + : mConstants.MAX_JOB_COUNTS_SCREEN_OFF; + + + switch (mLastMemoryTrimLevel) { + case ProcessStats.ADJ_MEM_FACTOR_MODERATE: + mMaxJobCounts = jobCounts.moderate; + break; + case ProcessStats.ADJ_MEM_FACTOR_LOW: + mMaxJobCounts = jobCounts.low; + break; + case ProcessStats.ADJ_MEM_FACTOR_CRITICAL: + mMaxJobCounts = jobCounts.critical; + break; + default: + mMaxJobCounts = jobCounts.normal; + break; + } + } + + /** + * Takes jobs from pending queue and runs them on available contexts. + * If no contexts are available, preempts lower priority jobs to + * run higher priority ones. + * Lock on mJobs before calling this function. + */ + @GuardedBy("mLock") + void assignJobsToContextsLocked() { + final long start = mStatLogger.getTime(); + + assignJobsToContextsInternalLocked(); + + mStatLogger.logDurationStat(Stats.ASSIGN_JOBS_TO_CONTEXTS, start); + } + + @GuardedBy("mLock") + private void assignJobsToContextsInternalLocked() { + if (DEBUG) { + Slog.d(TAG, printPendingQueueLocked()); + } + + final JobPackageTracker tracker = mService.mJobPackageTracker; + final List<JobStatus> pendingJobs = mService.mPendingJobs; + final List<JobServiceContext> activeServices = mService.mActiveServices; + final List<StateController> controllers = mService.mControllers; + + updateMaxCountsLocked(); + + // To avoid GC churn, we recycle the arrays. + JobStatus[] contextIdToJobMap = mRecycledAssignContextIdToJobMap; + boolean[] slotChanged = mRecycledSlotChanged; + int[] preferredUidForContext = mRecycledPreferredUidForContext; + + + // Initialize the work variables and also count running jobs. + mJobCountTracker.reset( + mMaxJobCounts.getMaxTotal(), + mMaxJobCounts.getMaxBg(), + mMaxJobCounts.getMinBg()); + + for (int i=0; i<MAX_JOB_CONTEXTS_COUNT; i++) { + final JobServiceContext js = mService.mActiveServices.get(i); + final JobStatus status = js.getRunningJobLocked(); + + if ((contextIdToJobMap[i] = status) != null) { + mJobCountTracker.incrementRunningJobCount(isFgJob(status)); + } + + slotChanged[i] = false; + preferredUidForContext[i] = js.getPreferredUid(); + } + if (DEBUG) { + Slog.d(TAG, printContextIdToJobMap(contextIdToJobMap, "running jobs initial")); + } + + // Next, update the job priorities, and also count the pending FG / BG jobs. + for (int i = 0; i < pendingJobs.size(); i++) { + final JobStatus pending = pendingJobs.get(i); + + // If job is already running, go to next job. + int jobRunningContext = findJobContextIdFromMap(pending, contextIdToJobMap); + if (jobRunningContext != -1) { + continue; + } + + final int priority = mService.evaluateJobPriorityLocked(pending); + pending.lastEvaluatedPriority = priority; + + mJobCountTracker.incrementPendingJobCount(isFgJob(pending)); + } + + mJobCountTracker.onCountDone(); + + for (int i = 0; i < pendingJobs.size(); i++) { + final JobStatus nextPending = pendingJobs.get(i); + + // Unfortunately we need to repeat this relatively expensive check. + int jobRunningContext = findJobContextIdFromMap(nextPending, contextIdToJobMap); + if (jobRunningContext != -1) { + continue; + } + + final boolean isPendingFg = isFgJob(nextPending); + + // Find an available slot for nextPending. The context should be available OR + // it should have lowest priority among all running jobs + // (sharing the same Uid as nextPending) + int minPriorityForPreemption = Integer.MAX_VALUE; + int selectedContextId = -1; + boolean startingJob = false; + for (int j=0; j<MAX_JOB_CONTEXTS_COUNT; j++) { + JobStatus job = contextIdToJobMap[j]; + int preferredUid = preferredUidForContext[j]; + if (job == null) { + final boolean preferredUidOkay = (preferredUid == nextPending.getUid()) + || (preferredUid == JobServiceContext.NO_PREFERRED_UID); + + if (preferredUidOkay && mJobCountTracker.canJobStart(isPendingFg)) { + // This slot is free, and we haven't yet hit the limit on + // concurrent jobs... we can just throw the job in to here. + selectedContextId = j; + startingJob = true; + break; + } + // No job on this context, but nextPending can't run here because + // the context has a preferred Uid or we have reached the limit on + // concurrent jobs. + continue; + } + if (job.getUid() != nextPending.getUid()) { + continue; + } + + final int jobPriority = mService.evaluateJobPriorityLocked(job); + if (jobPriority >= nextPending.lastEvaluatedPriority) { + continue; + } + + // TODO lastEvaluatedPriority should be evaluateJobPriorityLocked. (double check it) + if (minPriorityForPreemption > nextPending.lastEvaluatedPriority) { + minPriorityForPreemption = nextPending.lastEvaluatedPriority; + selectedContextId = j; + // In this case, we're just going to preempt a low priority job, we're not + // actually starting a job, so don't set startingJob. + } + } + if (selectedContextId != -1) { + contextIdToJobMap[selectedContextId] = nextPending; + slotChanged[selectedContextId] = true; + } + if (startingJob) { + // Increase the counters when we're going to start a job. + mJobCountTracker.onStartingNewJob(isPendingFg); + } + } + if (DEBUG) { + Slog.d(TAG, printContextIdToJobMap(contextIdToJobMap, "running jobs final")); + } + + mJobCountTracker.logStatus(); + + tracker.noteConcurrency(mJobCountTracker.getTotalRunningJobCountToNote(), + mJobCountTracker.getFgRunningJobCountToNote()); + + for (int i=0; i<MAX_JOB_CONTEXTS_COUNT; i++) { + boolean preservePreferredUid = false; + if (slotChanged[i]) { + JobStatus js = activeServices.get(i).getRunningJobLocked(); + if (js != null) { + if (DEBUG) { + Slog.d(TAG, "preempting job: " + + activeServices.get(i).getRunningJobLocked()); + } + // preferredUid will be set to uid of currently running job. + activeServices.get(i).preemptExecutingJobLocked(); + preservePreferredUid = true; + } else { + final JobStatus pendingJob = contextIdToJobMap[i]; + if (DEBUG) { + Slog.d(TAG, "About to run job on context " + + i + ", job: " + pendingJob); + } + for (int ic=0; ic<controllers.size(); ic++) { + controllers.get(ic).prepareForExecutionLocked(pendingJob); + } + if (!activeServices.get(i).executeRunnableJob(pendingJob)) { + Slog.d(TAG, "Error executing " + pendingJob); + } + if (pendingJobs.remove(pendingJob)) { + tracker.noteNonpending(pendingJob); + } + } + } + if (!preservePreferredUid) { + activeServices.get(i).clearPreferredUid(); + } + } + } + + private static int findJobContextIdFromMap(JobStatus jobStatus, JobStatus[] map) { + for (int i=0; i<map.length; i++) { + if (map[i] != null && map[i].matches(jobStatus.getUid(), jobStatus.getJobId())) { + return i; + } + } + return -1; + } + + @GuardedBy("mLock") + private String printPendingQueueLocked() { + StringBuilder s = new StringBuilder("Pending queue: "); + Iterator<JobStatus> it = mService.mPendingJobs.iterator(); + while (it.hasNext()) { + JobStatus js = it.next(); + s.append("(") + .append(js.getJob().getId()) + .append(", ") + .append(js.getUid()) + .append(") "); + } + return s.toString(); + } + + private static String printContextIdToJobMap(JobStatus[] map, String initial) { + StringBuilder s = new StringBuilder(initial + ": "); + for (int i=0; i<map.length; i++) { + s.append("(") + .append(map[i] == null? -1: map[i].getJobId()) + .append(map[i] == null? -1: map[i].getUid()) + .append(")" ); + } + return s.toString(); + } + + + public void dumpLocked(IndentingPrintWriter pw, long now, long nowRealtime) { + pw.println("Concurrency:"); + + pw.increaseIndent(); + try { + pw.print("Screen state: current "); + pw.print(mCurrentInteractiveState ? "ON" : "OFF"); + pw.print(" effective "); + pw.print(mEffectiveInteractiveState ? "ON" : "OFF"); + pw.println(); + + pw.print("Last screen ON: "); + TimeUtils.dumpTimeWithDelta(pw, now - nowRealtime + mLastScreenOnRealtime, now); + pw.println(); + + pw.print("Last screen OFF: "); + TimeUtils.dumpTimeWithDelta(pw, now - nowRealtime + mLastScreenOffRealtime, now); + pw.println(); + + pw.println(); + + pw.println("Current max jobs:"); + pw.println(" "); + pw.println(mJobCountTracker); + + pw.println(); + + pw.print("mLastMemoryTrimLevel: "); + pw.print(mLastMemoryTrimLevel); + pw.println(); + + mStatLogger.dump(pw); + } finally { + pw.decreaseIndent(); + } + } + + public void dumpProtoLocked(ProtoOutputStream proto, long tag, long now, long nowRealtime) { + final long token = proto.start(tag); + + proto.write(JobConcurrencyManagerProto.CURRENT_INTERACTIVE_STATE, mCurrentInteractiveState); + proto.write(JobConcurrencyManagerProto.EFFECTIVE_INTERACTIVE_STATE, + mEffectiveInteractiveState); + + proto.write(JobConcurrencyManagerProto.TIME_SINCE_LAST_SCREEN_ON_MS, + nowRealtime - mLastScreenOnRealtime); + proto.write(JobConcurrencyManagerProto.TIME_SINCE_LAST_SCREEN_OFF_MS, + nowRealtime - mLastScreenOffRealtime); + + mJobCountTracker.dumpProto(proto, JobConcurrencyManagerProto.JOB_COUNT_TRACKER); + + proto.write(JobConcurrencyManagerProto.MEMORY_TRIM_LEVEL, mLastMemoryTrimLevel); + + mStatLogger.dumpProto(proto, JobConcurrencyManagerProto.STATS); + + proto.end(token); + } + + /** + * This class decides, taking into account {@link #mMaxJobCounts} and how mny jos are running / + * pending, how many more job can start. + * + * Extracted for testing and logging. + */ + @VisibleForTesting + static class JobCountTracker { + private int mConfigNumMaxTotalJobs; + private int mConfigNumMaxBgJobs; + private int mConfigNumMinBgJobs; + + private int mNumRunningFgJobs; + private int mNumRunningBgJobs; + + private int mNumPendingFgJobs; + private int mNumPendingBgJobs; + + private int mNumStartingFgJobs; + private int mNumStartingBgJobs; + + private int mNumReservedForBg; + private int mNumActualMaxFgJobs; + private int mNumActualMaxBgJobs; + + void reset(int numTotalMaxJobs, int numMaxBgJobs, int numMinBgJobs) { + mConfigNumMaxTotalJobs = numTotalMaxJobs; + mConfigNumMaxBgJobs = numMaxBgJobs; + mConfigNumMinBgJobs = numMinBgJobs; + + mNumRunningFgJobs = 0; + mNumRunningBgJobs = 0; + + mNumPendingFgJobs = 0; + mNumPendingBgJobs = 0; + + mNumStartingFgJobs = 0; + mNumStartingBgJobs = 0; + + mNumReservedForBg = 0; + mNumActualMaxFgJobs = 0; + mNumActualMaxBgJobs = 0; + } + + void incrementRunningJobCount(boolean isFg) { + if (isFg) { + mNumRunningFgJobs++; + } else { + mNumRunningBgJobs++; + } + } + + void incrementPendingJobCount(boolean isFg) { + if (isFg) { + mNumPendingFgJobs++; + } else { + mNumPendingBgJobs++; + } + } + + void onStartingNewJob(boolean isFg) { + if (isFg) { + mNumStartingFgJobs++; + } else { + mNumStartingBgJobs++; + } + } + + void onCountDone() { + // Note some variables are used only here but are made class members in order to have + // them on logcat / dumpsys. + + // How many slots should we allocate to BG jobs at least? + // That's basically "getMinBg()", but if there are less jobs, decrease it. + // (e.g. even if min-bg is 2, if there's only 1 running+pending job, this has to be 1.) + final int reservedForBg = Math.min( + mConfigNumMinBgJobs, + mNumRunningBgJobs + mNumPendingBgJobs); + + // However, if there are FG jobs already running, we have to adjust it. + mNumReservedForBg = Math.min(reservedForBg, + mConfigNumMaxTotalJobs - mNumRunningFgJobs); + + // Max FG is [total - [number needed for BG jobs]] + // [number needed for BG jobs] is the bigger one of [running BG] or [reserved BG] + final int maxFg = + mConfigNumMaxTotalJobs - Math.max(mNumRunningBgJobs, mNumReservedForBg); + + // The above maxFg is the theoretical max. If there are less FG jobs, the actual + // max FG will be lower accordingly. + mNumActualMaxFgJobs = Math.min( + maxFg, + mNumRunningFgJobs + mNumPendingFgJobs); + + // Max BG is [total - actual max FG], but cap at [config max BG]. + final int maxBg = Math.min( + mConfigNumMaxBgJobs, + mConfigNumMaxTotalJobs - mNumActualMaxFgJobs); + + // If there are less BG jobs than maxBg, then reduce the actual max BG accordingly. + // This isn't needed for the logic to work, but this will give consistent output + // on logcat and dumpsys. + mNumActualMaxBgJobs = Math.min( + maxBg, + mNumRunningBgJobs + mNumPendingBgJobs); + } + + boolean canJobStart(boolean isFg) { + if (isFg) { + return mNumRunningFgJobs + mNumStartingFgJobs < mNumActualMaxFgJobs; + } else { + return mNumRunningBgJobs + mNumStartingBgJobs < mNumActualMaxBgJobs; + } + } + + public int getNumStartingFgJobs() { + return mNumStartingFgJobs; + } + + public int getNumStartingBgJobs() { + return mNumStartingBgJobs; + } + + int getTotalRunningJobCountToNote() { + return mNumRunningFgJobs + mNumRunningBgJobs + + mNumStartingFgJobs + mNumStartingBgJobs; + } + + int getFgRunningJobCountToNote() { + return mNumRunningFgJobs + mNumStartingFgJobs; + } + + void logStatus() { + if (DEBUG) { + Slog.d(TAG, "assignJobsToContexts: " + this); + } + } + + public String toString() { + final int totalFg = mNumRunningFgJobs + mNumStartingFgJobs; + final int totalBg = mNumRunningBgJobs + mNumStartingBgJobs; + return String.format( + "Config={tot=%d bg min/max=%d/%d}" + + " Running[FG/BG (total)]: %d / %d (%d)" + + " Pending: %d / %d (%d)" + + " Actual max: %d%s / %d%s (%d%s)" + + " Res BG: %d" + + " Starting: %d / %d (%d)" + + " Total: %d%s / %d%s (%d%s)", + mConfigNumMaxTotalJobs, mConfigNumMinBgJobs, mConfigNumMaxBgJobs, + + mNumRunningFgJobs, mNumRunningBgJobs, mNumRunningFgJobs + mNumRunningBgJobs, + + mNumPendingFgJobs, mNumPendingBgJobs, mNumPendingFgJobs + mNumPendingBgJobs, + + mNumActualMaxFgJobs, (totalFg <= mConfigNumMaxTotalJobs) ? "" : "*", + mNumActualMaxBgJobs, (totalBg <= mConfigNumMaxBgJobs) ? "" : "*", + mNumActualMaxFgJobs + mNumActualMaxBgJobs, + (mNumActualMaxFgJobs + mNumActualMaxBgJobs <= mConfigNumMaxTotalJobs) + ? "" : "*", + + mNumReservedForBg, + + mNumStartingFgJobs, mNumStartingBgJobs, mNumStartingFgJobs + mNumStartingBgJobs, + + totalFg, (totalFg <= mNumActualMaxFgJobs) ? "" : "*", + totalBg, (totalBg <= mNumActualMaxBgJobs) ? "" : "*", + totalFg + totalBg, (totalFg + totalBg <= mConfigNumMaxTotalJobs) ? "" : "*" + ); + } + + public void dumpProto(ProtoOutputStream proto, long fieldId) { + final long token = proto.start(fieldId); + + proto.write(JobCountTrackerProto.CONFIG_NUM_MAX_TOTAL_JOBS, mConfigNumMaxTotalJobs); + proto.write(JobCountTrackerProto.CONFIG_NUM_MAX_BG_JOBS, mConfigNumMaxBgJobs); + proto.write(JobCountTrackerProto.CONFIG_NUM_MIN_BG_JOBS, mConfigNumMinBgJobs); + + proto.write(JobCountTrackerProto.NUM_RUNNING_FG_JOBS, mNumRunningFgJobs); + proto.write(JobCountTrackerProto.NUM_RUNNING_BG_JOBS, mNumRunningBgJobs); + + proto.write(JobCountTrackerProto.NUM_PENDING_FG_JOBS, mNumPendingFgJobs); + proto.write(JobCountTrackerProto.NUM_PENDING_BG_JOBS, mNumPendingBgJobs); + + proto.write(JobCountTrackerProto.NUM_ACTUAL_MAX_FG_JOBS, mNumActualMaxFgJobs); + proto.write(JobCountTrackerProto.NUM_ACTUAL_MAX_BG_JOBS, mNumActualMaxBgJobs); + + proto.write(JobCountTrackerProto.NUM_RESERVED_FOR_BG, mNumReservedForBg); + + proto.write(JobCountTrackerProto.NUM_STARTING_FG_JOBS, mNumStartingFgJobs); + proto.write(JobCountTrackerProto.NUM_STARTING_BG_JOBS, mNumStartingBgJobs); + + proto.end(token); + } + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobPackageTracker.java b/apex/jobscheduler/service/java/com/android/server/job/JobPackageTracker.java new file mode 100644 index 000000000000..e28e5bd6c53d --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/JobPackageTracker.java @@ -0,0 +1,653 @@ +/* + * 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; + +import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; +import static com.android.server.job.JobSchedulerService.sSystemClock; +import static com.android.server.job.JobSchedulerService.sUptimeMillisClock; + +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.os.UserHandle; +import android.text.format.DateFormat; +import android.util.ArrayMap; +import android.util.SparseArray; +import android.util.SparseIntArray; +import android.util.TimeUtils; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.util.RingBufferIndices; +import com.android.server.job.controllers.JobStatus; + +import java.io.PrintWriter; + +public final class JobPackageTracker { + // We batch every 30 minutes. + static final long BATCHING_TIME = 30*60*1000; + // Number of historical data sets we keep. + static final int NUM_HISTORY = 5; + + private static final int EVENT_BUFFER_SIZE = 100; + + public static final int EVENT_CMD_MASK = 0xff; + public static final int EVENT_STOP_REASON_SHIFT = 8; + public static final int EVENT_STOP_REASON_MASK = 0xff << EVENT_STOP_REASON_SHIFT; + public static final int EVENT_NULL = 0; + public static final int EVENT_START_JOB = 1; + public static final int EVENT_STOP_JOB = 2; + public static final int EVENT_START_PERIODIC_JOB = 3; + public static final int EVENT_STOP_PERIODIC_JOB = 4; + + private final RingBufferIndices mEventIndices = new RingBufferIndices(EVENT_BUFFER_SIZE); + private final int[] mEventCmds = new int[EVENT_BUFFER_SIZE]; + private final long[] mEventTimes = new long[EVENT_BUFFER_SIZE]; + private final int[] mEventUids = new int[EVENT_BUFFER_SIZE]; + private final String[] mEventTags = new String[EVENT_BUFFER_SIZE]; + private final int[] mEventJobIds = new int[EVENT_BUFFER_SIZE]; + private final String[] mEventReasons = new String[EVENT_BUFFER_SIZE]; + + public void addEvent(int cmd, int uid, String tag, int jobId, int stopReason, + String debugReason) { + int index = mEventIndices.add(); + mEventCmds[index] = cmd | ((stopReason<<EVENT_STOP_REASON_SHIFT) & EVENT_STOP_REASON_MASK); + mEventTimes[index] = sElapsedRealtimeClock.millis(); + mEventUids[index] = uid; + mEventTags[index] = tag; + mEventJobIds[index] = jobId; + mEventReasons[index] = debugReason; + } + + DataSet mCurDataSet = new DataSet(); + DataSet[] mLastDataSets = new DataSet[NUM_HISTORY]; + + final static class PackageEntry { + long pastActiveTime; + long activeStartTime; + int activeNesting; + int activeCount; + boolean hadActive; + long pastActiveTopTime; + long activeTopStartTime; + int activeTopNesting; + int activeTopCount; + boolean hadActiveTop; + long pastPendingTime; + long pendingStartTime; + int pendingNesting; + int pendingCount; + boolean hadPending; + final SparseIntArray stopReasons = new SparseIntArray(); + + public long getActiveTime(long now) { + long time = pastActiveTime; + if (activeNesting > 0) { + time += now - activeStartTime; + } + return time; + } + + public long getActiveTopTime(long now) { + long time = pastActiveTopTime; + if (activeTopNesting > 0) { + time += now - activeTopStartTime; + } + return time; + } + + public long getPendingTime(long now) { + long time = pastPendingTime; + if (pendingNesting > 0) { + time += now - pendingStartTime; + } + return time; + } + } + + final static class DataSet { + final SparseArray<ArrayMap<String, PackageEntry>> mEntries = new SparseArray<>(); + final long mStartUptimeTime; + final long mStartElapsedTime; + final long mStartClockTime; + long mSummedTime; + int mMaxTotalActive; + int mMaxFgActive; + + public DataSet(DataSet otherTimes) { + mStartUptimeTime = otherTimes.mStartUptimeTime; + mStartElapsedTime = otherTimes.mStartElapsedTime; + mStartClockTime = otherTimes.mStartClockTime; + } + + public DataSet() { + mStartUptimeTime = sUptimeMillisClock.millis(); + mStartElapsedTime = sElapsedRealtimeClock.millis(); + mStartClockTime = sSystemClock.millis(); + } + + private PackageEntry getOrCreateEntry(int uid, String pkg) { + ArrayMap<String, PackageEntry> uidMap = mEntries.get(uid); + if (uidMap == null) { + uidMap = new ArrayMap<>(); + mEntries.put(uid, uidMap); + } + PackageEntry entry = uidMap.get(pkg); + if (entry == null) { + entry = new PackageEntry(); + uidMap.put(pkg, entry); + } + return entry; + } + + public PackageEntry getEntry(int uid, String pkg) { + ArrayMap<String, PackageEntry> uidMap = mEntries.get(uid); + if (uidMap == null) { + return null; + } + return uidMap.get(pkg); + } + + long getTotalTime(long now) { + if (mSummedTime > 0) { + return mSummedTime; + } + return now - mStartUptimeTime; + } + + void incPending(int uid, String pkg, long now) { + PackageEntry pe = getOrCreateEntry(uid, pkg); + if (pe.pendingNesting == 0) { + pe.pendingStartTime = now; + pe.pendingCount++; + } + pe.pendingNesting++; + } + + void decPending(int uid, String pkg, long now) { + PackageEntry pe = getOrCreateEntry(uid, pkg); + if (pe.pendingNesting == 1) { + pe.pastPendingTime += now - pe.pendingStartTime; + } + pe.pendingNesting--; + } + + void incActive(int uid, String pkg, long now) { + PackageEntry pe = getOrCreateEntry(uid, pkg); + if (pe.activeNesting == 0) { + pe.activeStartTime = now; + pe.activeCount++; + } + pe.activeNesting++; + } + + void decActive(int uid, String pkg, long now, int stopReason) { + PackageEntry pe = getOrCreateEntry(uid, pkg); + if (pe.activeNesting == 1) { + pe.pastActiveTime += now - pe.activeStartTime; + } + pe.activeNesting--; + int count = pe.stopReasons.get(stopReason, 0); + pe.stopReasons.put(stopReason, count+1); + } + + void incActiveTop(int uid, String pkg, long now) { + PackageEntry pe = getOrCreateEntry(uid, pkg); + if (pe.activeTopNesting == 0) { + pe.activeTopStartTime = now; + pe.activeTopCount++; + } + pe.activeTopNesting++; + } + + void decActiveTop(int uid, String pkg, long now, int stopReason) { + PackageEntry pe = getOrCreateEntry(uid, pkg); + if (pe.activeTopNesting == 1) { + pe.pastActiveTopTime += now - pe.activeTopStartTime; + } + pe.activeTopNesting--; + int count = pe.stopReasons.get(stopReason, 0); + pe.stopReasons.put(stopReason, count+1); + } + + void finish(DataSet next, long now) { + for (int i = mEntries.size() - 1; i >= 0; i--) { + ArrayMap<String, PackageEntry> uidMap = mEntries.valueAt(i); + for (int j = uidMap.size() - 1; j >= 0; j--) { + PackageEntry pe = uidMap.valueAt(j); + if (pe.activeNesting > 0 || pe.activeTopNesting > 0 || pe.pendingNesting > 0) { + // Propagate existing activity in to next data set. + PackageEntry nextPe = next.getOrCreateEntry(mEntries.keyAt(i), uidMap.keyAt(j)); + nextPe.activeStartTime = now; + nextPe.activeNesting = pe.activeNesting; + nextPe.activeTopStartTime = now; + nextPe.activeTopNesting = pe.activeTopNesting; + nextPe.pendingStartTime = now; + nextPe.pendingNesting = pe.pendingNesting; + // Finish it off. + if (pe.activeNesting > 0) { + pe.pastActiveTime += now - pe.activeStartTime; + pe.activeNesting = 0; + } + if (pe.activeTopNesting > 0) { + pe.pastActiveTopTime += now - pe.activeTopStartTime; + pe.activeTopNesting = 0; + } + if (pe.pendingNesting > 0) { + pe.pastPendingTime += now - pe.pendingStartTime; + pe.pendingNesting = 0; + } + } + } + } + } + + void addTo(DataSet out, long now) { + out.mSummedTime += getTotalTime(now); + for (int i = mEntries.size() - 1; i >= 0; i--) { + ArrayMap<String, PackageEntry> uidMap = mEntries.valueAt(i); + for (int j = uidMap.size() - 1; j >= 0; j--) { + PackageEntry pe = uidMap.valueAt(j); + PackageEntry outPe = out.getOrCreateEntry(mEntries.keyAt(i), uidMap.keyAt(j)); + outPe.pastActiveTime += pe.pastActiveTime; + outPe.activeCount += pe.activeCount; + outPe.pastActiveTopTime += pe.pastActiveTopTime; + outPe.activeTopCount += pe.activeTopCount; + outPe.pastPendingTime += pe.pastPendingTime; + outPe.pendingCount += pe.pendingCount; + if (pe.activeNesting > 0) { + outPe.pastActiveTime += now - pe.activeStartTime; + outPe.hadActive = true; + } + if (pe.activeTopNesting > 0) { + outPe.pastActiveTopTime += now - pe.activeTopStartTime; + outPe.hadActiveTop = true; + } + if (pe.pendingNesting > 0) { + outPe.pastPendingTime += now - pe.pendingStartTime; + outPe.hadPending = true; + } + for (int k = pe.stopReasons.size()-1; k >= 0; k--) { + int type = pe.stopReasons.keyAt(k); + outPe.stopReasons.put(type, outPe.stopReasons.get(type, 0) + + pe.stopReasons.valueAt(k)); + } + } + } + if (mMaxTotalActive > out.mMaxTotalActive) { + out.mMaxTotalActive = mMaxTotalActive; + } + if (mMaxFgActive > out.mMaxFgActive) { + out.mMaxFgActive = mMaxFgActive; + } + } + + void printDuration(PrintWriter pw, long period, long duration, int count, String suffix) { + float fraction = duration / (float) period; + int percent = (int) ((fraction * 100) + .5f); + if (percent > 0) { + pw.print(" "); + pw.print(percent); + pw.print("% "); + pw.print(count); + pw.print("x "); + pw.print(suffix); + } else if (count > 0) { + pw.print(" "); + pw.print(count); + pw.print("x "); + pw.print(suffix); + } + } + + void dump(PrintWriter pw, String header, String prefix, long now, long nowElapsed, + int filterUid) { + final long period = getTotalTime(now); + pw.print(prefix); pw.print(header); pw.print(" at "); + pw.print(DateFormat.format("yyyy-MM-dd-HH-mm-ss", mStartClockTime).toString()); + pw.print(" ("); + TimeUtils.formatDuration(mStartElapsedTime, nowElapsed, pw); + pw.print(") over "); + TimeUtils.formatDuration(period, pw); + pw.println(":"); + final int NE = mEntries.size(); + for (int i = 0; i < NE; i++) { + int uid = mEntries.keyAt(i); + if (filterUid != -1 && filterUid != UserHandle.getAppId(uid)) { + continue; + } + ArrayMap<String, PackageEntry> uidMap = mEntries.valueAt(i); + final int NP = uidMap.size(); + for (int j = 0; j < NP; j++) { + PackageEntry pe = uidMap.valueAt(j); + pw.print(prefix); pw.print(" "); + UserHandle.formatUid(pw, uid); + pw.print(" / "); pw.print(uidMap.keyAt(j)); + pw.println(":"); + pw.print(prefix); pw.print(" "); + printDuration(pw, period, pe.getPendingTime(now), pe.pendingCount, "pending"); + printDuration(pw, period, pe.getActiveTime(now), pe.activeCount, "active"); + printDuration(pw, period, pe.getActiveTopTime(now), pe.activeTopCount, + "active-top"); + if (pe.pendingNesting > 0 || pe.hadPending) { + pw.print(" (pending)"); + } + if (pe.activeNesting > 0 || pe.hadActive) { + pw.print(" (active)"); + } + if (pe.activeTopNesting > 0 || pe.hadActiveTop) { + pw.print(" (active-top)"); + } + pw.println(); + if (pe.stopReasons.size() > 0) { + pw.print(prefix); pw.print(" "); + for (int k = 0; k < pe.stopReasons.size(); k++) { + if (k > 0) { + pw.print(", "); + } + pw.print(pe.stopReasons.valueAt(k)); + pw.print("x "); + pw.print(JobParameters.getReasonName(pe.stopReasons.keyAt(k))); + } + pw.println(); + } + } + } + pw.print(prefix); pw.print(" Max concurrency: "); + pw.print(mMaxTotalActive); pw.print(" total, "); + pw.print(mMaxFgActive); pw.println(" foreground"); + } + + private void printPackageEntryState(ProtoOutputStream proto, long fieldId, + long duration, int count) { + final long token = proto.start(fieldId); + proto.write(DataSetProto.PackageEntryProto.State.DURATION_MS, duration); + proto.write(DataSetProto.PackageEntryProto.State.COUNT, count); + proto.end(token); + } + + void dump(ProtoOutputStream proto, long fieldId, long now, long nowElapsed, int filterUid) { + final long token = proto.start(fieldId); + final long period = getTotalTime(now); + + proto.write(DataSetProto.START_CLOCK_TIME_MS, mStartClockTime); + proto.write(DataSetProto.ELAPSED_TIME_MS, nowElapsed - mStartElapsedTime); + proto.write(DataSetProto.PERIOD_MS, period); + + final int NE = mEntries.size(); + for (int i = 0; i < NE; i++) { + int uid = mEntries.keyAt(i); + if (filterUid != -1 && filterUid != UserHandle.getAppId(uid)) { + continue; + } + ArrayMap<String, PackageEntry> uidMap = mEntries.valueAt(i); + final int NP = uidMap.size(); + for (int j = 0; j < NP; j++) { + final long peToken = proto.start(DataSetProto.PACKAGE_ENTRIES); + PackageEntry pe = uidMap.valueAt(j); + + proto.write(DataSetProto.PackageEntryProto.UID, uid); + proto.write(DataSetProto.PackageEntryProto.PACKAGE_NAME, uidMap.keyAt(j)); + + printPackageEntryState(proto, DataSetProto.PackageEntryProto.PENDING_STATE, + pe.getPendingTime(now), pe.pendingCount); + printPackageEntryState(proto, DataSetProto.PackageEntryProto.ACTIVE_STATE, + pe.getActiveTime(now), pe.activeCount); + printPackageEntryState(proto, DataSetProto.PackageEntryProto.ACTIVE_TOP_STATE, + pe.getActiveTopTime(now), pe.activeTopCount); + + proto.write(DataSetProto.PackageEntryProto.PENDING, + pe.pendingNesting > 0 || pe.hadPending); + proto.write(DataSetProto.PackageEntryProto.ACTIVE, + pe.activeNesting > 0 || pe.hadActive); + proto.write(DataSetProto.PackageEntryProto.ACTIVE_TOP, + pe.activeTopNesting > 0 || pe.hadActiveTop); + + for (int k = 0; k < pe.stopReasons.size(); k++) { + final long srcToken = + proto.start(DataSetProto.PackageEntryProto.STOP_REASONS); + + proto.write(DataSetProto.PackageEntryProto.StopReasonCount.REASON, + pe.stopReasons.keyAt(k)); + proto.write(DataSetProto.PackageEntryProto.StopReasonCount.COUNT, + pe.stopReasons.valueAt(k)); + + proto.end(srcToken); + } + + proto.end(peToken); + } + } + + proto.write(DataSetProto.MAX_CONCURRENCY, mMaxTotalActive); + proto.write(DataSetProto.MAX_FOREGROUND_CONCURRENCY, mMaxFgActive); + + proto.end(token); + } + } + + void rebatchIfNeeded(long now) { + long totalTime = mCurDataSet.getTotalTime(now); + if (totalTime > BATCHING_TIME) { + DataSet last = mCurDataSet; + last.mSummedTime = totalTime; + mCurDataSet = new DataSet(); + last.finish(mCurDataSet, now); + System.arraycopy(mLastDataSets, 0, mLastDataSets, 1, mLastDataSets.length-1); + mLastDataSets[0] = last; + } + } + + public void notePending(JobStatus job) { + final long now = sUptimeMillisClock.millis(); + job.madePending = now; + rebatchIfNeeded(now); + mCurDataSet.incPending(job.getSourceUid(), job.getSourcePackageName(), now); + } + + public void noteNonpending(JobStatus job) { + final long now = sUptimeMillisClock.millis(); + mCurDataSet.decPending(job.getSourceUid(), job.getSourcePackageName(), now); + rebatchIfNeeded(now); + } + + public void noteActive(JobStatus job) { + final long now = sUptimeMillisClock.millis(); + job.madeActive = now; + rebatchIfNeeded(now); + if (job.lastEvaluatedPriority >= JobInfo.PRIORITY_TOP_APP) { + mCurDataSet.incActiveTop(job.getSourceUid(), job.getSourcePackageName(), now); + } else { + mCurDataSet.incActive(job.getSourceUid(), job.getSourcePackageName(), now); + } + addEvent(job.getJob().isPeriodic() ? EVENT_START_PERIODIC_JOB : EVENT_START_JOB, + job.getSourceUid(), job.getBatteryName(), job.getJobId(), 0, null); + } + + public void noteInactive(JobStatus job, int stopReason, String debugReason) { + final long now = sUptimeMillisClock.millis(); + if (job.lastEvaluatedPriority >= JobInfo.PRIORITY_TOP_APP) { + mCurDataSet.decActiveTop(job.getSourceUid(), job.getSourcePackageName(), now, + stopReason); + } else { + mCurDataSet.decActive(job.getSourceUid(), job.getSourcePackageName(), now, stopReason); + } + rebatchIfNeeded(now); + addEvent(job.getJob().isPeriodic() ? EVENT_STOP_JOB : EVENT_STOP_PERIODIC_JOB, + job.getSourceUid(), job.getBatteryName(), job.getJobId(), stopReason, debugReason); + } + + public void noteConcurrency(int totalActive, int fgActive) { + if (totalActive > mCurDataSet.mMaxTotalActive) { + mCurDataSet.mMaxTotalActive = totalActive; + } + if (fgActive > mCurDataSet.mMaxFgActive) { + mCurDataSet.mMaxFgActive = fgActive; + } + } + + public float getLoadFactor(JobStatus job) { + final int uid = job.getSourceUid(); + final String pkg = job.getSourcePackageName(); + PackageEntry cur = mCurDataSet.getEntry(uid, pkg); + PackageEntry last = mLastDataSets[0] != null ? mLastDataSets[0].getEntry(uid, pkg) : null; + if (cur == null && last == null) { + return 0; + } + final long now = sUptimeMillisClock.millis(); + long time = 0; + if (cur != null) { + time += cur.getActiveTime(now) + cur.getPendingTime(now); + } + long period = mCurDataSet.getTotalTime(now); + if (last != null) { + time += last.getActiveTime(now) + last.getPendingTime(now); + period += mLastDataSets[0].getTotalTime(now); + } + return time / (float)period; + } + + public void dump(PrintWriter pw, String prefix, int filterUid) { + final long now = sUptimeMillisClock.millis(); + final long nowElapsed = sElapsedRealtimeClock.millis(); + final DataSet total; + if (mLastDataSets[0] != null) { + total = new DataSet(mLastDataSets[0]); + mLastDataSets[0].addTo(total, now); + } else { + total = new DataSet(mCurDataSet); + } + mCurDataSet.addTo(total, now); + for (int i = 1; i < mLastDataSets.length; i++) { + if (mLastDataSets[i] != null) { + mLastDataSets[i].dump(pw, "Historical stats", prefix, now, nowElapsed, filterUid); + pw.println(); + } + } + total.dump(pw, "Current stats", prefix, now, nowElapsed, filterUid); + } + + public void dump(ProtoOutputStream proto, long fieldId, int filterUid) { + final long token = proto.start(fieldId); + final long now = sUptimeMillisClock.millis(); + final long nowElapsed = sElapsedRealtimeClock.millis(); + + final DataSet total; + if (mLastDataSets[0] != null) { + total = new DataSet(mLastDataSets[0]); + mLastDataSets[0].addTo(total, now); + } else { + total = new DataSet(mCurDataSet); + } + mCurDataSet.addTo(total, now); + + for (int i = 1; i < mLastDataSets.length; i++) { + if (mLastDataSets[i] != null) { + mLastDataSets[i].dump(proto, JobPackageTrackerDumpProto.HISTORICAL_STATS, + now, nowElapsed, filterUid); + } + } + total.dump(proto, JobPackageTrackerDumpProto.CURRENT_STATS, + now, nowElapsed, filterUid); + + proto.end(token); + } + + public boolean dumpHistory(PrintWriter pw, String prefix, int filterUid) { + final int size = mEventIndices.size(); + if (size <= 0) { + return false; + } + pw.println(" Job history:"); + final long now = sElapsedRealtimeClock.millis(); + for (int i=0; i<size; i++) { + final int index = mEventIndices.indexOf(i); + final int uid = mEventUids[index]; + if (filterUid != -1 && filterUid != UserHandle.getAppId(uid)) { + continue; + } + final int cmd = mEventCmds[index] & EVENT_CMD_MASK; + if (cmd == EVENT_NULL) { + continue; + } + final String label; + switch (cmd) { + case EVENT_START_JOB: label = " START"; break; + case EVENT_STOP_JOB: label = " STOP"; break; + case EVENT_START_PERIODIC_JOB: label = "START-P"; break; + case EVENT_STOP_PERIODIC_JOB: label = " STOP-P"; break; + default: label = " ??"; break; + } + pw.print(prefix); + TimeUtils.formatDuration(mEventTimes[index]-now, pw, TimeUtils.HUNDRED_DAY_FIELD_LEN); + pw.print(" "); + pw.print(label); + pw.print(": #"); + UserHandle.formatUid(pw, uid); + pw.print("/"); + pw.print(mEventJobIds[index]); + pw.print(" "); + pw.print(mEventTags[index]); + if (cmd == EVENT_STOP_JOB || cmd == EVENT_STOP_PERIODIC_JOB) { + pw.print(" "); + final String reason = mEventReasons[index]; + if (reason != null) { + pw.print(mEventReasons[index]); + } else { + pw.print(JobParameters.getReasonName((mEventCmds[index] & EVENT_STOP_REASON_MASK) + >> EVENT_STOP_REASON_SHIFT)); + } + } + pw.println(); + } + return true; + } + + public void dumpHistory(ProtoOutputStream proto, long fieldId, int filterUid) { + final int size = mEventIndices.size(); + if (size == 0) { + return; + } + final long token = proto.start(fieldId); + + final long now = sElapsedRealtimeClock.millis(); + for (int i = 0; i < size; i++) { + final int index = mEventIndices.indexOf(i); + final int uid = mEventUids[index]; + if (filterUid != -1 && filterUid != UserHandle.getAppId(uid)) { + continue; + } + final int cmd = mEventCmds[index] & EVENT_CMD_MASK; + if (cmd == EVENT_NULL) { + continue; + } + final long heToken = proto.start(JobPackageHistoryProto.HISTORY_EVENT); + + proto.write(JobPackageHistoryProto.HistoryEvent.EVENT, cmd); + proto.write(JobPackageHistoryProto.HistoryEvent.TIME_SINCE_EVENT_MS, now - mEventTimes[index]); + proto.write(JobPackageHistoryProto.HistoryEvent.UID, uid); + proto.write(JobPackageHistoryProto.HistoryEvent.JOB_ID, mEventJobIds[index]); + proto.write(JobPackageHistoryProto.HistoryEvent.TAG, mEventTags[index]); + if (cmd == EVENT_STOP_JOB || cmd == EVENT_STOP_PERIODIC_JOB) { + proto.write(JobPackageHistoryProto.HistoryEvent.STOP_REASON, + (mEventCmds[index] & EVENT_STOP_REASON_MASK) >> EVENT_STOP_REASON_SHIFT); + } + + proto.end(heToken); + } + + proto.end(token); + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java new file mode 100644 index 000000000000..a633350996cd --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -0,0 +1,3283 @@ +/* + * Copyright (C) 2014 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; + +import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED; +import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER; +import static android.text.format.DateUtils.MINUTE_IN_MILLIS; + +import android.annotation.NonNull; +import android.annotation.UserIdInt; +import android.app.Activity; +import android.app.ActivityManager; +import android.app.ActivityManagerInternal; +import android.app.AppGlobals; +import android.app.IUidObserver; +import android.app.job.IJobScheduler; +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobProtoEnums; +import android.app.job.JobScheduler; +import android.app.job.JobService; +import android.app.job.JobSnapshot; +import android.app.job.JobWorkItem; +import android.app.usage.UsageStatsManager; +import android.app.usage.UsageStatsManagerInternal; +import android.app.usage.UsageStatsManagerInternal.AppIdleStateChangeListener; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.IPackageManager; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.PackageManagerInternal; +import android.content.pm.ParceledListSlice; +import android.content.pm.ServiceInfo; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.BatteryStats; +import android.os.BatteryStatsInternal; +import android.os.Binder; +import android.os.Handler; +import android.os.IThermalService; +import android.os.IThermalStatusListener; +import android.os.Looper; +import android.os.Message; +import android.os.Process; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.os.ServiceManager; +import android.os.ShellCallback; +import android.os.SystemClock; +import android.os.Temperature; +import android.os.UserHandle; +import android.os.UserManagerInternal; +import android.os.WorkSource; +import android.provider.Settings; +import android.text.format.DateUtils; +import android.util.KeyValueListParser; +import android.util.Log; +import android.util.Slog; +import android.util.SparseArray; +import android.util.SparseIntArray; +import android.util.StatsLog; +import android.util.TimeUtils; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.app.IBatteryStats; +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.DumpUtils; +import com.android.internal.util.IndentingPrintWriter; +import com.android.internal.util.Preconditions; +import com.android.server.AppStateTracker; +import com.android.server.DeviceIdleInternal; +import com.android.server.FgThread; +import com.android.server.LocalServices; +import com.android.server.job.JobSchedulerServiceDumpProto.ActiveJob; +import com.android.server.job.JobSchedulerServiceDumpProto.PendingJob; +import com.android.server.job.controllers.BackgroundJobsController; +import com.android.server.job.controllers.BatteryController; +import com.android.server.job.controllers.ConnectivityController; +import com.android.server.job.controllers.ContentObserverController; +import com.android.server.job.controllers.DeviceIdleJobsController; +import com.android.server.job.controllers.IdleController; +import com.android.server.job.controllers.JobStatus; +import com.android.server.job.controllers.QuotaController; +import com.android.server.job.controllers.StateController; +import com.android.server.job.controllers.StorageController; +import com.android.server.job.controllers.TimeController; + +import libcore.util.EmptyArray; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.time.Clock; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * Responsible for taking jobs representing work to be performed by a client app, and determining + * based on the criteria specified when that job should be run against the client application's + * endpoint. + * Implements logic for scheduling, and rescheduling jobs. The JobSchedulerService knows nothing + * about constraints, or the state of active jobs. It receives callbacks from the various + * controllers and completed jobs and operates accordingly. + * + * Note on locking: Any operations that manipulate {@link #mJobs} need to lock on that object. + * Any function with the suffix 'Locked' also needs to lock on {@link #mJobs}. + * @hide + */ +public class JobSchedulerService extends com.android.server.SystemService + implements StateChangedListener, JobCompletedListener { + public static final String TAG = "JobScheduler"; + public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + public static final boolean DEBUG_STANDBY = DEBUG || false; + + /** The maximum number of concurrent jobs we run at one time. */ + static final int MAX_JOB_CONTEXTS_COUNT = 16; + /** Enforce a per-app limit on scheduled jobs? */ + private static final boolean ENFORCE_MAX_JOBS = true; + /** The maximum number of jobs that we allow an unprivileged app to schedule */ + private static final int MAX_JOBS_PER_APP = 100; + + @VisibleForTesting + public static Clock sSystemClock = Clock.systemUTC(); + @VisibleForTesting + public static Clock sUptimeMillisClock = SystemClock.uptimeClock(); + @VisibleForTesting + public static Clock sElapsedRealtimeClock = SystemClock.elapsedRealtimeClock(); + + /** Global local for all job scheduler state. */ + final Object mLock = new Object(); + /** Master list of jobs. */ + final JobStore mJobs; + /** Tracking the standby bucket state of each app */ + final StandbyTracker mStandbyTracker; + /** Tracking amount of time each package runs for. */ + final JobPackageTracker mJobPackageTracker = new JobPackageTracker(); + final JobConcurrencyManager mConcurrencyManager; + + static final int MSG_JOB_EXPIRED = 0; + static final int MSG_CHECK_JOB = 1; + static final int MSG_STOP_JOB = 2; + static final int MSG_CHECK_JOB_GREEDY = 3; + static final int MSG_UID_STATE_CHANGED = 4; + static final int MSG_UID_GONE = 5; + static final int MSG_UID_ACTIVE = 6; + static final int MSG_UID_IDLE = 7; + + /** + * Track Services that have currently active or pending jobs. The index is provided by + * {@link JobStatus#getServiceToken()} + */ + final List<JobServiceContext> mActiveServices = new ArrayList<>(); + + /** List of controllers that will notify this service of updates to jobs. */ + final List<StateController> mControllers; + /** Need direct access to this for testing. */ + private final BatteryController mBatteryController; + /** Need direct access to this for testing. */ + private final StorageController mStorageController; + /** Need directly for sending uid state changes */ + private final DeviceIdleJobsController mDeviceIdleJobsController; + /** Needed to get remaining quota time. */ + private final QuotaController mQuotaController; + + /** Need directly for receiving thermal events */ + private IThermalService mThermalService; + /** Thermal constraint. */ + @GuardedBy("mLock") + private boolean mThermalConstraint = false; + + /** + * Queue of pending jobs. The JobServiceContext class will receive jobs from this list + * when ready to execute them. + */ + final ArrayList<JobStatus> mPendingJobs = new ArrayList<>(); + + int[] mStartedUsers = EmptyArray.INT; + + final JobHandler mHandler; + final JobSchedulerStub mJobSchedulerStub; + + PackageManagerInternal mLocalPM; + ActivityManagerInternal mActivityManagerInternal; + IBatteryStats mBatteryStats; + DeviceIdleInternal mLocalDeviceIdleController; + AppStateTracker mAppStateTracker; + final UsageStatsManagerInternal mUsageStats; + + /** + * Set to true once we are allowed to run third party apps. + */ + boolean mReadyToRock; + + /** + * What we last reported to DeviceIdleController about whether we are active. + */ + boolean mReportedActive; + + /** + * Are we currently in device-wide standby parole? + */ + volatile boolean mInParole; + + /** + * A mapping of which uids are currently in the foreground to their effective priority. + */ + final SparseIntArray mUidPriorityOverride = new SparseIntArray(); + + /** + * Which uids are currently performing backups, so we shouldn't allow their jobs to run. + */ + final SparseIntArray mBackingUpUids = new SparseIntArray(); + + /** + * Named indices into standby bucket arrays, for clarity in referring to + * specific buckets' bookkeeping. + */ + public static final int ACTIVE_INDEX = 0; + public static final int WORKING_INDEX = 1; + public static final int FREQUENT_INDEX = 2; + public static final int RARE_INDEX = 3; + public static final int NEVER_INDEX = 4; + + // -- Pre-allocated temporaries only for use in assignJobsToContextsLocked -- + + private class ConstantsObserver extends ContentObserver { + private ContentResolver mResolver; + + public ConstantsObserver(Handler handler) { + super(handler); + } + + public void start(ContentResolver resolver) { + mResolver = resolver; + mResolver.registerContentObserver(Settings.Global.getUriFor( + Settings.Global.JOB_SCHEDULER_CONSTANTS), false, this); + updateConstants(); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + updateConstants(); + } + + private void updateConstants() { + synchronized (mLock) { + try { + mConstants.updateConstantsLocked(Settings.Global.getString(mResolver, + Settings.Global.JOB_SCHEDULER_CONSTANTS)); + for (int controller = 0; controller < mControllers.size(); controller++) { + final StateController sc = mControllers.get(controller); + sc.onConstantsUpdatedLocked(); + } + } catch (IllegalArgumentException e) { + // Failed to parse the settings string, log this and move on + // with defaults. + Slog.e(TAG, "Bad jobscheduler settings", e); + } + } + } + } + + /** + * Thermal event received from Thermal Service + */ + private final class ThermalStatusListener extends IThermalStatusListener.Stub { + @Override public void onStatusChange(int status) { + // Throttle for Temperature.THROTTLING_SEVERE and above + synchronized (mLock) { + mThermalConstraint = status >= Temperature.THROTTLING_SEVERE; + } + onControllerStateChanged(); + } + } + + static class MaxJobCounts { + private final KeyValueListParser.IntValue mTotal; + private final KeyValueListParser.IntValue mMaxBg; + private final KeyValueListParser.IntValue mMinBg; + + MaxJobCounts(int totalDefault, String totalKey, + int maxBgDefault, String maxBgKey, int minBgDefault, String minBgKey) { + mTotal = new KeyValueListParser.IntValue(totalKey, totalDefault); + mMaxBg = new KeyValueListParser.IntValue(maxBgKey, maxBgDefault); + mMinBg = new KeyValueListParser.IntValue(minBgKey, minBgDefault); + } + + public void parse(KeyValueListParser parser) { + mTotal.parse(parser); + mMaxBg.parse(parser); + mMinBg.parse(parser); + + if (mTotal.getValue() < 1) { + mTotal.setValue(1); + } else if (mTotal.getValue() > MAX_JOB_CONTEXTS_COUNT) { + mTotal.setValue(MAX_JOB_CONTEXTS_COUNT); + } + + if (mMaxBg.getValue() < 1) { + mMaxBg.setValue(1); + } else if (mMaxBg.getValue() > mTotal.getValue()) { + mMaxBg.setValue(mTotal.getValue()); + } + if (mMinBg.getValue() < 0) { + mMinBg.setValue(0); + } else { + if (mMinBg.getValue() > mMaxBg.getValue()) { + mMinBg.setValue(mMaxBg.getValue()); + } + if (mMinBg.getValue() >= mTotal.getValue()) { + mMinBg.setValue(mTotal.getValue() - 1); + } + } + } + + /** Total number of jobs to run simultaneously. */ + public int getMaxTotal() { + return mTotal.getValue(); + } + + /** Max number of BG (== owned by non-TOP apps) jobs to run simultaneously. */ + public int getMaxBg() { + return mMaxBg.getValue(); + } + + /** + * We try to run at least this many BG (== owned by non-TOP apps) jobs, when there are any + * pending, rather than always running the TOTAL number of FG jobs. + */ + public int getMinBg() { + return mMinBg.getValue(); + } + + public void dump(PrintWriter pw, String prefix) { + mTotal.dump(pw, prefix); + mMaxBg.dump(pw, prefix); + mMinBg.dump(pw, prefix); + } + + public void dumpProto(ProtoOutputStream proto, long fieldId) { + final long token = proto.start(fieldId); + mTotal.dumpProto(proto, MaxJobCountsProto.TOTAL_JOBS); + mMaxBg.dumpProto(proto, MaxJobCountsProto.MAX_BG); + mMinBg.dumpProto(proto, MaxJobCountsProto.MIN_BG); + proto.end(token); + } + } + + /** {@link MaxJobCounts} for each memory trim level. */ + static class MaxJobCountsPerMemoryTrimLevel { + public final MaxJobCounts normal; + public final MaxJobCounts moderate; + public final MaxJobCounts low; + public final MaxJobCounts critical; + + MaxJobCountsPerMemoryTrimLevel( + MaxJobCounts normal, + MaxJobCounts moderate, MaxJobCounts low, + MaxJobCounts critical) { + this.normal = normal; + this.moderate = moderate; + this.low = low; + this.critical = critical; + } + + public void dumpProto(ProtoOutputStream proto, long fieldId) { + final long token = proto.start(fieldId); + normal.dumpProto(proto, MaxJobCountsPerMemoryTrimLevelProto.NORMAL); + moderate.dumpProto(proto, MaxJobCountsPerMemoryTrimLevelProto.MODERATE); + low.dumpProto(proto, MaxJobCountsPerMemoryTrimLevelProto.LOW); + critical.dumpProto(proto, MaxJobCountsPerMemoryTrimLevelProto.CRITICAL); + proto.end(token); + } + } + + /** + * All times are in milliseconds. These constants are kept synchronized with the system + * global Settings. Any access to this class or its fields should be done while + * holding the JobSchedulerService.mLock lock. + */ + public static class Constants { + // Key names stored in the settings value. + private static final String KEY_MIN_IDLE_COUNT = "min_idle_count"; + private static final String KEY_MIN_CHARGING_COUNT = "min_charging_count"; + private static final String KEY_MIN_BATTERY_NOT_LOW_COUNT = "min_battery_not_low_count"; + private static final String KEY_MIN_STORAGE_NOT_LOW_COUNT = "min_storage_not_low_count"; + private static final String KEY_MIN_CONNECTIVITY_COUNT = "min_connectivity_count"; + private static final String KEY_MIN_CONTENT_COUNT = "min_content_count"; + private static final String KEY_MIN_READY_JOBS_COUNT = "min_ready_jobs_count"; + private static final String KEY_MIN_READY_NON_ACTIVE_JOBS_COUNT = + "min_ready_non_active_jobs_count"; + private static final String KEY_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = + "max_non_active_job_batch_delay_ms"; + private static final String KEY_HEAVY_USE_FACTOR = "heavy_use_factor"; + private static final String KEY_MODERATE_USE_FACTOR = "moderate_use_factor"; + + // The following values used to be used on P and below. Do not reuse them. + private static final String DEPRECATED_KEY_FG_JOB_COUNT = "fg_job_count"; + private static final String DEPRECATED_KEY_BG_NORMAL_JOB_COUNT = "bg_normal_job_count"; + private static final String DEPRECATED_KEY_BG_MODERATE_JOB_COUNT = "bg_moderate_job_count"; + private static final String DEPRECATED_KEY_BG_LOW_JOB_COUNT = "bg_low_job_count"; + private static final String DEPRECATED_KEY_BG_CRITICAL_JOB_COUNT = "bg_critical_job_count"; + + private static final String KEY_MAX_STANDARD_RESCHEDULE_COUNT + = "max_standard_reschedule_count"; + private static final String KEY_MAX_WORK_RESCHEDULE_COUNT = "max_work_reschedule_count"; + private static final String KEY_MIN_LINEAR_BACKOFF_TIME = "min_linear_backoff_time"; + private static final String KEY_MIN_EXP_BACKOFF_TIME = "min_exp_backoff_time"; + private static final String DEPRECATED_KEY_STANDBY_HEARTBEAT_TIME = + "standby_heartbeat_time"; + private static final String DEPRECATED_KEY_STANDBY_WORKING_BEATS = "standby_working_beats"; + private static final String DEPRECATED_KEY_STANDBY_FREQUENT_BEATS = + "standby_frequent_beats"; + private static final String DEPRECATED_KEY_STANDBY_RARE_BEATS = "standby_rare_beats"; + private static final String KEY_CONN_CONGESTION_DELAY_FRAC = "conn_congestion_delay_frac"; + private static final String KEY_CONN_PREFETCH_RELAX_FRAC = "conn_prefetch_relax_frac"; + private static final String DEPRECATED_KEY_USE_HEARTBEATS = "use_heartbeats"; + + private static final int DEFAULT_MIN_IDLE_COUNT = 1; + private static final int DEFAULT_MIN_CHARGING_COUNT = 1; + private static final int DEFAULT_MIN_BATTERY_NOT_LOW_COUNT = 1; + private static final int DEFAULT_MIN_STORAGE_NOT_LOW_COUNT = 1; + private static final int DEFAULT_MIN_CONNECTIVITY_COUNT = 1; + private static final int DEFAULT_MIN_CONTENT_COUNT = 1; + private static final int DEFAULT_MIN_READY_JOBS_COUNT = 1; + private static final int DEFAULT_MIN_READY_NON_ACTIVE_JOBS_COUNT = 5; + private static final long DEFAULT_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = 31 * MINUTE_IN_MILLIS; + private static final float DEFAULT_HEAVY_USE_FACTOR = .9f; + private static final float DEFAULT_MODERATE_USE_FACTOR = .5f; + private static final int DEFAULT_MAX_STANDARD_RESCHEDULE_COUNT = Integer.MAX_VALUE; + private static final int DEFAULT_MAX_WORK_RESCHEDULE_COUNT = Integer.MAX_VALUE; + private static final long DEFAULT_MIN_LINEAR_BACKOFF_TIME = JobInfo.MIN_BACKOFF_MILLIS; + private static final long DEFAULT_MIN_EXP_BACKOFF_TIME = JobInfo.MIN_BACKOFF_MILLIS; + private static final float DEFAULT_CONN_CONGESTION_DELAY_FRAC = 0.5f; + private static final float DEFAULT_CONN_PREFETCH_RELAX_FRAC = 0.5f; + + /** + * Minimum # of idle jobs that must be ready in order to force the JMS to schedule things + * early. + */ + int MIN_IDLE_COUNT = DEFAULT_MIN_IDLE_COUNT; + /** + * Minimum # of charging jobs that must be ready in order to force the JMS to schedule + * things early. + */ + int MIN_CHARGING_COUNT = DEFAULT_MIN_CHARGING_COUNT; + /** + * Minimum # of "battery not low" jobs that must be ready in order to force the JMS to + * schedule things early. + */ + int MIN_BATTERY_NOT_LOW_COUNT = DEFAULT_MIN_BATTERY_NOT_LOW_COUNT; + /** + * Minimum # of "storage not low" jobs that must be ready in order to force the JMS to + * schedule things early. + */ + int MIN_STORAGE_NOT_LOW_COUNT = DEFAULT_MIN_STORAGE_NOT_LOW_COUNT; + /** + * Minimum # of connectivity jobs that must be ready in order to force the JMS to schedule + * things early. 1 == Run connectivity jobs as soon as ready. + */ + int MIN_CONNECTIVITY_COUNT = DEFAULT_MIN_CONNECTIVITY_COUNT; + /** + * Minimum # of content trigger jobs that must be ready in order to force the JMS to + * schedule things early. + */ + int MIN_CONTENT_COUNT = DEFAULT_MIN_CONTENT_COUNT; + /** + * Minimum # of jobs (with no particular constraints) for which the JMS will be happy + * running some work early. This (and thus the other min counts) is now set to 1, to + * prevent any batching at this level. Since we now do batching through doze, that is + * a much better mechanism. + */ + int MIN_READY_JOBS_COUNT = DEFAULT_MIN_READY_JOBS_COUNT; + + /** + * Minimum # of non-ACTIVE jobs for which the JMS will be happy running some work early. + */ + int MIN_READY_NON_ACTIVE_JOBS_COUNT = DEFAULT_MIN_READY_NON_ACTIVE_JOBS_COUNT; + + /** + * Don't batch a non-ACTIVE job if it's been delayed due to force batching attempts for + * at least this amount of time. + */ + long MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = DEFAULT_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS; + + /** + * This is the job execution factor that is considered to be heavy use of the system. + */ + float HEAVY_USE_FACTOR = DEFAULT_HEAVY_USE_FACTOR; + /** + * This is the job execution factor that is considered to be moderate use of the system. + */ + float MODERATE_USE_FACTOR = DEFAULT_MODERATE_USE_FACTOR; + + // Max job counts for screen on / off, for each memory trim level. + final MaxJobCountsPerMemoryTrimLevel MAX_JOB_COUNTS_SCREEN_ON = + new MaxJobCountsPerMemoryTrimLevel( + new MaxJobCounts( + 8, "max_job_total_on_normal", + 6, "max_job_max_bg_on_normal", + 2, "max_job_min_bg_on_normal"), + new MaxJobCounts( + 8, "max_job_total_on_moderate", + 4, "max_job_max_bg_on_moderate", + 2, "max_job_min_bg_on_moderate"), + new MaxJobCounts( + 5, "max_job_total_on_low", + 1, "max_job_max_bg_on_low", + 1, "max_job_min_bg_on_low"), + new MaxJobCounts( + 5, "max_job_total_on_critical", + 1, "max_job_max_bg_on_critical", + 1, "max_job_min_bg_on_critical")); + + final MaxJobCountsPerMemoryTrimLevel MAX_JOB_COUNTS_SCREEN_OFF = + new MaxJobCountsPerMemoryTrimLevel( + new MaxJobCounts( + 10, "max_job_total_off_normal", + 6, "max_job_max_bg_off_normal", + 2, "max_job_min_bg_off_normal"), + new MaxJobCounts( + 10, "max_job_total_off_moderate", + 4, "max_job_max_bg_off_moderate", + 2, "max_job_min_bg_off_moderate"), + new MaxJobCounts( + 5, "max_job_total_off_low", + 1, "max_job_max_bg_off_low", + 1, "max_job_min_bg_off_low"), + new MaxJobCounts( + 5, "max_job_total_off_critical", + 1, "max_job_max_bg_off_critical", + 1, "max_job_min_bg_off_critical")); + + + /** Wait for this long after screen off before increasing the job concurrency. */ + final KeyValueListParser.IntValue SCREEN_OFF_JOB_CONCURRENCY_INCREASE_DELAY_MS = + new KeyValueListParser.IntValue( + "screen_off_job_concurrency_increase_delay_ms", 30_000); + + /** + * The maximum number of times we allow a job to have itself rescheduled before + * giving up on it, for standard jobs. + */ + int MAX_STANDARD_RESCHEDULE_COUNT = DEFAULT_MAX_STANDARD_RESCHEDULE_COUNT; + /** + * The maximum number of times we allow a job to have itself rescheduled before + * giving up on it, for jobs that are executing work. + */ + int MAX_WORK_RESCHEDULE_COUNT = DEFAULT_MAX_WORK_RESCHEDULE_COUNT; + /** + * The minimum backoff time to allow for linear backoff. + */ + long MIN_LINEAR_BACKOFF_TIME = DEFAULT_MIN_LINEAR_BACKOFF_TIME; + /** + * The minimum backoff time to allow for exponential backoff. + */ + long MIN_EXP_BACKOFF_TIME = DEFAULT_MIN_EXP_BACKOFF_TIME; + + /** + * The fraction of a job's running window that must pass before we + * consider running it when the network is congested. + */ + public float CONN_CONGESTION_DELAY_FRAC = DEFAULT_CONN_CONGESTION_DELAY_FRAC; + /** + * The fraction of a prefetch job's running window that must pass before + * we consider matching it against a metered network. + */ + public float CONN_PREFETCH_RELAX_FRAC = DEFAULT_CONN_PREFETCH_RELAX_FRAC; + + private final KeyValueListParser mParser = new KeyValueListParser(','); + + void updateConstantsLocked(String value) { + try { + mParser.setString(value); + } catch (Exception e) { + // Failed to parse the settings string, log this and move on + // with defaults. + Slog.e(TAG, "Bad jobscheduler settings", e); + } + + MIN_IDLE_COUNT = mParser.getInt(KEY_MIN_IDLE_COUNT, + DEFAULT_MIN_IDLE_COUNT); + MIN_CHARGING_COUNT = mParser.getInt(KEY_MIN_CHARGING_COUNT, + DEFAULT_MIN_CHARGING_COUNT); + MIN_BATTERY_NOT_LOW_COUNT = mParser.getInt(KEY_MIN_BATTERY_NOT_LOW_COUNT, + DEFAULT_MIN_BATTERY_NOT_LOW_COUNT); + MIN_STORAGE_NOT_LOW_COUNT = mParser.getInt(KEY_MIN_STORAGE_NOT_LOW_COUNT, + DEFAULT_MIN_STORAGE_NOT_LOW_COUNT); + MIN_CONNECTIVITY_COUNT = mParser.getInt(KEY_MIN_CONNECTIVITY_COUNT, + DEFAULT_MIN_CONNECTIVITY_COUNT); + MIN_CONTENT_COUNT = mParser.getInt(KEY_MIN_CONTENT_COUNT, + DEFAULT_MIN_CONTENT_COUNT); + MIN_READY_JOBS_COUNT = mParser.getInt(KEY_MIN_READY_JOBS_COUNT, + DEFAULT_MIN_READY_JOBS_COUNT); + MIN_READY_NON_ACTIVE_JOBS_COUNT = mParser.getInt( + KEY_MIN_READY_NON_ACTIVE_JOBS_COUNT, + DEFAULT_MIN_READY_NON_ACTIVE_JOBS_COUNT); + MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = mParser.getLong( + KEY_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS, + DEFAULT_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS); + HEAVY_USE_FACTOR = mParser.getFloat(KEY_HEAVY_USE_FACTOR, + DEFAULT_HEAVY_USE_FACTOR); + MODERATE_USE_FACTOR = mParser.getFloat(KEY_MODERATE_USE_FACTOR, + DEFAULT_MODERATE_USE_FACTOR); + + MAX_JOB_COUNTS_SCREEN_ON.normal.parse(mParser); + MAX_JOB_COUNTS_SCREEN_ON.moderate.parse(mParser); + MAX_JOB_COUNTS_SCREEN_ON.low.parse(mParser); + MAX_JOB_COUNTS_SCREEN_ON.critical.parse(mParser); + + MAX_JOB_COUNTS_SCREEN_OFF.normal.parse(mParser); + MAX_JOB_COUNTS_SCREEN_OFF.moderate.parse(mParser); + MAX_JOB_COUNTS_SCREEN_OFF.low.parse(mParser); + MAX_JOB_COUNTS_SCREEN_OFF.critical.parse(mParser); + + SCREEN_OFF_JOB_CONCURRENCY_INCREASE_DELAY_MS.parse(mParser); + + MAX_STANDARD_RESCHEDULE_COUNT = mParser.getInt(KEY_MAX_STANDARD_RESCHEDULE_COUNT, + DEFAULT_MAX_STANDARD_RESCHEDULE_COUNT); + MAX_WORK_RESCHEDULE_COUNT = mParser.getInt(KEY_MAX_WORK_RESCHEDULE_COUNT, + DEFAULT_MAX_WORK_RESCHEDULE_COUNT); + MIN_LINEAR_BACKOFF_TIME = mParser.getDurationMillis(KEY_MIN_LINEAR_BACKOFF_TIME, + DEFAULT_MIN_LINEAR_BACKOFF_TIME); + MIN_EXP_BACKOFF_TIME = mParser.getDurationMillis(KEY_MIN_EXP_BACKOFF_TIME, + DEFAULT_MIN_EXP_BACKOFF_TIME); + CONN_CONGESTION_DELAY_FRAC = mParser.getFloat(KEY_CONN_CONGESTION_DELAY_FRAC, + DEFAULT_CONN_CONGESTION_DELAY_FRAC); + CONN_PREFETCH_RELAX_FRAC = mParser.getFloat(KEY_CONN_PREFETCH_RELAX_FRAC, + DEFAULT_CONN_PREFETCH_RELAX_FRAC); + } + + void dump(IndentingPrintWriter pw) { + pw.println("Settings:"); + pw.increaseIndent(); + pw.printPair(KEY_MIN_IDLE_COUNT, MIN_IDLE_COUNT).println(); + pw.printPair(KEY_MIN_CHARGING_COUNT, MIN_CHARGING_COUNT).println(); + pw.printPair(KEY_MIN_BATTERY_NOT_LOW_COUNT, MIN_BATTERY_NOT_LOW_COUNT).println(); + pw.printPair(KEY_MIN_STORAGE_NOT_LOW_COUNT, MIN_STORAGE_NOT_LOW_COUNT).println(); + pw.printPair(KEY_MIN_CONNECTIVITY_COUNT, MIN_CONNECTIVITY_COUNT).println(); + pw.printPair(KEY_MIN_CONTENT_COUNT, MIN_CONTENT_COUNT).println(); + pw.printPair(KEY_MIN_READY_JOBS_COUNT, MIN_READY_JOBS_COUNT).println(); + pw.printPair(KEY_MIN_READY_NON_ACTIVE_JOBS_COUNT, + MIN_READY_NON_ACTIVE_JOBS_COUNT).println(); + pw.printPair(KEY_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS, + MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS).println(); + pw.printPair(KEY_HEAVY_USE_FACTOR, HEAVY_USE_FACTOR).println(); + pw.printPair(KEY_MODERATE_USE_FACTOR, MODERATE_USE_FACTOR).println(); + + MAX_JOB_COUNTS_SCREEN_ON.normal.dump(pw, ""); + MAX_JOB_COUNTS_SCREEN_ON.moderate.dump(pw, ""); + MAX_JOB_COUNTS_SCREEN_ON.low.dump(pw, ""); + MAX_JOB_COUNTS_SCREEN_ON.critical.dump(pw, ""); + + MAX_JOB_COUNTS_SCREEN_OFF.normal.dump(pw, ""); + MAX_JOB_COUNTS_SCREEN_OFF.moderate.dump(pw, ""); + MAX_JOB_COUNTS_SCREEN_OFF.low.dump(pw, ""); + MAX_JOB_COUNTS_SCREEN_OFF.critical.dump(pw, ""); + + SCREEN_OFF_JOB_CONCURRENCY_INCREASE_DELAY_MS.dump(pw, ""); + + pw.printPair(KEY_MAX_STANDARD_RESCHEDULE_COUNT, MAX_STANDARD_RESCHEDULE_COUNT).println(); + pw.printPair(KEY_MAX_WORK_RESCHEDULE_COUNT, MAX_WORK_RESCHEDULE_COUNT).println(); + pw.printPair(KEY_MIN_LINEAR_BACKOFF_TIME, MIN_LINEAR_BACKOFF_TIME).println(); + pw.printPair(KEY_MIN_EXP_BACKOFF_TIME, MIN_EXP_BACKOFF_TIME).println(); + pw.printPair(KEY_CONN_CONGESTION_DELAY_FRAC, CONN_CONGESTION_DELAY_FRAC).println(); + pw.printPair(KEY_CONN_PREFETCH_RELAX_FRAC, CONN_PREFETCH_RELAX_FRAC).println(); + + pw.decreaseIndent(); + } + + void dump(ProtoOutputStream proto) { + proto.write(ConstantsProto.MIN_IDLE_COUNT, MIN_IDLE_COUNT); + proto.write(ConstantsProto.MIN_CHARGING_COUNT, MIN_CHARGING_COUNT); + proto.write(ConstantsProto.MIN_BATTERY_NOT_LOW_COUNT, MIN_BATTERY_NOT_LOW_COUNT); + proto.write(ConstantsProto.MIN_STORAGE_NOT_LOW_COUNT, MIN_STORAGE_NOT_LOW_COUNT); + proto.write(ConstantsProto.MIN_CONNECTIVITY_COUNT, MIN_CONNECTIVITY_COUNT); + proto.write(ConstantsProto.MIN_CONTENT_COUNT, MIN_CONTENT_COUNT); + proto.write(ConstantsProto.MIN_READY_JOBS_COUNT, MIN_READY_JOBS_COUNT); + proto.write(ConstantsProto.MIN_READY_NON_ACTIVE_JOBS_COUNT, + MIN_READY_NON_ACTIVE_JOBS_COUNT); + proto.write(ConstantsProto.MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS, + MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS); + proto.write(ConstantsProto.HEAVY_USE_FACTOR, HEAVY_USE_FACTOR); + proto.write(ConstantsProto.MODERATE_USE_FACTOR, MODERATE_USE_FACTOR); + + MAX_JOB_COUNTS_SCREEN_ON.dumpProto(proto, ConstantsProto.MAX_JOB_COUNTS_SCREEN_ON); + MAX_JOB_COUNTS_SCREEN_OFF.dumpProto(proto, ConstantsProto.MAX_JOB_COUNTS_SCREEN_OFF); + + SCREEN_OFF_JOB_CONCURRENCY_INCREASE_DELAY_MS.dumpProto(proto, + ConstantsProto.SCREEN_OFF_JOB_CONCURRENCY_INCREASE_DELAY_MS); + + proto.write(ConstantsProto.MAX_STANDARD_RESCHEDULE_COUNT, MAX_STANDARD_RESCHEDULE_COUNT); + proto.write(ConstantsProto.MAX_WORK_RESCHEDULE_COUNT, MAX_WORK_RESCHEDULE_COUNT); + proto.write(ConstantsProto.MIN_LINEAR_BACKOFF_TIME_MS, MIN_LINEAR_BACKOFF_TIME); + proto.write(ConstantsProto.MIN_EXP_BACKOFF_TIME_MS, MIN_EXP_BACKOFF_TIME); + proto.write(ConstantsProto.CONN_CONGESTION_DELAY_FRAC, CONN_CONGESTION_DELAY_FRAC); + proto.write(ConstantsProto.CONN_PREFETCH_RELAX_FRAC, CONN_PREFETCH_RELAX_FRAC); + } + } + + final Constants mConstants; + final ConstantsObserver mConstantsObserver; + + static final Comparator<JobStatus> mEnqueueTimeComparator = (o1, o2) -> { + if (o1.enqueueTime < o2.enqueueTime) { + return -1; + } + return o1.enqueueTime > o2.enqueueTime ? 1 : 0; + }; + + static <T> void addOrderedItem(ArrayList<T> array, T newItem, Comparator<T> comparator) { + int where = Collections.binarySearch(array, newItem, comparator); + if (where < 0) { + where = ~where; + } + array.add(where, newItem); + } + + /** + * Cleans up outstanding jobs when a package is removed. Even if it's being replaced later we + * still clean up. On reinstall the package will have a new uid. + */ + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (DEBUG) { + Slog.d(TAG, "Receieved: " + action); + } + final String pkgName = getPackageName(intent); + final int pkgUid = intent.getIntExtra(Intent.EXTRA_UID, -1); + + if (Intent.ACTION_PACKAGE_CHANGED.equals(action)) { + // Purge the app's jobs if the whole package was just disabled. When this is + // the case the component name will be a bare package name. + if (pkgName != null && pkgUid != -1) { + final String[] changedComponents = intent.getStringArrayExtra( + Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST); + if (changedComponents != null) { + for (String component : changedComponents) { + if (component.equals(pkgName)) { + if (DEBUG) { + Slog.d(TAG, "Package state change: " + pkgName); + } + try { + final int userId = UserHandle.getUserId(pkgUid); + IPackageManager pm = AppGlobals.getPackageManager(); + final int state = pm.getApplicationEnabledSetting(pkgName, userId); + if (state == COMPONENT_ENABLED_STATE_DISABLED + || state == COMPONENT_ENABLED_STATE_DISABLED_USER) { + if (DEBUG) { + Slog.d(TAG, "Removing jobs for package " + pkgName + + " in user " + userId); + } + cancelJobsForPackageAndUid(pkgName, pkgUid, + "app disabled"); + } + } catch (RemoteException|IllegalArgumentException e) { + /* + * IllegalArgumentException means that the package doesn't exist. + * This arises when PACKAGE_CHANGED broadcast delivery has lagged + * behind outright uninstall, so by the time we try to act it's gone. + * We don't need to act on this PACKAGE_CHANGED when this happens; + * we'll get a PACKAGE_REMOVED later and clean up then. + * + * RemoteException can't actually happen; the package manager is + * running in this same process. + */ + } + break; + } + } + if (DEBUG) { + Slog.d(TAG, "Something in " + pkgName + + " changed. Reevaluating controller states."); + } + synchronized (mLock) { + for (int c = mControllers.size() - 1; c >= 0; --c) { + mControllers.get(c).reevaluateStateLocked(pkgUid); + } + } + } + } else { + Slog.w(TAG, "PACKAGE_CHANGED for " + pkgName + " / uid " + pkgUid); + } + } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) { + // If this is an outright uninstall rather than the first half of an + // app update sequence, cancel the jobs associated with the app. + if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { + int uidRemoved = intent.getIntExtra(Intent.EXTRA_UID, -1); + if (DEBUG) { + Slog.d(TAG, "Removing jobs for uid: " + uidRemoved); + } + cancelJobsForPackageAndUid(pkgName, uidRemoved, "app uninstalled"); + synchronized (mLock) { + for (int c = 0; c < mControllers.size(); ++c) { + mControllers.get(c).onAppRemovedLocked(pkgName, pkgUid); + } + } + } + } else if (Intent.ACTION_USER_REMOVED.equals(action)) { + final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0); + if (DEBUG) { + Slog.d(TAG, "Removing jobs for user: " + userId); + } + cancelJobsForUser(userId); + synchronized (mLock) { + for (int c = 0; c < mControllers.size(); ++c) { + mControllers.get(c).onUserRemovedLocked(userId); + } + } + } else if (Intent.ACTION_QUERY_PACKAGE_RESTART.equals(action)) { + // Has this package scheduled any jobs, such that we will take action + // if it were to be force-stopped? + if (pkgUid != -1) { + List<JobStatus> jobsForUid; + synchronized (mLock) { + jobsForUid = mJobs.getJobsByUid(pkgUid); + } + for (int i = jobsForUid.size() - 1; i >= 0; i--) { + if (jobsForUid.get(i).getSourcePackageName().equals(pkgName)) { + if (DEBUG) { + Slog.d(TAG, "Restart query: package " + pkgName + " at uid " + + pkgUid + " has jobs"); + } + setResultCode(Activity.RESULT_OK); + break; + } + } + } + } else if (Intent.ACTION_PACKAGE_RESTARTED.equals(action)) { + // possible force-stop + if (pkgUid != -1) { + if (DEBUG) { + Slog.d(TAG, "Removing jobs for pkg " + pkgName + " at uid " + pkgUid); + } + cancelJobsForPackageAndUid(pkgName, pkgUid, "app force stopped"); + } + } + } + }; + + private String getPackageName(Intent intent) { + Uri uri = intent.getData(); + String pkg = uri != null ? uri.getSchemeSpecificPart() : null; + return pkg; + } + + final private IUidObserver mUidObserver = new IUidObserver.Stub() { + @Override public void onUidStateChanged(int uid, int procState, long procStateSeq) { + mHandler.obtainMessage(MSG_UID_STATE_CHANGED, uid, procState).sendToTarget(); + } + + @Override public void onUidGone(int uid, boolean disabled) { + mHandler.obtainMessage(MSG_UID_GONE, uid, disabled ? 1 : 0).sendToTarget(); + } + + @Override public void onUidActive(int uid) throws RemoteException { + mHandler.obtainMessage(MSG_UID_ACTIVE, uid, 0).sendToTarget(); + } + + @Override public void onUidIdle(int uid, boolean disabled) { + mHandler.obtainMessage(MSG_UID_IDLE, uid, disabled ? 1 : 0).sendToTarget(); + } + + @Override public void onUidCachedChanged(int uid, boolean cached) { + } + }; + + public Context getTestableContext() { + return getContext(); + } + + public Object getLock() { + return mLock; + } + + public JobStore getJobStore() { + return mJobs; + } + + public Constants getConstants() { + return mConstants; + } + + public boolean isChainedAttributionEnabled() { + return WorkSource.isChainedBatteryAttributionEnabled(getContext()); + } + + @Override + public void onStartUser(int userHandle) { + synchronized (mLock) { + mStartedUsers = ArrayUtils.appendInt(mStartedUsers, userHandle); + } + // Let's kick any outstanding jobs for this user. + mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget(); + } + + @Override + public void onUnlockUser(int userHandle) { + // Let's kick any outstanding jobs for this user. + mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget(); + } + + @Override + public void onStopUser(int userHandle) { + synchronized (mLock) { + mStartedUsers = ArrayUtils.removeInt(mStartedUsers, userHandle); + } + } + + /** + * Return whether an UID is active or idle. + */ + private boolean isUidActive(int uid) { + return mAppStateTracker.isUidActiveSynced(uid); + } + + private final Predicate<Integer> mIsUidActivePredicate = this::isUidActive; + + public int scheduleAsPackage(JobInfo job, JobWorkItem work, int uId, String packageName, + int userId, String tag) { + try { + if (ActivityManager.getService().isAppStartModeDisabled(uId, + job.getService().getPackageName())) { + Slog.w(TAG, "Not scheduling job " + uId + ":" + job.toString() + + " -- package not allowed to start"); + return JobScheduler.RESULT_FAILURE; + } + } catch (RemoteException e) { + } + + synchronized (mLock) { + final JobStatus toCancel = mJobs.getJobByUidAndJobId(uId, job.getId()); + + if (work != null && toCancel != null) { + // Fast path: we are adding work to an existing job, and the JobInfo is not + // changing. We can just directly enqueue this work in to the job. + if (toCancel.getJob().equals(job)) { + + toCancel.enqueueWorkLocked(work); + + // If any of work item is enqueued when the source is in the foreground, + // exempt the entire job. + toCancel.maybeAddForegroundExemption(mIsUidActivePredicate); + + return JobScheduler.RESULT_SUCCESS; + } + } + + JobStatus jobStatus = JobStatus.createFromJobInfo(job, uId, packageName, userId, tag); + + // Give exemption if the source is in the foreground just now. + // Note if it's a sync job, this method is called on the handler so it's not exactly + // the state when requestSync() was called, but that should be fine because of the + // 1 minute foreground grace period. + jobStatus.maybeAddForegroundExemption(mIsUidActivePredicate); + + if (DEBUG) Slog.d(TAG, "SCHEDULE: " + jobStatus.toShortString()); + // Jobs on behalf of others don't apply to the per-app job cap + if (ENFORCE_MAX_JOBS && packageName == null) { + if (mJobs.countJobsForUid(uId) > MAX_JOBS_PER_APP) { + Slog.w(TAG, "Too many jobs for uid " + uId); + throw new IllegalStateException("Apps may not schedule more than " + + MAX_JOBS_PER_APP + " distinct jobs"); + } + } + + // This may throw a SecurityException. + jobStatus.prepareLocked(); + + if (work != null) { + // If work has been supplied, enqueue it into the new job. + jobStatus.enqueueWorkLocked(work); + } + + if (toCancel != null) { + // Implicitly replaces the existing job record with the new instance + cancelJobImplLocked(toCancel, jobStatus, "job rescheduled by app"); + } else { + startTrackingJobLocked(jobStatus, null); + } + StatsLog.write_non_chained(StatsLog.SCHEDULED_JOB_STATE_CHANGED, + uId, null, jobStatus.getBatteryName(), + StatsLog.SCHEDULED_JOB_STATE_CHANGED__STATE__SCHEDULED, + JobProtoEnums.STOP_REASON_CANCELLED, jobStatus.getStandbyBucket(), + jobStatus.getJobId()); + + // If the job is immediately ready to run, then we can just immediately + // put it in the pending list and try to schedule it. This is especially + // important for jobs with a 0 deadline constraint, since they will happen a fair + // amount, we want to handle them as quickly as possible, and semantically we want to + // make sure we have started holding the wake lock for the job before returning to + // the caller. + // If the job is not yet ready to run, there is nothing more to do -- we are + // now just waiting for one of its controllers to change state and schedule + // the job appropriately. + if (isReadyToBeExecutedLocked(jobStatus)) { + // This is a new job, we can just immediately put it on the pending + // list and try to run it. + mJobPackageTracker.notePending(jobStatus); + addOrderedItem(mPendingJobs, jobStatus, mEnqueueTimeComparator); + maybeRunPendingJobsLocked(); + } else { + evaluateControllerStatesLocked(jobStatus); + } + } + return JobScheduler.RESULT_SUCCESS; + } + + public List<JobInfo> getPendingJobs(int uid) { + synchronized (mLock) { + List<JobStatus> jobs = mJobs.getJobsByUid(uid); + ArrayList<JobInfo> outList = new ArrayList<JobInfo>(jobs.size()); + for (int i = jobs.size() - 1; i >= 0; i--) { + JobStatus job = jobs.get(i); + outList.add(job.getJob()); + } + return outList; + } + } + + public JobInfo getPendingJob(int uid, int jobId) { + synchronized (mLock) { + List<JobStatus> jobs = mJobs.getJobsByUid(uid); + for (int i = jobs.size() - 1; i >= 0; i--) { + JobStatus job = jobs.get(i); + if (job.getJobId() == jobId) { + return job.getJob(); + } + } + return null; + } + } + + void cancelJobsForUser(int userHandle) { + synchronized (mLock) { + final List<JobStatus> jobsForUser = mJobs.getJobsByUser(userHandle); + for (int i=0; i<jobsForUser.size(); i++) { + JobStatus toRemove = jobsForUser.get(i); + cancelJobImplLocked(toRemove, null, "user removed"); + } + } + } + + private void cancelJobsForNonExistentUsers() { + UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class); + synchronized (mLock) { + mJobs.removeJobsOfNonUsers(umi.getUserIds()); + } + } + + void cancelJobsForPackageAndUid(String pkgName, int uid, String reason) { + if ("android".equals(pkgName)) { + Slog.wtfStack(TAG, "Can't cancel all jobs for system package"); + return; + } + synchronized (mLock) { + final List<JobStatus> jobsForUid = mJobs.getJobsByUid(uid); + for (int i = jobsForUid.size() - 1; i >= 0; i--) { + final JobStatus job = jobsForUid.get(i); + if (job.getSourcePackageName().equals(pkgName)) { + cancelJobImplLocked(job, null, reason); + } + } + } + } + + /** + * Entry point from client to cancel all jobs originating from their uid. + * This will remove the job from the master list, and cancel the job if it was staged for + * execution or being executed. + * @param uid Uid to check against for removal of a job. + * + */ + public boolean cancelJobsForUid(int uid, String reason) { + if (uid == Process.SYSTEM_UID) { + Slog.wtfStack(TAG, "Can't cancel all jobs for system uid"); + return false; + } + + boolean jobsCanceled = false; + synchronized (mLock) { + final List<JobStatus> jobsForUid = mJobs.getJobsByUid(uid); + for (int i=0; i<jobsForUid.size(); i++) { + JobStatus toRemove = jobsForUid.get(i); + cancelJobImplLocked(toRemove, null, reason); + jobsCanceled = true; + } + } + return jobsCanceled; + } + + /** + * Entry point from client to cancel the job corresponding to the jobId provided. + * This will remove the job from the master list, and cancel the job if it was staged for + * execution or being executed. + * @param uid Uid of the calling client. + * @param jobId Id of the job, provided at schedule-time. + */ + public boolean cancelJob(int uid, int jobId, int callingUid) { + JobStatus toCancel; + synchronized (mLock) { + toCancel = mJobs.getJobByUidAndJobId(uid, jobId); + if (toCancel != null) { + cancelJobImplLocked(toCancel, null, + "cancel() called by app, callingUid=" + callingUid + + " uid=" + uid + " jobId=" + jobId); + } + return (toCancel != null); + } + } + + /** + * Cancel the given job, stopping it if it's currently executing. If {@code incomingJob} + * is null, the cancelled job is removed outright from the system. If + * {@code incomingJob} is non-null, it replaces {@code cancelled} in the store of + * currently scheduled jobs. + */ + private void cancelJobImplLocked(JobStatus cancelled, JobStatus incomingJob, String reason) { + if (DEBUG) Slog.d(TAG, "CANCEL: " + cancelled.toShortString()); + cancelled.unprepareLocked(); + stopTrackingJobLocked(cancelled, incomingJob, true /* writeBack */); + // Remove from pending queue. + if (mPendingJobs.remove(cancelled)) { + mJobPackageTracker.noteNonpending(cancelled); + } + // Cancel if running. + stopJobOnServiceContextLocked(cancelled, JobParameters.REASON_CANCELED, reason); + // If this is a replacement, bring in the new version of the job + if (incomingJob != null) { + if (DEBUG) Slog.i(TAG, "Tracking replacement job " + incomingJob.toShortString()); + startTrackingJobLocked(incomingJob, cancelled); + } + reportActiveLocked(); + } + + void updateUidState(int uid, int procState) { + synchronized (mLock) { + if (procState == ActivityManager.PROCESS_STATE_TOP) { + // Only use this if we are exactly the top app. All others can live + // with just the foreground priority. This means that persistent processes + // can never be the top app priority... that is fine. + mUidPriorityOverride.put(uid, JobInfo.PRIORITY_TOP_APP); + } else if (procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) { + mUidPriorityOverride.put(uid, JobInfo.PRIORITY_FOREGROUND_SERVICE); + } else if (procState <= ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE) { + mUidPriorityOverride.put(uid, JobInfo.PRIORITY_BOUND_FOREGROUND_SERVICE); + } else { + mUidPriorityOverride.delete(uid); + } + } + } + + @Override + public void onDeviceIdleStateChanged(boolean deviceIdle) { + synchronized (mLock) { + if (DEBUG) { + Slog.d(TAG, "Doze state changed: " + deviceIdle); + } + if (deviceIdle) { + // When becoming idle, make sure no jobs are actively running, + // except those using the idle exemption flag. + for (int i=0; i<mActiveServices.size(); i++) { + JobServiceContext jsc = mActiveServices.get(i); + final JobStatus executing = jsc.getRunningJobLocked(); + if (executing != null + && (executing.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) == 0) { + jsc.cancelExecutingJobLocked(JobParameters.REASON_DEVICE_IDLE, + "cancelled due to doze"); + } + } + } else { + // When coming out of idle, allow thing to start back up. + if (mReadyToRock) { + if (mLocalDeviceIdleController != null) { + if (!mReportedActive) { + mReportedActive = true; + mLocalDeviceIdleController.setJobsActive(true); + } + } + mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget(); + } + } + } + } + + void reportActiveLocked() { + // active is true if pending queue contains jobs OR some job is running. + boolean active = mPendingJobs.size() > 0; + if (mPendingJobs.size() <= 0) { + for (int i=0; i<mActiveServices.size(); i++) { + final JobServiceContext jsc = mActiveServices.get(i); + final JobStatus job = jsc.getRunningJobLocked(); + if (job != null + && (job.getJob().getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) == 0 + && !job.dozeWhitelisted + && !job.uidActive) { + // We will report active if we have a job running and it is not an exception + // due to being in the foreground or whitelisted. + active = true; + break; + } + } + } + + if (mReportedActive != active) { + mReportedActive = active; + if (mLocalDeviceIdleController != null) { + mLocalDeviceIdleController.setJobsActive(active); + } + } + } + + void reportAppUsage(String packageName, int userId) { + // This app just transitioned into interactive use or near equivalent, so we should + // take a look at its job state for feedback purposes. + } + + /** + * Initializes the system service. + * <p> + * Subclasses must define a single argument constructor that accepts the context + * and passes it to super. + * </p> + * + * @param context The system server context. + */ + public JobSchedulerService(Context context) { + super(context); + + mLocalPM = LocalServices.getService(PackageManagerInternal.class); + mActivityManagerInternal = Preconditions.checkNotNull( + LocalServices.getService(ActivityManagerInternal.class)); + + mHandler = new JobHandler(context.getMainLooper()); + mConstants = new Constants(); + mConstantsObserver = new ConstantsObserver(mHandler); + mJobSchedulerStub = new JobSchedulerStub(); + + mConcurrencyManager = new JobConcurrencyManager(this); + + // Set up the app standby bucketing tracker + mStandbyTracker = new StandbyTracker(); + mUsageStats = LocalServices.getService(UsageStatsManagerInternal.class); + mUsageStats.addAppIdleStateChangeListener(mStandbyTracker); + + // The job store needs to call back + publishLocalService(JobSchedulerInternal.class, new LocalService()); + + // Initialize the job store and set up any persisted jobs + mJobs = JobStore.initAndGet(this); + + // Create the controllers. + mControllers = new ArrayList<StateController>(); + mControllers.add(new ConnectivityController(this)); + mControllers.add(new TimeController(this)); + mControllers.add(new IdleController(this)); + mBatteryController = new BatteryController(this); + mControllers.add(mBatteryController); + mStorageController = new StorageController(this); + mControllers.add(mStorageController); + mControllers.add(new BackgroundJobsController(this)); + mControllers.add(new ContentObserverController(this)); + mDeviceIdleJobsController = new DeviceIdleJobsController(this); + mControllers.add(mDeviceIdleJobsController); + mQuotaController = new QuotaController(this); + mControllers.add(mQuotaController); + + // If the job store determined that it can't yet reschedule persisted jobs, + // we need to start watching the clock. + if (!mJobs.jobTimesInflatedValid()) { + Slog.w(TAG, "!!! RTC not yet good; tracking time updates for job scheduling"); + context.registerReceiver(mTimeSetReceiver, new IntentFilter(Intent.ACTION_TIME_CHANGED)); + } + } + + private final BroadcastReceiver mTimeSetReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (Intent.ACTION_TIME_CHANGED.equals(intent.getAction())) { + // When we reach clock sanity, recalculate the temporal windows + // of all affected jobs. + if (mJobs.clockNowValidToInflate(sSystemClock.millis())) { + Slog.i(TAG, "RTC now valid; recalculating persisted job windows"); + + // We've done our job now, so stop watching the time. + context.unregisterReceiver(this); + + // And kick off the work to update the affected jobs, using a secondary + // thread instead of chugging away here on the main looper thread. + FgThread.getHandler().post(mJobTimeUpdater); + } + } + } + }; + + private final Runnable mJobTimeUpdater = () -> { + final ArrayList<JobStatus> toRemove = new ArrayList<>(); + final ArrayList<JobStatus> toAdd = new ArrayList<>(); + synchronized (mLock) { + // Note: we intentionally both look up the existing affected jobs and replace them + // with recalculated ones inside the same lock lifetime. + getJobStore().getRtcCorrectedJobsLocked(toAdd, toRemove); + + // Now, at each position [i], we have both the existing JobStatus + // and the one that replaces it. + final int N = toAdd.size(); + for (int i = 0; i < N; i++) { + final JobStatus oldJob = toRemove.get(i); + final JobStatus newJob = toAdd.get(i); + if (DEBUG) { + Slog.v(TAG, " replacing " + oldJob + " with " + newJob); + } + cancelJobImplLocked(oldJob, newJob, "deferred rtc calculation"); + } + } + }; + + @Override + public void onStart() { + publishBinderService(Context.JOB_SCHEDULER_SERVICE, mJobSchedulerStub); + } + + @Override + public void onBootPhase(int phase) { + if (PHASE_SYSTEM_SERVICES_READY == phase) { + mConstantsObserver.start(getContext().getContentResolver()); + for (StateController controller : mControllers) { + controller.onSystemServicesReady(); + } + + mAppStateTracker = Preconditions.checkNotNull( + LocalServices.getService(AppStateTracker.class)); + + // Register br for package removals and user removals. + final IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addAction(Intent.ACTION_PACKAGE_CHANGED); + filter.addAction(Intent.ACTION_PACKAGE_RESTARTED); + filter.addAction(Intent.ACTION_QUERY_PACKAGE_RESTART); + filter.addDataScheme("package"); + getContext().registerReceiverAsUser( + mBroadcastReceiver, UserHandle.ALL, filter, null, null); + final IntentFilter userFilter = new IntentFilter(Intent.ACTION_USER_REMOVED); + getContext().registerReceiverAsUser( + mBroadcastReceiver, UserHandle.ALL, userFilter, null, null); + try { + ActivityManager.getService().registerUidObserver(mUidObserver, + ActivityManager.UID_OBSERVER_PROCSTATE | ActivityManager.UID_OBSERVER_GONE + | ActivityManager.UID_OBSERVER_IDLE | ActivityManager.UID_OBSERVER_ACTIVE, + ActivityManager.PROCESS_STATE_UNKNOWN, null); + } catch (RemoteException e) { + // ignored; both services live in system_server + } + + mConcurrencyManager.onSystemReady(); + + // Remove any jobs that are not associated with any of the current users. + cancelJobsForNonExistentUsers(); + // Register thermal callback + mThermalService = IThermalService.Stub.asInterface( + ServiceManager.getService(Context.THERMAL_SERVICE)); + if (mThermalService != null) { + try { + mThermalService.registerThermalStatusListener(new ThermalStatusListener()); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to register thermal callback.", e); + } + } + } else if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) { + synchronized (mLock) { + // Let's go! + mReadyToRock = true; + mBatteryStats = IBatteryStats.Stub.asInterface(ServiceManager.getService( + BatteryStats.SERVICE_NAME)); + mLocalDeviceIdleController = + LocalServices.getService(DeviceIdleInternal.class); + // Create the "runners". + for (int i = 0; i < MAX_JOB_CONTEXTS_COUNT; i++) { + mActiveServices.add( + new JobServiceContext(this, mBatteryStats, mJobPackageTracker, + getContext().getMainLooper())); + } + // Attach jobs to their controllers. + mJobs.forEachJob((job) -> { + for (int controller = 0; controller < mControllers.size(); controller++) { + final StateController sc = mControllers.get(controller); + sc.maybeStartTrackingJobLocked(job, null); + } + }); + // GO GO GO! + mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget(); + } + } + } + + /** + * Called when we have a job status object that we need to insert in our + * {@link com.android.server.job.JobStore}, and make sure all the relevant controllers know + * about. + */ + private void startTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) { + if (!jobStatus.isPreparedLocked()) { + Slog.wtf(TAG, "Not yet prepared when started tracking: " + jobStatus); + } + jobStatus.enqueueTime = sElapsedRealtimeClock.millis(); + final boolean update = mJobs.add(jobStatus); + if (mReadyToRock) { + for (int i = 0; i < mControllers.size(); i++) { + StateController controller = mControllers.get(i); + if (update) { + controller.maybeStopTrackingJobLocked(jobStatus, null, true); + } + controller.maybeStartTrackingJobLocked(jobStatus, lastJob); + } + } + } + + /** + * Called when we want to remove a JobStatus object that we've finished executing. + * @return true if the job was removed. + */ + private boolean stopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, + boolean removeFromPersisted) { + // Deal with any remaining work items in the old job. + jobStatus.stopTrackingJobLocked(incomingJob); + + // Remove from store as well as controllers. + final boolean removed = mJobs.remove(jobStatus, removeFromPersisted); + if (removed && mReadyToRock) { + for (int i=0; i<mControllers.size(); i++) { + StateController controller = mControllers.get(i); + controller.maybeStopTrackingJobLocked(jobStatus, incomingJob, false); + } + } + return removed; + } + + private boolean stopJobOnServiceContextLocked(JobStatus job, int reason, String debugReason) { + for (int i=0; i<mActiveServices.size(); i++) { + JobServiceContext jsc = mActiveServices.get(i); + final JobStatus executing = jsc.getRunningJobLocked(); + if (executing != null && executing.matches(job.getUid(), job.getJobId())) { + jsc.cancelExecutingJobLocked(reason, debugReason); + return true; + } + } + return false; + } + + /** + * @param job JobStatus we are querying against. + * @return Whether or not the job represented by the status object is currently being run or + * is pending. + */ + private boolean isCurrentlyActiveLocked(JobStatus job) { + for (int i=0; i<mActiveServices.size(); i++) { + JobServiceContext serviceContext = mActiveServices.get(i); + final JobStatus running = serviceContext.getRunningJobLocked(); + if (running != null && running.matches(job.getUid(), job.getJobId())) { + return true; + } + } + return false; + } + + void noteJobsPending(List<JobStatus> jobs) { + for (int i = jobs.size() - 1; i >= 0; i--) { + JobStatus job = jobs.get(i); + mJobPackageTracker.notePending(job); + } + } + + void noteJobsNonpending(List<JobStatus> jobs) { + for (int i = jobs.size() - 1; i >= 0; i--) { + JobStatus job = jobs.get(i); + mJobPackageTracker.noteNonpending(job); + } + } + + /** + * Reschedules the given job based on the job's backoff policy. It doesn't make sense to + * specify an override deadline on a failed job (the failed job will run even though it's not + * ready), so we reschedule it with {@link JobStatus#NO_LATEST_RUNTIME}, but specify that any + * ready job with {@link JobStatus#getNumFailures()} > 0 will be executed. + * + * @param failureToReschedule Provided job status that we will reschedule. + * @return A newly instantiated JobStatus with the same constraints as the last job except + * with adjusted timing constraints. + * + * @see #maybeQueueReadyJobsForExecutionLocked + */ + @VisibleForTesting + JobStatus getRescheduleJobForFailureLocked(JobStatus failureToReschedule) { + final long elapsedNowMillis = sElapsedRealtimeClock.millis(); + final JobInfo job = failureToReschedule.getJob(); + + final long initialBackoffMillis = job.getInitialBackoffMillis(); + final int backoffAttempts = failureToReschedule.getNumFailures() + 1; + long delayMillis; + + if (failureToReschedule.hasWorkLocked()) { + if (backoffAttempts > mConstants.MAX_WORK_RESCHEDULE_COUNT) { + Slog.w(TAG, "Not rescheduling " + failureToReschedule + ": attempt #" + + backoffAttempts + " > work limit " + + mConstants.MAX_STANDARD_RESCHEDULE_COUNT); + return null; + } + } else if (backoffAttempts > mConstants.MAX_STANDARD_RESCHEDULE_COUNT) { + Slog.w(TAG, "Not rescheduling " + failureToReschedule + ": attempt #" + + backoffAttempts + " > std limit " + mConstants.MAX_STANDARD_RESCHEDULE_COUNT); + return null; + } + + switch (job.getBackoffPolicy()) { + case JobInfo.BACKOFF_POLICY_LINEAR: { + long backoff = initialBackoffMillis; + if (backoff < mConstants.MIN_LINEAR_BACKOFF_TIME) { + backoff = mConstants.MIN_LINEAR_BACKOFF_TIME; + } + delayMillis = backoff * backoffAttempts; + } break; + default: + if (DEBUG) { + Slog.v(TAG, "Unrecognised back-off policy, defaulting to exponential."); + } + case JobInfo.BACKOFF_POLICY_EXPONENTIAL: { + long backoff = initialBackoffMillis; + if (backoff < mConstants.MIN_EXP_BACKOFF_TIME) { + backoff = mConstants.MIN_EXP_BACKOFF_TIME; + } + delayMillis = (long) Math.scalb(backoff, backoffAttempts - 1); + } break; + } + delayMillis = + Math.min(delayMillis, JobInfo.MAX_BACKOFF_DELAY_MILLIS); + JobStatus newJob = new JobStatus(failureToReschedule, + elapsedNowMillis + delayMillis, + JobStatus.NO_LATEST_RUNTIME, backoffAttempts, + failureToReschedule.getLastSuccessfulRunTime(), sSystemClock.millis()); + if (job.isPeriodic()) { + newJob.setOriginalLatestRunTimeElapsed( + failureToReschedule.getOriginalLatestRunTimeElapsed()); + } + for (int ic=0; ic<mControllers.size(); ic++) { + StateController controller = mControllers.get(ic); + controller.rescheduleForFailureLocked(newJob, failureToReschedule); + } + return newJob; + } + + /** + * Maximum time buffer in which JobScheduler will try to optimize periodic job scheduling. This + * does not cause a job's period to be larger than requested (eg: if the requested period is + * shorter than this buffer). This is used to put a limit on when JobScheduler will intervene + * and try to optimize scheduling if the current job finished less than this amount of time to + * the start of the next period + */ + private static final long PERIODIC_JOB_WINDOW_BUFFER = 30 * MINUTE_IN_MILLIS; + + /** The maximum period a periodic job can have. Anything higher will be clamped down to this. */ + public static final long MAX_ALLOWED_PERIOD_MS = 365 * 24 * 60 * 60 * 1000L; + + /** + * Called after a periodic has executed so we can reschedule it. We take the last execution + * time of the job to be the time of completion (i.e. the time at which this function is + * called). + * <p>This could be inaccurate b/c the job can run for as long as + * {@link com.android.server.job.JobServiceContext#EXECUTING_TIMESLICE_MILLIS}, but will lead + * to underscheduling at least, rather than if we had taken the last execution time to be the + * start of the execution. + * + * @return A new job representing the execution criteria for this instantiation of the + * recurring job. + */ + @VisibleForTesting + JobStatus getRescheduleJobForPeriodic(JobStatus periodicToReschedule) { + final long elapsedNow = sElapsedRealtimeClock.millis(); + final long newLatestRuntimeElapsed; + // Make sure period is in the interval [min_possible_period, max_possible_period]. + final long period = Math.max(JobInfo.getMinPeriodMillis(), + Math.min(MAX_ALLOWED_PERIOD_MS, periodicToReschedule.getJob().getIntervalMillis())); + // Make sure flex is in the interval [min_possible_flex, period]. + final long flex = Math.max(JobInfo.getMinFlexMillis(), + Math.min(period, periodicToReschedule.getJob().getFlexMillis())); + long rescheduleBuffer = 0; + + long olrte = periodicToReschedule.getOriginalLatestRunTimeElapsed(); + if (olrte < 0 || olrte == JobStatus.NO_LATEST_RUNTIME) { + Slog.wtf(TAG, "Invalid periodic job original latest run time: " + olrte); + olrte = elapsedNow; + } + final long latestRunTimeElapsed = olrte; + + final long diffMs = Math.abs(elapsedNow - latestRunTimeElapsed); + if (elapsedNow > latestRunTimeElapsed) { + // The job ran past its expected run window. Have it count towards the current window + // and schedule a new job for the next window. + if (DEBUG) { + Slog.i(TAG, "Periodic job ran after its intended window."); + } + long numSkippedWindows = (diffMs / period) + 1; // +1 to include original window + if (period != flex && diffMs > Math.min(PERIODIC_JOB_WINDOW_BUFFER, + (period - flex) / 2)) { + if (DEBUG) { + Slog.d(TAG, "Custom flex job ran too close to next window."); + } + // For custom flex periods, if the job was run too close to the next window, + // skip the next window and schedule for the following one. + numSkippedWindows += 1; + } + newLatestRuntimeElapsed = latestRunTimeElapsed + (period * numSkippedWindows); + } else { + newLatestRuntimeElapsed = latestRunTimeElapsed + period; + if (diffMs < PERIODIC_JOB_WINDOW_BUFFER && diffMs < period / 6) { + // Add a little buffer to the start of the next window so the job doesn't run + // too soon after this completed one. + rescheduleBuffer = Math.min(PERIODIC_JOB_WINDOW_BUFFER, period / 6 - diffMs); + } + } + + if (newLatestRuntimeElapsed < elapsedNow) { + Slog.wtf(TAG, "Rescheduling calculated latest runtime in the past: " + + newLatestRuntimeElapsed); + return new JobStatus(periodicToReschedule, + elapsedNow + period - flex, elapsedNow + period, + 0 /* backoffAttempt */, + sSystemClock.millis() /* lastSuccessfulRunTime */, + periodicToReschedule.getLastFailedRunTime()); + } + + final long newEarliestRunTimeElapsed = newLatestRuntimeElapsed + - Math.min(flex, period - rescheduleBuffer); + + if (DEBUG) { + Slog.v(TAG, "Rescheduling executed periodic. New execution window [" + + newEarliestRunTimeElapsed / 1000 + ", " + newLatestRuntimeElapsed / 1000 + + "]s"); + } + return new JobStatus(periodicToReschedule, + newEarliestRunTimeElapsed, newLatestRuntimeElapsed, + 0 /* backoffAttempt */, + sSystemClock.millis() /* lastSuccessfulRunTime */, + periodicToReschedule.getLastFailedRunTime()); + } + + // JobCompletedListener implementations. + + /** + * A job just finished executing. We fetch the + * {@link com.android.server.job.controllers.JobStatus} from the store and depending on + * whether we want to reschedule we re-add it to the controllers. + * @param jobStatus Completed job. + * @param needsReschedule Whether the implementing class should reschedule this job. + */ + @Override + public void onJobCompletedLocked(JobStatus jobStatus, boolean needsReschedule) { + if (DEBUG) { + Slog.d(TAG, "Completed " + jobStatus + ", reschedule=" + needsReschedule); + } + + // If the job wants to be rescheduled, we first need to make the next upcoming + // job so we can transfer any appropriate state over from the previous job when + // we stop it. + final JobStatus rescheduledJob = needsReschedule + ? getRescheduleJobForFailureLocked(jobStatus) : null; + + // Do not write back immediately if this is a periodic job. The job may get lost if system + // shuts down before it is added back. + if (!stopTrackingJobLocked(jobStatus, rescheduledJob, !jobStatus.getJob().isPeriodic())) { + if (DEBUG) { + Slog.d(TAG, "Could not find job to remove. Was job removed while executing?"); + } + // We still want to check for jobs to execute, because this job may have + // scheduled a new job under the same job id, and now we can run it. + mHandler.obtainMessage(MSG_CHECK_JOB_GREEDY).sendToTarget(); + return; + } + + if (rescheduledJob != null) { + try { + rescheduledJob.prepareLocked(); + } catch (SecurityException e) { + Slog.w(TAG, "Unable to regrant job permissions for " + rescheduledJob); + } + startTrackingJobLocked(rescheduledJob, jobStatus); + } else if (jobStatus.getJob().isPeriodic()) { + JobStatus rescheduledPeriodic = getRescheduleJobForPeriodic(jobStatus); + try { + rescheduledPeriodic.prepareLocked(); + } catch (SecurityException e) { + Slog.w(TAG, "Unable to regrant job permissions for " + rescheduledPeriodic); + } + startTrackingJobLocked(rescheduledPeriodic, jobStatus); + } + jobStatus.unprepareLocked(); + reportActiveLocked(); + mHandler.obtainMessage(MSG_CHECK_JOB_GREEDY).sendToTarget(); + } + + // StateChangedListener implementations. + + /** + * Posts a message to the {@link com.android.server.job.JobSchedulerService.JobHandler} that + * some controller's state has changed, so as to run through the list of jobs and start/stop + * any that are eligible. + */ + @Override + public void onControllerStateChanged() { + mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget(); + } + + @Override + public void onRunJobNow(JobStatus jobStatus) { + mHandler.obtainMessage(MSG_JOB_EXPIRED, jobStatus).sendToTarget(); + } + + final private class JobHandler extends Handler { + + public JobHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message message) { + synchronized (mLock) { + if (!mReadyToRock) { + return; + } + switch (message.what) { + case MSG_JOB_EXPIRED: { + JobStatus runNow = (JobStatus) message.obj; + // runNow can be null, which is a controller's way of indicating that its + // state is such that all ready jobs should be run immediately. + if (runNow != null && isReadyToBeExecutedLocked(runNow)) { + mJobPackageTracker.notePending(runNow); + addOrderedItem(mPendingJobs, runNow, mEnqueueTimeComparator); + } else { + queueReadyJobsForExecutionLocked(); + } + } break; + case MSG_CHECK_JOB: + if (DEBUG) { + Slog.d(TAG, "MSG_CHECK_JOB"); + } + removeMessages(MSG_CHECK_JOB); + if (mReportedActive) { + // if jobs are currently being run, queue all ready jobs for execution. + queueReadyJobsForExecutionLocked(); + } else { + // Check the list of jobs and run some of them if we feel inclined. + maybeQueueReadyJobsForExecutionLocked(); + } + break; + case MSG_CHECK_JOB_GREEDY: + if (DEBUG) { + Slog.d(TAG, "MSG_CHECK_JOB_GREEDY"); + } + queueReadyJobsForExecutionLocked(); + break; + case MSG_STOP_JOB: + cancelJobImplLocked((JobStatus) message.obj, null, + "app no longer allowed to run"); + break; + + case MSG_UID_STATE_CHANGED: { + final int uid = message.arg1; + final int procState = message.arg2; + updateUidState(uid, procState); + break; + } + case MSG_UID_GONE: { + final int uid = message.arg1; + final boolean disabled = message.arg2 != 0; + updateUidState(uid, ActivityManager.PROCESS_STATE_CACHED_EMPTY); + if (disabled) { + cancelJobsForUid(uid, "uid gone"); + } + synchronized (mLock) { + mDeviceIdleJobsController.setUidActiveLocked(uid, false); + } + break; + } + case MSG_UID_ACTIVE: { + final int uid = message.arg1; + synchronized (mLock) { + mDeviceIdleJobsController.setUidActiveLocked(uid, true); + } + break; + } + case MSG_UID_IDLE: { + final int uid = message.arg1; + final boolean disabled = message.arg2 != 0; + if (disabled) { + cancelJobsForUid(uid, "app uid idle"); + } + synchronized (mLock) { + mDeviceIdleJobsController.setUidActiveLocked(uid, false); + } + break; + } + + } + maybeRunPendingJobsLocked(); + // Don't remove JOB_EXPIRED in case one came along while processing the queue. + } + } + } + + private boolean isJobThermalConstrainedLocked(JobStatus job) { + return mThermalConstraint && job.hasConnectivityConstraint() + && (evaluateJobPriorityLocked(job) < JobInfo.PRIORITY_FOREGROUND_APP); + } + + private void stopNonReadyActiveJobsLocked() { + for (int i=0; i<mActiveServices.size(); i++) { + JobServiceContext serviceContext = mActiveServices.get(i); + final JobStatus running = serviceContext.getRunningJobLocked(); + if (running == null) { + continue; + } + if (!running.isReady()) { + serviceContext.cancelExecutingJobLocked( + JobParameters.REASON_CONSTRAINTS_NOT_SATISFIED, + "cancelled due to unsatisfied constraints"); + } else if (isJobThermalConstrainedLocked(running)) { + serviceContext.cancelExecutingJobLocked( + JobParameters.REASON_DEVICE_THERMAL, + "cancelled due to thermal condition"); + } + } + } + + /** + * Run through list of jobs and execute all possible - at least one is expired so we do + * as many as we can. + */ + private void queueReadyJobsForExecutionLocked() { + if (DEBUG) { + Slog.d(TAG, "queuing all ready jobs for execution:"); + } + noteJobsNonpending(mPendingJobs); + mPendingJobs.clear(); + stopNonReadyActiveJobsLocked(); + mJobs.forEachJob(mReadyQueueFunctor); + mReadyQueueFunctor.postProcess(); + + if (DEBUG) { + final int queuedJobs = mPendingJobs.size(); + if (queuedJobs == 0) { + Slog.d(TAG, "No jobs pending."); + } else { + Slog.d(TAG, queuedJobs + " jobs queued."); + } + } + } + + final class ReadyJobQueueFunctor implements Consumer<JobStatus> { + final ArrayList<JobStatus> newReadyJobs = new ArrayList<>(); + + @Override + public void accept(JobStatus job) { + if (isReadyToBeExecutedLocked(job)) { + if (DEBUG) { + Slog.d(TAG, " queued " + job.toShortString()); + } + newReadyJobs.add(job); + } else { + evaluateControllerStatesLocked(job); + } + } + + public void postProcess() { + noteJobsPending(newReadyJobs); + mPendingJobs.addAll(newReadyJobs); + if (mPendingJobs.size() > 1) { + mPendingJobs.sort(mEnqueueTimeComparator); + } + + newReadyJobs.clear(); + } + } + private final ReadyJobQueueFunctor mReadyQueueFunctor = new ReadyJobQueueFunctor(); + + /** + * The state of at least one job has changed. Here is where we could enforce various + * policies on when we want to execute jobs. + */ + final class MaybeReadyJobQueueFunctor implements Consumer<JobStatus> { + int chargingCount; + int batteryNotLowCount; + int storageNotLowCount; + int idleCount; + int backoffCount; + int connectivityCount; + int contentCount; + int forceBatchedCount; + int unbatchedCount; + final List<JobStatus> runnableJobs = new ArrayList<>(); + + public MaybeReadyJobQueueFunctor() { + reset(); + } + + // Functor method invoked for each job via JobStore.forEachJob() + @Override + public void accept(JobStatus job) { + if (isReadyToBeExecutedLocked(job)) { + try { + if (ActivityManager.getService().isAppStartModeDisabled(job.getUid(), + job.getJob().getService().getPackageName())) { + Slog.w(TAG, "Aborting job " + job.getUid() + ":" + + job.getJob().toString() + " -- package not allowed to start"); + mHandler.obtainMessage(MSG_STOP_JOB, job).sendToTarget(); + return; + } + } catch (RemoteException e) { + } + if (mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT > 1 + && job.getEffectiveStandbyBucket() != ACTIVE_INDEX + && (job.getFirstForceBatchedTimeElapsed() == 0 + || sElapsedRealtimeClock.millis() - job.getFirstForceBatchedTimeElapsed() + < mConstants.MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS)) { + // Force batching non-ACTIVE jobs. Don't include them in the other counts. + forceBatchedCount++; + if (job.getFirstForceBatchedTimeElapsed() == 0) { + job.setFirstForceBatchedTimeElapsed(sElapsedRealtimeClock.millis()); + } + } else { + unbatchedCount++; + if (job.getNumFailures() > 0) { + backoffCount++; + } + if (job.hasIdleConstraint()) { + idleCount++; + } + if (job.hasConnectivityConstraint()) { + connectivityCount++; + } + if (job.hasChargingConstraint()) { + chargingCount++; + } + if (job.hasBatteryNotLowConstraint()) { + batteryNotLowCount++; + } + if (job.hasStorageNotLowConstraint()) { + storageNotLowCount++; + } + if (job.hasContentTriggerConstraint()) { + contentCount++; + } + } + runnableJobs.add(job); + } else { + evaluateControllerStatesLocked(job); + } + } + + public void postProcess() { + if (backoffCount > 0 || + idleCount >= mConstants.MIN_IDLE_COUNT || + connectivityCount >= mConstants.MIN_CONNECTIVITY_COUNT || + chargingCount >= mConstants.MIN_CHARGING_COUNT || + batteryNotLowCount >= mConstants.MIN_BATTERY_NOT_LOW_COUNT || + storageNotLowCount >= mConstants.MIN_STORAGE_NOT_LOW_COUNT || + contentCount >= mConstants.MIN_CONTENT_COUNT || + forceBatchedCount >= mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT || + (unbatchedCount > 0 && (unbatchedCount + forceBatchedCount) + >= mConstants.MIN_READY_JOBS_COUNT)) { + if (DEBUG) { + Slog.d(TAG, "maybeQueueReadyJobsForExecutionLocked: Running jobs."); + } + noteJobsPending(runnableJobs); + mPendingJobs.addAll(runnableJobs); + if (mPendingJobs.size() > 1) { + mPendingJobs.sort(mEnqueueTimeComparator); + } + } else { + if (DEBUG) { + Slog.d(TAG, "maybeQueueReadyJobsForExecutionLocked: Not running anything."); + } + } + + // Be ready for next time + reset(); + } + + @VisibleForTesting + void reset() { + chargingCount = 0; + idleCount = 0; + backoffCount = 0; + connectivityCount = 0; + batteryNotLowCount = 0; + storageNotLowCount = 0; + contentCount = 0; + forceBatchedCount = 0; + unbatchedCount = 0; + runnableJobs.clear(); + } + } + private final MaybeReadyJobQueueFunctor mMaybeQueueFunctor = new MaybeReadyJobQueueFunctor(); + + private void maybeQueueReadyJobsForExecutionLocked() { + if (DEBUG) Slog.d(TAG, "Maybe queuing ready jobs..."); + + noteJobsNonpending(mPendingJobs); + mPendingJobs.clear(); + stopNonReadyActiveJobsLocked(); + mJobs.forEachJob(mMaybeQueueFunctor); + mMaybeQueueFunctor.postProcess(); + } + + /** Returns true if both the calling and source users for the job are started. */ + private boolean areUsersStartedLocked(final JobStatus job) { + boolean sourceStarted = ArrayUtils.contains(mStartedUsers, job.getSourceUserId()); + if (job.getUserId() == job.getSourceUserId()) { + return sourceStarted; + } + return sourceStarted && ArrayUtils.contains(mStartedUsers, job.getUserId()); + } + + /** + * Criteria for moving a job into the pending queue: + * - It's ready. + * - It's not pending. + * - It's not already running on a JSC. + * - The user that requested the job is running. + * - The job's standby bucket has come due to be runnable. + * - The component is enabled and runnable. + */ + @VisibleForTesting + boolean isReadyToBeExecutedLocked(JobStatus job) { + final boolean jobReady = job.isReady(); + + if (DEBUG) { + Slog.v(TAG, "isReadyToBeExecutedLocked: " + job.toShortString() + + " ready=" + jobReady); + } + + // This is a condition that is very likely to be false (most jobs that are + // scheduled are sitting there, not ready yet) and very cheap to check (just + // a few conditions on data in JobStatus). + if (!jobReady) { + if (job.getSourcePackageName().equals("android.jobscheduler.cts.jobtestapp")) { + Slog.v(TAG, " NOT READY: " + job); + } + return false; + } + + final boolean jobExists = mJobs.containsJob(job); + + final boolean userStarted = areUsersStartedLocked(job); + + if (DEBUG) { + Slog.v(TAG, "isReadyToBeExecutedLocked: " + job.toShortString() + + " exists=" + jobExists + " userStarted=" + userStarted); + } + + // These are also fairly cheap to check, though they typically will not + // be conditions we fail. + if (!jobExists || !userStarted) { + return false; + } + + if (isJobThermalConstrainedLocked(job)) { + return false; + } + + final boolean jobPending = mPendingJobs.contains(job); + final boolean jobActive = isCurrentlyActiveLocked(job); + + if (DEBUG) { + Slog.v(TAG, "isReadyToBeExecutedLocked: " + job.toShortString() + + " pending=" + jobPending + " active=" + jobActive); + } + + // These can be a little more expensive (especially jobActive, since we need to + // go through the array of all potentially active jobs), so we are doing them + // later... but still before checking with the package manager! + if (jobPending || jobActive) { + return false; + } + + // The expensive check: validate that the defined package+service is + // still present & viable. + return isComponentUsable(job); + } + + private boolean isComponentUsable(@NonNull JobStatus job) { + final ServiceInfo service; + try { + // TODO: cache result until we're notified that something in the package changed. + service = AppGlobals.getPackageManager().getServiceInfo( + job.getServiceComponent(), PackageManager.MATCH_DEBUG_TRIAGED_MISSING, + job.getUserId()); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + + if (service == null) { + if (DEBUG) { + Slog.v(TAG, "isComponentUsable: " + job.toShortString() + + " component not present"); + } + return false; + } + + // Everything else checked out so far, so this is the final yes/no check + final boolean appIsBad = mActivityManagerInternal.isAppBad(service.applicationInfo); + if (DEBUG && appIsBad) { + Slog.i(TAG, "App is bad for " + job.toShortString() + " so not runnable"); + } + return !appIsBad; + } + + @VisibleForTesting + void evaluateControllerStatesLocked(final JobStatus job) { + for (int c = mControllers.size() - 1; c >= 0; --c) { + final StateController sc = mControllers.get(c); + sc.evaluateStateLocked(job); + } + } + + /** + * Returns true if non-job constraint components are in place -- if job.isReady() returns true + * and this method returns true, then the job is ready to be executed. + */ + public boolean areComponentsInPlaceLocked(JobStatus job) { + // This code is very similar to the code in isReadyToBeExecutedLocked --- it uses the same + // conditions. + + final boolean jobExists = mJobs.containsJob(job); + final boolean userStarted = areUsersStartedLocked(job); + + if (DEBUG) { + Slog.v(TAG, "areComponentsInPlaceLocked: " + job.toShortString() + + " exists=" + jobExists + " userStarted=" + userStarted); + } + + // These are also fairly cheap to check, though they typically will not + // be conditions we fail. + if (!jobExists || !userStarted) { + return false; + } + + if (isJobThermalConstrainedLocked(job)) { + return false; + } + + // Job pending/active doesn't affect the readiness of a job. + + // The expensive check: validate that the defined package+service is + // still present & viable. + return isComponentUsable(job); + } + + /** Returns the maximum amount of time this job could run for. */ + public long getMaxJobExecutionTimeMs(JobStatus job) { + synchronized (mLock) { + return Math.min(mQuotaController.getMaxJobExecutionTimeMsLocked(job), + JobServiceContext.EXECUTING_TIMESLICE_MILLIS); + } + } + + /** + * Reconcile jobs in the pending queue against available execution contexts. + * A controller can force a job into the pending queue even if it's already running, but + * here is where we decide whether to actually execute it. + */ + void maybeRunPendingJobsLocked() { + if (DEBUG) { + Slog.d(TAG, "pending queue: " + mPendingJobs.size() + " jobs."); + } + mConcurrencyManager.assignJobsToContextsLocked(); + reportActiveLocked(); + } + + private int adjustJobPriority(int curPriority, JobStatus job) { + if (curPriority < JobInfo.PRIORITY_TOP_APP) { + float factor = mJobPackageTracker.getLoadFactor(job); + if (factor >= mConstants.HEAVY_USE_FACTOR) { + curPriority += JobInfo.PRIORITY_ADJ_ALWAYS_RUNNING; + } else if (factor >= mConstants.MODERATE_USE_FACTOR) { + curPriority += JobInfo.PRIORITY_ADJ_OFTEN_RUNNING; + } + } + return curPriority; + } + + int evaluateJobPriorityLocked(JobStatus job) { + int priority = job.getPriority(); + if (priority >= JobInfo.PRIORITY_BOUND_FOREGROUND_SERVICE) { + return adjustJobPriority(priority, job); + } + int override = mUidPriorityOverride.get(job.getSourceUid(), 0); + if (override != 0) { + return adjustJobPriority(override, job); + } + return adjustJobPriority(priority, job); + } + + final class LocalService implements JobSchedulerInternal { + + /** + * Returns a list of all pending jobs. A running job is not considered pending. Periodic + * jobs are always considered pending. + */ + @Override + public List<JobInfo> getSystemScheduledPendingJobs() { + synchronized (mLock) { + final List<JobInfo> pendingJobs = new ArrayList<JobInfo>(); + mJobs.forEachJob(Process.SYSTEM_UID, (job) -> { + if (job.getJob().isPeriodic() || !isCurrentlyActiveLocked(job)) { + pendingJobs.add(job.getJob()); + } + }); + return pendingJobs; + } + } + + @Override + public void cancelJobsForUid(int uid, String reason) { + JobSchedulerService.this.cancelJobsForUid(uid, reason); + } + + @Override + public void addBackingUpUid(int uid) { + synchronized (mLock) { + // No need to actually do anything here, since for a full backup the + // activity manager will kill the process which will kill the job (and + // cause it to restart, but now it can't run). + mBackingUpUids.put(uid, uid); + } + } + + @Override + public void removeBackingUpUid(int uid) { + synchronized (mLock) { + mBackingUpUids.delete(uid); + // If there are any jobs for this uid, we need to rebuild the pending list + // in case they are now ready to run. + if (mJobs.countJobsForUid(uid) > 0) { + mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget(); + } + } + } + + @Override + public void clearAllBackingUpUids() { + synchronized (mLock) { + if (mBackingUpUids.size() > 0) { + mBackingUpUids.clear(); + mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget(); + } + } + } + + @Override + public void reportAppUsage(String packageName, int userId) { + JobSchedulerService.this.reportAppUsage(packageName, userId); + } + + @Override + public JobStorePersistStats getPersistStats() { + synchronized (mLock) { + return new JobStorePersistStats(mJobs.getPersistStats()); + } + } + } + + /** + * Tracking of app assignments to standby buckets + */ + final class StandbyTracker extends AppIdleStateChangeListener { + + // AppIdleStateChangeListener interface for live updates + + @Override + public void onAppIdleStateChanged(final String packageName, final @UserIdInt int userId, + boolean idle, int bucket, int reason) { + // QuotaController handles this now. + } + + @Override + public void onParoleStateChanged(boolean isParoleOn) { + if (DEBUG_STANDBY) { + Slog.i(TAG, "Global parole state now " + (isParoleOn ? "ON" : "OFF")); + } + mInParole = isParoleOn; + } + + @Override + public void onUserInteractionStarted(String packageName, int userId) { + final int uid = mLocalPM.getPackageUid(packageName, + PackageManager.MATCH_UNINSTALLED_PACKAGES, userId); + if (uid < 0) { + // Quietly ignore; the case is already logged elsewhere + return; + } + + long sinceLast = mUsageStats.getTimeSinceLastJobRun(packageName, userId); + if (sinceLast > 2 * DateUtils.DAY_IN_MILLIS) { + // Too long ago, not worth logging + sinceLast = 0L; + } + final DeferredJobCounter counter = new DeferredJobCounter(); + synchronized (mLock) { + mJobs.forEachJobForSourceUid(uid, counter); + } + if (counter.numDeferred() > 0 || sinceLast > 0) { + BatteryStatsInternal mBatteryStatsInternal = LocalServices.getService + (BatteryStatsInternal.class); + mBatteryStatsInternal.noteJobsDeferred(uid, counter.numDeferred(), sinceLast); + StatsLog.write_non_chained(StatsLog.DEFERRED_JOB_STATS_REPORTED, uid, null, + counter.numDeferred(), sinceLast); + } + } + } + + static class DeferredJobCounter implements Consumer<JobStatus> { + private int mDeferred = 0; + + public int numDeferred() { + return mDeferred; + } + + @Override + public void accept(JobStatus job) { + if (job.getWhenStandbyDeferred() > 0) { + mDeferred++; + } + } + } + + public static int standbyBucketToBucketIndex(int bucket) { + // Normalize AppStandby constants to indices into our bookkeeping + if (bucket == UsageStatsManager.STANDBY_BUCKET_NEVER) return NEVER_INDEX; + else if (bucket > UsageStatsManager.STANDBY_BUCKET_FREQUENT) return RARE_INDEX; + else if (bucket > UsageStatsManager.STANDBY_BUCKET_WORKING_SET) return FREQUENT_INDEX; + else if (bucket > UsageStatsManager.STANDBY_BUCKET_ACTIVE) return WORKING_INDEX; + else return ACTIVE_INDEX; + } + + // Static to support external callers + public static int standbyBucketForPackage(String packageName, int userId, long elapsedNow) { + UsageStatsManagerInternal usageStats = LocalServices.getService( + UsageStatsManagerInternal.class); + int bucket = usageStats != null + ? usageStats.getAppStandbyBucket(packageName, userId, elapsedNow) + : 0; + + bucket = standbyBucketToBucketIndex(bucket); + + if (DEBUG_STANDBY) { + Slog.v(TAG, packageName + "/" + userId + " standby bucket index: " + bucket); + } + return bucket; + } + + /** + * Binder stub trampoline implementation + */ + final class JobSchedulerStub extends IJobScheduler.Stub { + /** Cache determination of whether a given app can persist jobs + * key is uid of the calling app; value is undetermined/true/false + */ + private final SparseArray<Boolean> mPersistCache = new SparseArray<Boolean>(); + + // Enforce that only the app itself (or shared uid participant) can schedule a + // job that runs one of the app's services, as well as verifying that the + // named service properly requires the BIND_JOB_SERVICE permission + private void enforceValidJobRequest(int uid, JobInfo job) { + final IPackageManager pm = AppGlobals.getPackageManager(); + final ComponentName service = job.getService(); + try { + ServiceInfo si = pm.getServiceInfo(service, + PackageManager.MATCH_DIRECT_BOOT_AWARE + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, + UserHandle.getUserId(uid)); + if (si == null) { + throw new IllegalArgumentException("No such service " + service); + } + if (si.applicationInfo.uid != uid) { + throw new IllegalArgumentException("uid " + uid + + " cannot schedule job in " + service.getPackageName()); + } + if (!JobService.PERMISSION_BIND.equals(si.permission)) { + throw new IllegalArgumentException("Scheduled service " + service + + " does not require android.permission.BIND_JOB_SERVICE permission"); + } + } catch (RemoteException e) { + // Can't happen; the Package Manager is in this same process + } + } + + private boolean canPersistJobs(int pid, int uid) { + // If we get this far we're good to go; all we need to do now is check + // whether the app is allowed to persist its scheduled work. + final boolean canPersist; + synchronized (mPersistCache) { + Boolean cached = mPersistCache.get(uid); + if (cached != null) { + canPersist = cached.booleanValue(); + } else { + // Persisting jobs is tantamount to running at boot, so we permit + // it when the app has declared that it uses the RECEIVE_BOOT_COMPLETED + // permission + int result = getContext().checkPermission( + android.Manifest.permission.RECEIVE_BOOT_COMPLETED, pid, uid); + canPersist = (result == PackageManager.PERMISSION_GRANTED); + mPersistCache.put(uid, canPersist); + } + } + return canPersist; + } + + private void validateJobFlags(JobInfo job, int callingUid) { + if ((job.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0) { + getContext().enforceCallingOrSelfPermission( + android.Manifest.permission.CONNECTIVITY_INTERNAL, TAG); + } + if ((job.getFlags() & JobInfo.FLAG_EXEMPT_FROM_APP_STANDBY) != 0) { + if (callingUid != Process.SYSTEM_UID) { + throw new SecurityException("Job has invalid flags"); + } + if (job.isPeriodic()) { + Slog.wtf(TAG, "Periodic jobs mustn't have" + + " FLAG_EXEMPT_FROM_APP_STANDBY. Job=" + job); + } + } + } + + // IJobScheduler implementation + @Override + public int schedule(JobInfo job) throws RemoteException { + if (DEBUG) { + Slog.d(TAG, "Scheduling job: " + job.toString()); + } + final int pid = Binder.getCallingPid(); + final int uid = Binder.getCallingUid(); + final int userId = UserHandle.getUserId(uid); + + enforceValidJobRequest(uid, job); + if (job.isPersisted()) { + if (!canPersistJobs(pid, uid)) { + throw new IllegalArgumentException("Error: requested job be persisted without" + + " holding RECEIVE_BOOT_COMPLETED permission."); + } + } + + validateJobFlags(job, uid); + + long ident = Binder.clearCallingIdentity(); + try { + return JobSchedulerService.this.scheduleAsPackage(job, null, uid, null, userId, + null); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + // IJobScheduler implementation + @Override + public int enqueue(JobInfo job, JobWorkItem work) throws RemoteException { + if (DEBUG) { + Slog.d(TAG, "Enqueueing job: " + job.toString() + " work: " + work); + } + final int uid = Binder.getCallingUid(); + final int userId = UserHandle.getUserId(uid); + + enforceValidJobRequest(uid, job); + if (job.isPersisted()) { + throw new IllegalArgumentException("Can't enqueue work for persisted jobs"); + } + if (work == null) { + throw new NullPointerException("work is null"); + } + + validateJobFlags(job, uid); + + long ident = Binder.clearCallingIdentity(); + try { + return JobSchedulerService.this.scheduleAsPackage(job, work, uid, null, userId, + null); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override + public int scheduleAsPackage(JobInfo job, String packageName, int userId, String tag) + throws RemoteException { + final int callerUid = Binder.getCallingUid(); + if (DEBUG) { + Slog.d(TAG, "Caller uid " + callerUid + " scheduling job: " + job.toString() + + " on behalf of " + packageName + "/"); + } + + if (packageName == null) { + throw new NullPointerException("Must specify a package for scheduleAsPackage()"); + } + + int mayScheduleForOthers = getContext().checkCallingOrSelfPermission( + android.Manifest.permission.UPDATE_DEVICE_STATS); + if (mayScheduleForOthers != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Caller uid " + callerUid + + " not permitted to schedule jobs for other apps"); + } + + validateJobFlags(job, callerUid); + + long ident = Binder.clearCallingIdentity(); + try { + return JobSchedulerService.this.scheduleAsPackage(job, null, callerUid, + packageName, userId, tag); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override + public ParceledListSlice<JobInfo> getAllPendingJobs() throws RemoteException { + final int uid = Binder.getCallingUid(); + + long ident = Binder.clearCallingIdentity(); + try { + return new ParceledListSlice<>(JobSchedulerService.this.getPendingJobs(uid)); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override + public JobInfo getPendingJob(int jobId) throws RemoteException { + final int uid = Binder.getCallingUid(); + + long ident = Binder.clearCallingIdentity(); + try { + return JobSchedulerService.this.getPendingJob(uid, jobId); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override + public void cancelAll() throws RemoteException { + final int uid = Binder.getCallingUid(); + long ident = Binder.clearCallingIdentity(); + try { + JobSchedulerService.this.cancelJobsForUid(uid, + "cancelAll() called by app, callingUid=" + uid); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override + public void cancel(int jobId) throws RemoteException { + final int uid = Binder.getCallingUid(); + + long ident = Binder.clearCallingIdentity(); + try { + JobSchedulerService.this.cancelJob(uid, jobId, uid); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + /** + * "dumpsys" infrastructure + */ + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + if (!DumpUtils.checkDumpAndUsageStatsPermission(getContext(), TAG, pw)) return; + + int filterUid = -1; + boolean proto = false; + if (!ArrayUtils.isEmpty(args)) { + int opti = 0; + while (opti < args.length) { + String arg = args[opti]; + if ("-h".equals(arg)) { + dumpHelp(pw); + return; + } else if ("-a".equals(arg)) { + // Ignore, we always dump all. + } else if ("--proto".equals(arg)) { + proto = true; + } else if (arg.length() > 0 && arg.charAt(0) == '-') { + pw.println("Unknown option: " + arg); + return; + } else { + break; + } + opti++; + } + if (opti < args.length) { + String pkg = args[opti]; + try { + filterUid = getContext().getPackageManager().getPackageUid(pkg, + PackageManager.MATCH_ANY_USER); + } catch (NameNotFoundException ignored) { + pw.println("Invalid package: " + pkg); + return; + } + } + } + + final long identityToken = Binder.clearCallingIdentity(); + try { + if (proto) { + JobSchedulerService.this.dumpInternalProto(fd, filterUid); + } else { + JobSchedulerService.this.dumpInternal(new IndentingPrintWriter(pw, " "), + filterUid); + } + } finally { + Binder.restoreCallingIdentity(identityToken); + } + } + + @Override + public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, + String[] args, ShellCallback callback, ResultReceiver resultReceiver) { + (new JobSchedulerShellCommand(JobSchedulerService.this)).exec( + this, in, out, err, args, callback, resultReceiver); + } + + /** + * <b>For internal system user only!</b> + * Returns a list of all currently-executing jobs. + */ + @Override + public List<JobInfo> getStartedJobs() { + final int uid = Binder.getCallingUid(); + if (uid != Process.SYSTEM_UID) { + throw new SecurityException( + "getStartedJobs() is system internal use only."); + } + + final ArrayList<JobInfo> runningJobs; + + synchronized (mLock) { + runningJobs = new ArrayList<>(mActiveServices.size()); + for (JobServiceContext jsc : mActiveServices) { + final JobStatus job = jsc.getRunningJobLocked(); + if (job != null) { + runningJobs.add(job.getJob()); + } + } + } + + return runningJobs; + } + + /** + * <b>For internal system user only!</b> + * Returns a snapshot of the state of all jobs known to the system. + * + * <p class="note">This is a slow operation, so it should be called sparingly. + */ + @Override + public ParceledListSlice<JobSnapshot> getAllJobSnapshots() { + final int uid = Binder.getCallingUid(); + if (uid != Process.SYSTEM_UID) { + throw new SecurityException( + "getAllJobSnapshots() is system internal use only."); + } + synchronized (mLock) { + final ArrayList<JobSnapshot> snapshots = new ArrayList<>(mJobs.size()); + mJobs.forEachJob((job) -> snapshots.add( + new JobSnapshot(job.getJob(), job.getSatisfiedConstraintFlags(), + isReadyToBeExecutedLocked(job)))); + return new ParceledListSlice<>(snapshots); + } + } + }; + + // Shell command infrastructure: run the given job immediately + int executeRunCommand(String pkgName, int userId, int jobId, boolean force) { + if (DEBUG) { + Slog.v(TAG, "executeRunCommand(): " + pkgName + "/" + userId + + " " + jobId + " f=" + force); + } + + try { + final int uid = AppGlobals.getPackageManager().getPackageUid(pkgName, 0, + userId != UserHandle.USER_ALL ? userId : UserHandle.USER_SYSTEM); + if (uid < 0) { + return JobSchedulerShellCommand.CMD_ERR_NO_PACKAGE; + } + + synchronized (mLock) { + final JobStatus js = mJobs.getJobByUidAndJobId(uid, jobId); + if (js == null) { + return JobSchedulerShellCommand.CMD_ERR_NO_JOB; + } + + js.overrideState = (force) ? JobStatus.OVERRIDE_FULL : JobStatus.OVERRIDE_SOFT; + if (!js.isConstraintsSatisfied()) { + js.overrideState = 0; + return JobSchedulerShellCommand.CMD_ERR_CONSTRAINTS; + } + + queueReadyJobsForExecutionLocked(); + maybeRunPendingJobsLocked(); + } + } catch (RemoteException e) { + // can't happen + } + return 0; + } + + // Shell command infrastructure: immediately timeout currently executing jobs + int executeTimeoutCommand(PrintWriter pw, String pkgName, int userId, + boolean hasJobId, int jobId) { + if (DEBUG) { + Slog.v(TAG, "executeTimeoutCommand(): " + pkgName + "/" + userId + " " + jobId); + } + + synchronized (mLock) { + boolean foundSome = false; + for (int i=0; i<mActiveServices.size(); i++) { + final JobServiceContext jc = mActiveServices.get(i); + final JobStatus js = jc.getRunningJobLocked(); + if (jc.timeoutIfExecutingLocked(pkgName, userId, hasJobId, jobId, "shell")) { + foundSome = true; + pw.print("Timing out: "); + js.printUniqueId(pw); + pw.print(" "); + pw.println(js.getServiceComponent().flattenToShortString()); + } + } + if (!foundSome) { + pw.println("No matching executing jobs found."); + } + } + return 0; + } + + // Shell command infrastructure: cancel a scheduled job + int executeCancelCommand(PrintWriter pw, String pkgName, int userId, + boolean hasJobId, int jobId) { + if (DEBUG) { + Slog.v(TAG, "executeCancelCommand(): " + pkgName + "/" + userId + " " + jobId); + } + + int pkgUid = -1; + try { + IPackageManager pm = AppGlobals.getPackageManager(); + pkgUid = pm.getPackageUid(pkgName, 0, userId); + } catch (RemoteException e) { /* can't happen */ } + + if (pkgUid < 0) { + pw.println("Package " + pkgName + " not found."); + return JobSchedulerShellCommand.CMD_ERR_NO_PACKAGE; + } + + if (!hasJobId) { + pw.println("Canceling all jobs for " + pkgName + " in user " + userId); + if (!cancelJobsForUid(pkgUid, "cancel shell command for package")) { + pw.println("No matching jobs found."); + } + } else { + pw.println("Canceling job " + pkgName + "/#" + jobId + " in user " + userId); + if (!cancelJob(pkgUid, jobId, Process.SHELL_UID)) { + pw.println("No matching job found."); + } + } + + return 0; + } + + void setMonitorBattery(boolean enabled) { + synchronized (mLock) { + if (mBatteryController != null) { + mBatteryController.getTracker().setMonitorBatteryLocked(enabled); + } + } + } + + int getBatterySeq() { + synchronized (mLock) { + return mBatteryController != null ? mBatteryController.getTracker().getSeq() : -1; + } + } + + boolean getBatteryCharging() { + synchronized (mLock) { + return mBatteryController != null + ? mBatteryController.getTracker().isOnStablePower() : false; + } + } + + boolean getBatteryNotLow() { + synchronized (mLock) { + return mBatteryController != null + ? mBatteryController.getTracker().isBatteryNotLow() : false; + } + } + + int getStorageSeq() { + synchronized (mLock) { + return mStorageController != null ? mStorageController.getTracker().getSeq() : -1; + } + } + + boolean getStorageNotLow() { + synchronized (mLock) { + return mStorageController != null + ? mStorageController.getTracker().isStorageNotLow() : false; + } + } + + // Shell command infrastructure + int getJobState(PrintWriter pw, String pkgName, int userId, int jobId) { + try { + final int uid = AppGlobals.getPackageManager().getPackageUid(pkgName, 0, + userId != UserHandle.USER_ALL ? userId : UserHandle.USER_SYSTEM); + if (uid < 0) { + pw.print("unknown("); pw.print(pkgName); pw.println(")"); + return JobSchedulerShellCommand.CMD_ERR_NO_PACKAGE; + } + + synchronized (mLock) { + final JobStatus js = mJobs.getJobByUidAndJobId(uid, jobId); + if (DEBUG) Slog.d(TAG, "get-job-state " + uid + "/" + jobId + ": " + js); + if (js == null) { + pw.print("unknown("); UserHandle.formatUid(pw, uid); + pw.print("/jid"); pw.print(jobId); pw.println(")"); + return JobSchedulerShellCommand.CMD_ERR_NO_JOB; + } + + boolean printed = false; + if (mPendingJobs.contains(js)) { + pw.print("pending"); + printed = true; + } + if (isCurrentlyActiveLocked(js)) { + if (printed) { + pw.print(" "); + } + printed = true; + pw.println("active"); + } + if (!ArrayUtils.contains(mStartedUsers, js.getUserId())) { + if (printed) { + pw.print(" "); + } + printed = true; + pw.println("user-stopped"); + } + if (!ArrayUtils.contains(mStartedUsers, js.getSourceUserId())) { + if (printed) { + pw.print(" "); + } + printed = true; + pw.println("source-user-stopped"); + } + if (mBackingUpUids.indexOfKey(js.getSourceUid()) >= 0) { + if (printed) { + pw.print(" "); + } + printed = true; + pw.println("backing-up"); + } + boolean componentPresent = false; + try { + componentPresent = (AppGlobals.getPackageManager().getServiceInfo( + js.getServiceComponent(), + PackageManager.MATCH_DEBUG_TRIAGED_MISSING, + js.getUserId()) != null); + } catch (RemoteException e) { + } + if (!componentPresent) { + if (printed) { + pw.print(" "); + } + printed = true; + pw.println("no-component"); + } + if (js.isReady()) { + if (printed) { + pw.print(" "); + } + printed = true; + pw.println("ready"); + } + if (!printed) { + pw.print("waiting"); + } + pw.println(); + } + } catch (RemoteException e) { + // can't happen + } + return 0; + } + + void triggerDockState(boolean idleState) { + final Intent dockIntent; + if (idleState) { + dockIntent = new Intent(Intent.ACTION_DOCK_IDLE); + } else { + dockIntent = new Intent(Intent.ACTION_DOCK_ACTIVE); + } + dockIntent.setPackage("android"); + dockIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY | Intent.FLAG_RECEIVER_FOREGROUND); + getContext().sendBroadcastAsUser(dockIntent, UserHandle.ALL); + } + + static void dumpHelp(PrintWriter pw) { + pw.println("Job Scheduler (jobscheduler) dump options:"); + pw.println(" [-h] [package] ..."); + pw.println(" -h: print this help"); + pw.println(" [package] is an optional package name to limit the output to."); + } + + /** Sort jobs by caller UID, then by Job ID. */ + private static void sortJobs(List<JobStatus> jobs) { + Collections.sort(jobs, new Comparator<JobStatus>() { + @Override + public int compare(JobStatus o1, JobStatus o2) { + int uid1 = o1.getUid(); + int uid2 = o2.getUid(); + int id1 = o1.getJobId(); + int id2 = o2.getJobId(); + if (uid1 != uid2) { + return uid1 < uid2 ? -1 : 1; + } + return id1 < id2 ? -1 : (id1 > id2 ? 1 : 0); + } + }); + } + + void dumpInternal(final IndentingPrintWriter pw, int filterUid) { + final int filterUidFinal = UserHandle.getAppId(filterUid); + final long now = sSystemClock.millis(); + final long nowElapsed = sElapsedRealtimeClock.millis(); + final long nowUptime = sUptimeMillisClock.millis(); + + final Predicate<JobStatus> predicate = (js) -> { + return filterUidFinal == -1 || UserHandle.getAppId(js.getUid()) == filterUidFinal + || UserHandle.getAppId(js.getSourceUid()) == filterUidFinal; + }; + synchronized (mLock) { + mConstants.dump(pw); + for (StateController controller : mControllers) { + pw.increaseIndent(); + controller.dumpConstants(pw); + pw.decreaseIndent(); + } + pw.println(); + + pw.print(" In parole?: "); + pw.print(mInParole); + pw.println(); + pw.print(" In thermal throttling?: "); + pw.print(mThermalConstraint); + pw.println(); + pw.println(); + + pw.println("Started users: " + Arrays.toString(mStartedUsers)); + pw.print("Registered "); + pw.print(mJobs.size()); + pw.println(" jobs:"); + if (mJobs.size() > 0) { + final List<JobStatus> jobs = mJobs.mJobSet.getAllJobs(); + sortJobs(jobs); + for (JobStatus job : jobs) { + pw.print(" JOB #"); job.printUniqueId(pw); pw.print(": "); + pw.println(job.toShortStringExceptUniqueId()); + + // Skip printing details if the caller requested a filter + if (!predicate.test(job)) { + continue; + } + + job.dump(pw, " ", true, nowElapsed); + + pw.print(" Ready: "); + pw.print(isReadyToBeExecutedLocked(job)); + pw.print(" (job="); + pw.print(job.isReady()); + pw.print(" user="); + pw.print(areUsersStartedLocked(job)); + pw.print(" !thermal="); + pw.print(!isJobThermalConstrainedLocked(job)); + pw.print(" !pending="); + pw.print(!mPendingJobs.contains(job)); + pw.print(" !active="); + pw.print(!isCurrentlyActiveLocked(job)); + pw.print(" !backingup="); + pw.print(!(mBackingUpUids.indexOfKey(job.getSourceUid()) >= 0)); + pw.print(" comp="); + pw.print(isComponentUsable(job)); + pw.println(")"); + } + } else { + pw.println(" None."); + } + for (int i=0; i<mControllers.size(); i++) { + pw.println(); + pw.println(mControllers.get(i).getClass().getSimpleName() + ":"); + pw.increaseIndent(); + mControllers.get(i).dumpControllerStateLocked(pw, predicate); + pw.decreaseIndent(); + } + pw.println(); + pw.println("Uid priority overrides:"); + for (int i=0; i< mUidPriorityOverride.size(); i++) { + int uid = mUidPriorityOverride.keyAt(i); + if (filterUidFinal == -1 || filterUidFinal == UserHandle.getAppId(uid)) { + pw.print(" "); pw.print(UserHandle.formatUid(uid)); + pw.print(": "); pw.println(mUidPriorityOverride.valueAt(i)); + } + } + if (mBackingUpUids.size() > 0) { + pw.println(); + pw.println("Backing up uids:"); + boolean first = true; + for (int i = 0; i < mBackingUpUids.size(); i++) { + int uid = mBackingUpUids.keyAt(i); + if (filterUidFinal == -1 || filterUidFinal == UserHandle.getAppId(uid)) { + if (first) { + pw.print(" "); + first = false; + } else { + pw.print(", "); + } + pw.print(UserHandle.formatUid(uid)); + } + } + pw.println(); + } + pw.println(); + mJobPackageTracker.dump(pw, "", filterUidFinal); + pw.println(); + if (mJobPackageTracker.dumpHistory(pw, "", filterUidFinal)) { + pw.println(); + } + pw.println("Pending queue:"); + for (int i=0; i<mPendingJobs.size(); i++) { + JobStatus job = mPendingJobs.get(i); + pw.print(" Pending #"); pw.print(i); pw.print(": "); + pw.println(job.toShortString()); + job.dump(pw, " ", false, nowElapsed); + int priority = evaluateJobPriorityLocked(job); + pw.print(" Evaluated priority: "); + pw.println(JobInfo.getPriorityString(priority)); + + pw.print(" Tag: "); pw.println(job.getTag()); + pw.print(" Enq: "); + TimeUtils.formatDuration(job.madePending - nowUptime, pw); + pw.println(); + } + pw.println(); + pw.println("Active jobs:"); + for (int i=0; i<mActiveServices.size(); i++) { + JobServiceContext jsc = mActiveServices.get(i); + pw.print(" Slot #"); pw.print(i); pw.print(": "); + final JobStatus job = jsc.getRunningJobLocked(); + if (job == null) { + if (jsc.mStoppedReason != null) { + pw.print("inactive since "); + TimeUtils.formatDuration(jsc.mStoppedTime, nowElapsed, pw); + pw.print(", stopped because: "); + pw.println(jsc.mStoppedReason); + } else { + pw.println("inactive"); + } + continue; + } else { + pw.println(job.toShortString()); + pw.print(" Running for: "); + TimeUtils.formatDuration(nowElapsed - jsc.getExecutionStartTimeElapsed(), pw); + pw.print(", timeout at: "); + TimeUtils.formatDuration(jsc.getTimeoutElapsed() - nowElapsed, pw); + pw.println(); + job.dump(pw, " ", false, nowElapsed); + int priority = evaluateJobPriorityLocked(jsc.getRunningJobLocked()); + pw.print(" Evaluated priority: "); + pw.println(JobInfo.getPriorityString(priority)); + + pw.print(" Active at "); + TimeUtils.formatDuration(job.madeActive - nowUptime, pw); + pw.print(", pending for "); + TimeUtils.formatDuration(job.madeActive - job.madePending, pw); + pw.println(); + } + } + if (filterUid == -1) { + pw.println(); + pw.print("mReadyToRock="); pw.println(mReadyToRock); + pw.print("mReportedActive="); pw.println(mReportedActive); + } + pw.println(); + + mConcurrencyManager.dumpLocked(pw, now, nowElapsed); + + pw.println(); + pw.print("PersistStats: "); + pw.println(mJobs.getPersistStats()); + } + pw.println(); + } + + void dumpInternalProto(final FileDescriptor fd, int filterUid) { + ProtoOutputStream proto = new ProtoOutputStream(fd); + final int filterUidFinal = UserHandle.getAppId(filterUid); + final long now = sSystemClock.millis(); + final long nowElapsed = sElapsedRealtimeClock.millis(); + final long nowUptime = sUptimeMillisClock.millis(); + final Predicate<JobStatus> predicate = (js) -> { + return filterUidFinal == -1 || UserHandle.getAppId(js.getUid()) == filterUidFinal + || UserHandle.getAppId(js.getSourceUid()) == filterUidFinal; + }; + + synchronized (mLock) { + final long settingsToken = proto.start(JobSchedulerServiceDumpProto.SETTINGS); + mConstants.dump(proto); + for (StateController controller : mControllers) { + controller.dumpConstants(proto); + } + proto.end(settingsToken); + + proto.write(JobSchedulerServiceDumpProto.IN_PAROLE, mInParole); + proto.write(JobSchedulerServiceDumpProto.IN_THERMAL, mThermalConstraint); + + for (int u : mStartedUsers) { + proto.write(JobSchedulerServiceDumpProto.STARTED_USERS, u); + } + if (mJobs.size() > 0) { + final List<JobStatus> jobs = mJobs.mJobSet.getAllJobs(); + sortJobs(jobs); + for (JobStatus job : jobs) { + final long rjToken = proto.start(JobSchedulerServiceDumpProto.REGISTERED_JOBS); + job.writeToShortProto(proto, JobSchedulerServiceDumpProto.RegisteredJob.INFO); + + // Skip printing details if the caller requested a filter + if (!predicate.test(job)) { + continue; + } + + job.dump(proto, JobSchedulerServiceDumpProto.RegisteredJob.DUMP, true, nowElapsed); + + proto.write( + JobSchedulerServiceDumpProto.RegisteredJob.IS_JOB_READY_TO_BE_EXECUTED, + isReadyToBeExecutedLocked(job)); + proto.write(JobSchedulerServiceDumpProto.RegisteredJob.IS_JOB_READY, + job.isReady()); + proto.write(JobSchedulerServiceDumpProto.RegisteredJob.ARE_USERS_STARTED, + areUsersStartedLocked(job)); + proto.write( + JobSchedulerServiceDumpProto.RegisteredJob.IS_JOB_THERMAL_CONSTRAINED, + isJobThermalConstrainedLocked(job)); + proto.write(JobSchedulerServiceDumpProto.RegisteredJob.IS_JOB_PENDING, + mPendingJobs.contains(job)); + proto.write(JobSchedulerServiceDumpProto.RegisteredJob.IS_JOB_CURRENTLY_ACTIVE, + isCurrentlyActiveLocked(job)); + proto.write(JobSchedulerServiceDumpProto.RegisteredJob.IS_UID_BACKING_UP, + mBackingUpUids.indexOfKey(job.getSourceUid()) >= 0); + proto.write(JobSchedulerServiceDumpProto.RegisteredJob.IS_COMPONENT_USABLE, + isComponentUsable(job)); + + proto.end(rjToken); + } + } + for (StateController controller : mControllers) { + controller.dumpControllerStateLocked( + proto, JobSchedulerServiceDumpProto.CONTROLLERS, predicate); + } + for (int i=0; i< mUidPriorityOverride.size(); i++) { + int uid = mUidPriorityOverride.keyAt(i); + if (filterUidFinal == -1 || filterUidFinal == UserHandle.getAppId(uid)) { + long pToken = proto.start(JobSchedulerServiceDumpProto.PRIORITY_OVERRIDES); + proto.write(JobSchedulerServiceDumpProto.PriorityOverride.UID, uid); + proto.write(JobSchedulerServiceDumpProto.PriorityOverride.OVERRIDE_VALUE, + mUidPriorityOverride.valueAt(i)); + proto.end(pToken); + } + } + for (int i = 0; i < mBackingUpUids.size(); i++) { + int uid = mBackingUpUids.keyAt(i); + if (filterUidFinal == -1 || filterUidFinal == UserHandle.getAppId(uid)) { + proto.write(JobSchedulerServiceDumpProto.BACKING_UP_UIDS, uid); + } + } + + mJobPackageTracker.dump(proto, JobSchedulerServiceDumpProto.PACKAGE_TRACKER, + filterUidFinal); + mJobPackageTracker.dumpHistory(proto, JobSchedulerServiceDumpProto.HISTORY, + filterUidFinal); + + for (JobStatus job : mPendingJobs) { + final long pjToken = proto.start(JobSchedulerServiceDumpProto.PENDING_JOBS); + + job.writeToShortProto(proto, PendingJob.INFO); + job.dump(proto, PendingJob.DUMP, false, nowElapsed); + proto.write(PendingJob.EVALUATED_PRIORITY, evaluateJobPriorityLocked(job)); + proto.write(PendingJob.PENDING_DURATION_MS, nowUptime - job.madePending); + + proto.end(pjToken); + } + for (JobServiceContext jsc : mActiveServices) { + final long ajToken = proto.start(JobSchedulerServiceDumpProto.ACTIVE_JOBS); + final JobStatus job = jsc.getRunningJobLocked(); + + if (job == null) { + final long ijToken = proto.start(ActiveJob.INACTIVE); + + proto.write(ActiveJob.InactiveJob.TIME_SINCE_STOPPED_MS, + nowElapsed - jsc.mStoppedTime); + if (jsc.mStoppedReason != null) { + proto.write(ActiveJob.InactiveJob.STOPPED_REASON, + jsc.mStoppedReason); + } + + proto.end(ijToken); + } else { + final long rjToken = proto.start(ActiveJob.RUNNING); + + job.writeToShortProto(proto, ActiveJob.RunningJob.INFO); + + proto.write(ActiveJob.RunningJob.RUNNING_DURATION_MS, + nowElapsed - jsc.getExecutionStartTimeElapsed()); + proto.write(ActiveJob.RunningJob.TIME_UNTIL_TIMEOUT_MS, + jsc.getTimeoutElapsed() - nowElapsed); + + job.dump(proto, ActiveJob.RunningJob.DUMP, false, nowElapsed); + + proto.write(ActiveJob.RunningJob.EVALUATED_PRIORITY, + evaluateJobPriorityLocked(jsc.getRunningJobLocked())); + + proto.write(ActiveJob.RunningJob.TIME_SINCE_MADE_ACTIVE_MS, + nowUptime - job.madeActive); + proto.write(ActiveJob.RunningJob.PENDING_DURATION_MS, + job.madeActive - job.madePending); + + proto.end(rjToken); + } + proto.end(ajToken); + } + if (filterUid == -1) { + proto.write(JobSchedulerServiceDumpProto.IS_READY_TO_ROCK, mReadyToRock); + proto.write(JobSchedulerServiceDumpProto.REPORTED_ACTIVE, mReportedActive); + } + mConcurrencyManager.dumpProtoLocked(proto, + JobSchedulerServiceDumpProto.CONCURRENCY_MANAGER, now, nowElapsed); + + mJobs.getPersistStats().writeToProto(proto, JobSchedulerServiceDumpProto.PERSIST_STATS); + } + + proto.flush(); + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java new file mode 100644 index 000000000000..01d158ba9452 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java @@ -0,0 +1,428 @@ +/* + * 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; + +import android.app.ActivityManager; +import android.app.AppGlobals; +import android.content.pm.IPackageManager; +import android.content.pm.PackageManager; +import android.os.Binder; +import android.os.ShellCommand; +import android.os.UserHandle; + +import java.io.PrintWriter; + +public final class JobSchedulerShellCommand extends ShellCommand { + public static final int CMD_ERR_NO_PACKAGE = -1000; + public static final int CMD_ERR_NO_JOB = -1001; + public static final int CMD_ERR_CONSTRAINTS = -1002; + + JobSchedulerService mInternal; + IPackageManager mPM; + + JobSchedulerShellCommand(JobSchedulerService service) { + mInternal = service; + mPM = AppGlobals.getPackageManager(); + } + + @Override + public int onCommand(String cmd) { + final PrintWriter pw = getOutPrintWriter(); + try { + switch (cmd != null ? cmd : "") { + case "run": + return runJob(pw); + case "timeout": + return timeout(pw); + case "cancel": + return cancelJob(pw); + case "monitor-battery": + return monitorBattery(pw); + case "get-battery-seq": + return getBatterySeq(pw); + case "get-battery-charging": + return getBatteryCharging(pw); + case "get-battery-not-low": + return getBatteryNotLow(pw); + case "get-storage-seq": + return getStorageSeq(pw); + case "get-storage-not-low": + return getStorageNotLow(pw); + case "get-job-state": + return getJobState(pw); + case "heartbeat": + return doHeartbeat(pw); + case "trigger-dock-state": + return triggerDockState(pw); + default: + return handleDefaultCommands(cmd); + } + } catch (Exception e) { + pw.println("Exception: " + e); + } + return -1; + } + + private void checkPermission(String operation) throws Exception { + final int uid = Binder.getCallingUid(); + if (uid == 0) { + // Root can do anything. + return; + } + final int perm = mPM.checkUidPermission( + "android.permission.CHANGE_APP_IDLE_STATE", uid); + if (perm != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Uid " + uid + + " not permitted to " + operation); + } + } + + private boolean printError(int errCode, String pkgName, int userId, int jobId) { + PrintWriter pw; + switch (errCode) { + case CMD_ERR_NO_PACKAGE: + pw = getErrPrintWriter(); + pw.print("Package not found: "); + pw.print(pkgName); + pw.print(" / user "); + pw.println(userId); + return true; + + case CMD_ERR_NO_JOB: + pw = getErrPrintWriter(); + pw.print("Could not find job "); + pw.print(jobId); + pw.print(" in package "); + pw.print(pkgName); + pw.print(" / user "); + pw.println(userId); + return true; + + case CMD_ERR_CONSTRAINTS: + pw = getErrPrintWriter(); + pw.print("Job "); + pw.print(jobId); + pw.print(" in package "); + pw.print(pkgName); + pw.print(" / user "); + pw.print(userId); + pw.println(" has functional constraints but --force not specified"); + return true; + + default: + return false; + } + } + + private int runJob(PrintWriter pw) throws Exception { + checkPermission("force scheduled jobs"); + + boolean force = false; + int userId = UserHandle.USER_SYSTEM; + + String opt; + while ((opt = getNextOption()) != null) { + switch (opt) { + case "-f": + case "--force": + force = true; + break; + + case "-u": + case "--user": + userId = Integer.parseInt(getNextArgRequired()); + break; + + default: + pw.println("Error: unknown option '" + opt + "'"); + return -1; + } + } + + final String pkgName = getNextArgRequired(); + final int jobId = Integer.parseInt(getNextArgRequired()); + + final long ident = Binder.clearCallingIdentity(); + try { + int ret = mInternal.executeRunCommand(pkgName, userId, jobId, force); + if (printError(ret, pkgName, userId, jobId)) { + return ret; + } + + // success! + pw.print("Running job"); + if (force) { + pw.print(" [FORCED]"); + } + pw.println(); + + return ret; + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + private int timeout(PrintWriter pw) throws Exception { + checkPermission("force timeout jobs"); + + int userId = UserHandle.USER_ALL; + + String opt; + while ((opt = getNextOption()) != null) { + switch (opt) { + case "-u": + case "--user": + userId = UserHandle.parseUserArg(getNextArgRequired()); + break; + + default: + pw.println("Error: unknown option '" + opt + "'"); + return -1; + } + } + + if (userId == UserHandle.USER_CURRENT) { + userId = ActivityManager.getCurrentUser(); + } + + final String pkgName = getNextArg(); + final String jobIdStr = getNextArg(); + final int jobId = jobIdStr != null ? Integer.parseInt(jobIdStr) : -1; + + final long ident = Binder.clearCallingIdentity(); + try { + return mInternal.executeTimeoutCommand(pw, pkgName, userId, jobIdStr != null, jobId); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + private int cancelJob(PrintWriter pw) throws Exception { + checkPermission("cancel jobs"); + + int userId = UserHandle.USER_SYSTEM; + + String opt; + while ((opt = getNextOption()) != null) { + switch (opt) { + case "-u": + case "--user": + userId = UserHandle.parseUserArg(getNextArgRequired()); + break; + + default: + pw.println("Error: unknown option '" + opt + "'"); + return -1; + } + } + + if (userId < 0) { + pw.println("Error: must specify a concrete user ID"); + return -1; + } + + final String pkgName = getNextArg(); + final String jobIdStr = getNextArg(); + final int jobId = jobIdStr != null ? Integer.parseInt(jobIdStr) : -1; + + final long ident = Binder.clearCallingIdentity(); + try { + return mInternal.executeCancelCommand(pw, pkgName, userId, jobIdStr != null, jobId); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + private int monitorBattery(PrintWriter pw) throws Exception { + checkPermission("change battery monitoring"); + String opt = getNextArgRequired(); + boolean enabled; + if ("on".equals(opt)) { + enabled = true; + } else if ("off".equals(opt)) { + enabled = false; + } else { + getErrPrintWriter().println("Error: unknown option " + opt); + return 1; + } + final long ident = Binder.clearCallingIdentity(); + try { + mInternal.setMonitorBattery(enabled); + if (enabled) pw.println("Battery monitoring enabled"); + else pw.println("Battery monitoring disabled"); + } finally { + Binder.restoreCallingIdentity(ident); + } + return 0; + } + + private int getBatterySeq(PrintWriter pw) { + int seq = mInternal.getBatterySeq(); + pw.println(seq); + return 0; + } + + private int getBatteryCharging(PrintWriter pw) { + boolean val = mInternal.getBatteryCharging(); + pw.println(val); + return 0; + } + + private int getBatteryNotLow(PrintWriter pw) { + boolean val = mInternal.getBatteryNotLow(); + pw.println(val); + return 0; + } + + private int getStorageSeq(PrintWriter pw) { + int seq = mInternal.getStorageSeq(); + pw.println(seq); + return 0; + } + + private int getStorageNotLow(PrintWriter pw) { + boolean val = mInternal.getStorageNotLow(); + pw.println(val); + return 0; + } + + private int getJobState(PrintWriter pw) throws Exception { + checkPermission("force timeout jobs"); + + int userId = UserHandle.USER_SYSTEM; + + String opt; + while ((opt = getNextOption()) != null) { + switch (opt) { + case "-u": + case "--user": + userId = UserHandle.parseUserArg(getNextArgRequired()); + break; + + default: + pw.println("Error: unknown option '" + opt + "'"); + return -1; + } + } + + if (userId == UserHandle.USER_CURRENT) { + userId = ActivityManager.getCurrentUser(); + } + + final String pkgName = getNextArgRequired(); + final String jobIdStr = getNextArgRequired(); + final int jobId = Integer.parseInt(jobIdStr); + + final long ident = Binder.clearCallingIdentity(); + try { + int ret = mInternal.getJobState(pw, pkgName, userId, jobId); + printError(ret, pkgName, userId, jobId); + return ret; + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + private int doHeartbeat(PrintWriter pw) throws Exception { + checkPermission("manipulate scheduler heartbeat"); + + pw.println("Heartbeat command is no longer supported"); + return -1; + } + + private int triggerDockState(PrintWriter pw) throws Exception { + checkPermission("trigger wireless charging dock state"); + + final String opt = getNextArgRequired(); + boolean idleState; + if ("idle".equals(opt)) { + idleState = true; + } else if ("active".equals(opt)) { + idleState = false; + } else { + getErrPrintWriter().println("Error: unknown option " + opt); + return 1; + } + + final long ident = Binder.clearCallingIdentity(); + try { + mInternal.triggerDockState(idleState); + } finally { + Binder.restoreCallingIdentity(ident); + } + return 0; + } + + @Override + public void onHelp() { + final PrintWriter pw = getOutPrintWriter(); + + pw.println("Job scheduler (jobscheduler) commands:"); + pw.println(" help"); + pw.println(" Print this help text."); + pw.println(" run [-f | --force] [-u | --user USER_ID] PACKAGE JOB_ID"); + pw.println(" Trigger immediate execution of a specific scheduled job."); + pw.println(" Options:"); + pw.println(" -f or --force: run the job even if technical constraints such as"); + pw.println(" connectivity are not currently met"); + pw.println(" -u or --user: specify which user's job is to be run; the default is"); + pw.println(" the primary or system user"); + pw.println(" timeout [-u | --user USER_ID] [PACKAGE] [JOB_ID]"); + pw.println(" Trigger immediate timeout of currently executing jobs, as if their."); + pw.println(" execution timeout had expired."); + pw.println(" Options:"); + pw.println(" -u or --user: specify which user's job is to be run; the default is"); + pw.println(" all users"); + pw.println(" cancel [-u | --user USER_ID] PACKAGE [JOB_ID]"); + pw.println(" Cancel a scheduled job. If a job ID is not supplied, all jobs scheduled"); + pw.println(" by that package will be canceled. USE WITH CAUTION."); + pw.println(" Options:"); + pw.println(" -u or --user: specify which user's job is to be run; the default is"); + pw.println(" the primary or system user"); + pw.println(" heartbeat [num]"); + pw.println(" No longer used."); + pw.println(" monitor-battery [on|off]"); + pw.println(" Control monitoring of all battery changes. Off by default. Turning"); + pw.println(" on makes get-battery-seq useful."); + pw.println(" get-battery-seq"); + pw.println(" Return the last battery update sequence number that was received."); + pw.println(" get-battery-charging"); + pw.println(" Return whether the battery is currently considered to be charging."); + pw.println(" get-battery-not-low"); + pw.println(" Return whether the battery is currently considered to not be low."); + pw.println(" get-storage-seq"); + pw.println(" Return the last storage update sequence number that was received."); + pw.println(" get-storage-not-low"); + pw.println(" Return whether storage is currently considered to not be low."); + pw.println(" get-job-state [-u | --user USER_ID] PACKAGE JOB_ID"); + pw.println(" Return the current state of a job, may be any combination of:"); + pw.println(" pending: currently on the pending list, waiting to be active"); + pw.println(" active: job is actively running"); + pw.println(" user-stopped: job can't run because its user is stopped"); + pw.println(" backing-up: job can't run because app is currently backing up its data"); + pw.println(" no-component: job can't run because its component is not available"); + pw.println(" ready: job is ready to run (all constraints satisfied or bypassed)"); + pw.println(" waiting: if nothing else above is printed, job not ready to run"); + pw.println(" Options:"); + pw.println(" -u or --user: specify which user's job is to be run; the default is"); + pw.println(" the primary or system user"); + pw.println(" trigger-dock-state [idle|active]"); + pw.println(" Trigger wireless charging dock state. Active by default."); + pw.println(); + } + +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java new file mode 100644 index 000000000000..782e6463d845 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java @@ -0,0 +1,852 @@ +/* + * Copyright (C) 2014 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; + +import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; + +import android.app.job.IJobCallback; +import android.app.job.IJobService; +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobWorkItem; +import android.app.usage.UsageStatsManagerInternal; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.net.Uri; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.PowerManager; +import android.os.RemoteException; +import android.os.UserHandle; +import android.os.WorkSource; +import android.util.EventLog; +import android.util.Slog; +import android.util.TimeUtils; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.app.IBatteryStats; +import com.android.server.EventLogTags; +import com.android.server.LocalServices; +import com.android.server.job.controllers.JobStatus; + +/** + * Handles client binding and lifecycle of a job. Jobs execute one at a time on an instance of this + * class. + * + * There are two important interactions into this class from the + * {@link com.android.server.job.JobSchedulerService}. To execute a job and to cancel a job. + * - Execution of a new job is handled by the {@link #mAvailable}. This bit is flipped once when a + * job lands, and again when it is complete. + * - Cancelling is trickier, because there are also interactions from the client. It's possible + * the {@link com.android.server.job.JobServiceContext.JobServiceHandler} tries to process a + * {@link #doCancelLocked} after the client has already finished. This is handled by having + * {@link com.android.server.job.JobServiceContext.JobServiceHandler#handleCancelLocked} check whether + * the context is still valid. + * To mitigate this, we avoid sending duplicate onStopJob() + * calls to the client after they've specified jobFinished(). + */ +public final class JobServiceContext implements ServiceConnection { + private static final boolean DEBUG = JobSchedulerService.DEBUG; + private static final boolean DEBUG_STANDBY = JobSchedulerService.DEBUG_STANDBY; + + private static final String TAG = "JobServiceContext"; + /** Amount of time a job is allowed to execute for before being considered timed-out. */ + public static final long EXECUTING_TIMESLICE_MILLIS = 10 * 60 * 1000; // 10mins. + /** Amount of time the JobScheduler waits for the initial service launch+bind. */ + private static final long OP_BIND_TIMEOUT_MILLIS = 18 * 1000; + /** Amount of time the JobScheduler will wait for a response from an app for a message. */ + private static final long OP_TIMEOUT_MILLIS = 8 * 1000; + + private static final String[] VERB_STRINGS = { + "VERB_BINDING", "VERB_STARTING", "VERB_EXECUTING", "VERB_STOPPING", "VERB_FINISHED" + }; + + // States that a job occupies while interacting with the client. + static final int VERB_BINDING = 0; + static final int VERB_STARTING = 1; + static final int VERB_EXECUTING = 2; + static final int VERB_STOPPING = 3; + static final int VERB_FINISHED = 4; + + // Messages that result from interactions with the client service. + /** System timed out waiting for a response. */ + private static final int MSG_TIMEOUT = 0; + + public static final int NO_PREFERRED_UID = -1; + + private final Handler mCallbackHandler; + /** Make callbacks to {@link JobSchedulerService} to inform on job completion status. */ + private final JobCompletedListener mCompletedListener; + /** Used for service binding, etc. */ + private final Context mContext; + private final Object mLock; + private final IBatteryStats mBatteryStats; + private final JobPackageTracker mJobPackageTracker; + private PowerManager.WakeLock mWakeLock; + + // Execution state. + private JobParameters mParams; + @VisibleForTesting + int mVerb; + private boolean mCancelled; + + /** + * All the information maintained about the job currently being executed. + * + * Any reads (dereferences) not done from the handler thread must be synchronized on + * {@link #mLock}. + * Writes can only be done from the handler thread, or {@link #executeRunnableJob(JobStatus)}. + */ + private JobStatus mRunningJob; + private JobCallback mRunningCallback; + /** Used to store next job to run when current job is to be preempted. */ + private int mPreferredUid; + IJobService service; + + /** + * Whether this context is free. This is set to false at the start of execution, and reset to + * true when execution is complete. + */ + @GuardedBy("mLock") + private boolean mAvailable; + /** Track start time. */ + private long mExecutionStartTimeElapsed; + /** Track when job will timeout. */ + private long mTimeoutElapsed; + + // Debugging: reason this job was last stopped. + public String mStoppedReason; + + // Debugging: time this job was last stopped. + public long mStoppedTime; + + final class JobCallback extends IJobCallback.Stub { + public String mStoppedReason; + public long mStoppedTime; + + @Override + public void acknowledgeStartMessage(int jobId, boolean ongoing) { + doAcknowledgeStartMessage(this, jobId, ongoing); + } + + @Override + public void acknowledgeStopMessage(int jobId, boolean reschedule) { + doAcknowledgeStopMessage(this, jobId, reschedule); + } + + @Override + public JobWorkItem dequeueWork(int jobId) { + return doDequeueWork(this, jobId); + } + + @Override + public boolean completeWork(int jobId, int workId) { + return doCompleteWork(this, jobId, workId); + } + + @Override + public void jobFinished(int jobId, boolean reschedule) { + doJobFinished(this, jobId, reschedule); + } + } + + JobServiceContext(JobSchedulerService service, IBatteryStats batteryStats, + JobPackageTracker tracker, Looper looper) { + this(service.getContext(), service.getLock(), batteryStats, tracker, service, looper); + } + + @VisibleForTesting + JobServiceContext(Context context, Object lock, IBatteryStats batteryStats, + JobPackageTracker tracker, JobCompletedListener completedListener, Looper looper) { + mContext = context; + mLock = lock; + mBatteryStats = batteryStats; + mJobPackageTracker = tracker; + mCallbackHandler = new JobServiceHandler(looper); + mCompletedListener = completedListener; + mAvailable = true; + mVerb = VERB_FINISHED; + mPreferredUid = NO_PREFERRED_UID; + } + + /** + * Give a job to this context for execution. Callers must first check {@link #getRunningJobLocked()} + * and ensure it is null to make sure this is a valid context. + * @param job The status of the job that we are going to run. + * @return True if the job is valid and is running. False if the job cannot be executed. + */ + boolean executeRunnableJob(JobStatus job) { + synchronized (mLock) { + if (!mAvailable) { + Slog.e(TAG, "Starting new runnable but context is unavailable > Error."); + return false; + } + + mPreferredUid = NO_PREFERRED_UID; + + mRunningJob = job; + mRunningCallback = new JobCallback(); + final boolean isDeadlineExpired = + job.hasDeadlineConstraint() && + (job.getLatestRunTimeElapsed() < sElapsedRealtimeClock.millis()); + Uri[] triggeredUris = null; + if (job.changedUris != null) { + triggeredUris = new Uri[job.changedUris.size()]; + job.changedUris.toArray(triggeredUris); + } + String[] triggeredAuthorities = null; + if (job.changedAuthorities != null) { + triggeredAuthorities = new String[job.changedAuthorities.size()]; + job.changedAuthorities.toArray(triggeredAuthorities); + } + final JobInfo ji = job.getJob(); + mParams = new JobParameters(mRunningCallback, job.getJobId(), ji.getExtras(), + ji.getTransientExtras(), ji.getClipData(), ji.getClipGrantFlags(), + isDeadlineExpired, triggeredUris, triggeredAuthorities, job.network); + mExecutionStartTimeElapsed = sElapsedRealtimeClock.millis(); + + final long whenDeferred = job.getWhenStandbyDeferred(); + if (whenDeferred > 0) { + final long deferral = mExecutionStartTimeElapsed - whenDeferred; + EventLog.writeEvent(EventLogTags.JOB_DEFERRED_EXECUTION, deferral); + if (DEBUG_STANDBY) { + StringBuilder sb = new StringBuilder(128); + sb.append("Starting job deferred for standby by "); + TimeUtils.formatDuration(deferral, sb); + sb.append(" ms : "); + sb.append(job.toShortString()); + Slog.v(TAG, sb.toString()); + } + } + + // Once we'e begun executing a job, we by definition no longer care whether + // it was inflated from disk with not-yet-coherent delay/deadline bounds. + job.clearPersistedUtcTimes(); + + mVerb = VERB_BINDING; + scheduleOpTimeOutLocked(); + final Intent intent = new Intent().setComponent(job.getServiceComponent()); + boolean binding = false; + try { + binding = mContext.bindServiceAsUser(intent, this, + Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND + | Context.BIND_NOT_PERCEPTIBLE, + new UserHandle(job.getUserId())); + } catch (SecurityException e) { + // Some permission policy, for example INTERACT_ACROSS_USERS and + // android:singleUser, can result in a SecurityException being thrown from + // bindServiceAsUser(). If this happens, catch it and fail gracefully. + Slog.w(TAG, "Job service " + job.getServiceComponent().getShortClassName() + + " cannot be executed: " + e.getMessage()); + binding = false; + } + if (!binding) { + if (DEBUG) { + Slog.d(TAG, job.getServiceComponent().getShortClassName() + " unavailable."); + } + mRunningJob = null; + mRunningCallback = null; + mParams = null; + mExecutionStartTimeElapsed = 0L; + mVerb = VERB_FINISHED; + removeOpTimeOutLocked(); + return false; + } + mJobPackageTracker.noteActive(job); + try { + mBatteryStats.noteJobStart(job.getBatteryName(), job.getSourceUid(), + job.getStandbyBucket(), job.getJobId()); + } catch (RemoteException e) { + // Whatever. + } + final String jobPackage = job.getSourcePackageName(); + final int jobUserId = job.getSourceUserId(); + UsageStatsManagerInternal usageStats = + LocalServices.getService(UsageStatsManagerInternal.class); + usageStats.setLastJobRunTime(jobPackage, jobUserId, mExecutionStartTimeElapsed); + mAvailable = false; + mStoppedReason = null; + mStoppedTime = 0; + return true; + } + } + + /** + * Used externally to query the running job. Will return null if there is no job running. + */ + JobStatus getRunningJobLocked() { + return mRunningJob; + } + + /** + * Used only for debugging. Will return <code>"<null>"</code> if there is no job running. + */ + private String getRunningJobNameLocked() { + return mRunningJob != null ? mRunningJob.toShortString() : "<null>"; + } + + /** Called externally when a job that was scheduled for execution should be cancelled. */ + @GuardedBy("mLock") + void cancelExecutingJobLocked(int reason, String debugReason) { + doCancelLocked(reason, debugReason); + } + + @GuardedBy("mLock") + void preemptExecutingJobLocked() { + doCancelLocked(JobParameters.REASON_PREEMPT, "cancelled due to preemption"); + } + + int getPreferredUid() { + return mPreferredUid; + } + + void clearPreferredUid() { + mPreferredUid = NO_PREFERRED_UID; + } + + long getExecutionStartTimeElapsed() { + return mExecutionStartTimeElapsed; + } + + long getTimeoutElapsed() { + return mTimeoutElapsed; + } + + @GuardedBy("mLock") + boolean timeoutIfExecutingLocked(String pkgName, int userId, boolean matchJobId, int jobId, + String reason) { + final JobStatus executing = getRunningJobLocked(); + if (executing != null && (userId == UserHandle.USER_ALL || userId == executing.getUserId()) + && (pkgName == null || pkgName.equals(executing.getSourcePackageName())) + && (!matchJobId || jobId == executing.getJobId())) { + if (mVerb == VERB_EXECUTING) { + mParams.setStopReason(JobParameters.REASON_TIMEOUT, reason); + sendStopMessageLocked("force timeout from shell"); + return true; + } + } + return false; + } + + void doJobFinished(JobCallback cb, int jobId, boolean reschedule) { + doCallback(cb, reschedule, "app called jobFinished"); + } + + void doAcknowledgeStopMessage(JobCallback cb, int jobId, boolean reschedule) { + doCallback(cb, reschedule, null); + } + + void doAcknowledgeStartMessage(JobCallback cb, int jobId, boolean ongoing) { + doCallback(cb, ongoing, "finished start"); + } + + JobWorkItem doDequeueWork(JobCallback cb, int jobId) { + final long ident = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + assertCallerLocked(cb); + if (mVerb == VERB_STOPPING || mVerb == VERB_FINISHED) { + // This job is either all done, or on its way out. Either way, it + // should not dispatch any more work. We will pick up any remaining + // work the next time we start the job again. + return null; + } + final JobWorkItem work = mRunningJob.dequeueWorkLocked(); + if (work == null && !mRunningJob.hasExecutingWorkLocked()) { + // This will finish the job. + doCallbackLocked(false, "last work dequeued"); + } + return work; + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + boolean doCompleteWork(JobCallback cb, int jobId, int workId) { + final long ident = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + assertCallerLocked(cb); + return mRunningJob.completeWorkLocked(workId); + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + /** + * We acquire/release a wakelock on onServiceConnected/unbindService. This mirrors the work + * we intend to send to the client - we stop sending work when the service is unbound so until + * then we keep the wakelock. + * @param name The concrete component name of the service that has been connected. + * @param service The IBinder of the Service's communication channel, + */ + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + JobStatus runningJob; + synchronized (mLock) { + // This isn't strictly necessary b/c the JobServiceHandler is running on the main + // looper and at this point we can't get any binder callbacks from the client. Better + // safe than sorry. + runningJob = mRunningJob; + + if (runningJob == null || !name.equals(runningJob.getServiceComponent())) { + closeAndCleanupJobLocked(true /* needsReschedule */, + "connected for different component"); + return; + } + this.service = IJobService.Stub.asInterface(service); + final PowerManager pm = + (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, + runningJob.getTag()); + wl.setWorkSource(deriveWorkSource(runningJob)); + wl.setReferenceCounted(false); + wl.acquire(); + + // We use a new wakelock instance per job. In rare cases there is a race between + // teardown following job completion/cancellation and new job service spin-up + // such that if we simply assign mWakeLock to be the new instance, we orphan + // the currently-live lock instead of cleanly replacing it. Watch for this and + // explicitly fast-forward the release if we're in that situation. + if (mWakeLock != null) { + Slog.w(TAG, "Bound new job " + runningJob + " but live wakelock " + mWakeLock + + " tag=" + mWakeLock.getTag()); + mWakeLock.release(); + } + mWakeLock = wl; + doServiceBoundLocked(); + } + } + + private WorkSource deriveWorkSource(JobStatus runningJob) { + final int jobUid = runningJob.getSourceUid(); + if (WorkSource.isChainedBatteryAttributionEnabled(mContext)) { + WorkSource workSource = new WorkSource(); + workSource.createWorkChain() + .addNode(jobUid, null) + .addNode(android.os.Process.SYSTEM_UID, "JobScheduler"); + return workSource; + } else { + return new WorkSource(jobUid); + } + } + + /** If the client service crashes we reschedule this job and clean up. */ + @Override + public void onServiceDisconnected(ComponentName name) { + synchronized (mLock) { + closeAndCleanupJobLocked(true /* needsReschedule */, "unexpectedly disconnected"); + } + } + + /** + * This class is reused across different clients, and passes itself in as a callback. Check + * whether the client exercising the callback is the client we expect. + * @return True if the binder calling is coming from the client we expect. + */ + private boolean verifyCallerLocked(JobCallback cb) { + if (mRunningCallback != cb) { + if (DEBUG) { + Slog.d(TAG, "Stale callback received, ignoring."); + } + return false; + } + return true; + } + + private void assertCallerLocked(JobCallback cb) { + if (!verifyCallerLocked(cb)) { + StringBuilder sb = new StringBuilder(128); + sb.append("Caller no longer running"); + if (cb.mStoppedReason != null) { + sb.append(", last stopped "); + TimeUtils.formatDuration(sElapsedRealtimeClock.millis() - cb.mStoppedTime, sb); + sb.append(" because: "); + sb.append(cb.mStoppedReason); + } + throw new SecurityException(sb.toString()); + } + } + + /** + * Scheduling of async messages (basically timeouts at this point). + */ + private class JobServiceHandler extends Handler { + JobServiceHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_TIMEOUT: + synchronized (mLock) { + if (message.obj == mRunningCallback) { + handleOpTimeoutLocked(); + } else { + JobCallback jc = (JobCallback)message.obj; + StringBuilder sb = new StringBuilder(128); + sb.append("Ignoring timeout of no longer active job"); + if (jc.mStoppedReason != null) { + sb.append(", stopped "); + TimeUtils.formatDuration(sElapsedRealtimeClock.millis() + - jc.mStoppedTime, sb); + sb.append(" because: "); + sb.append(jc.mStoppedReason); + } + Slog.w(TAG, sb.toString()); + } + } + break; + default: + Slog.e(TAG, "Unrecognised message: " + message); + } + } + } + + @GuardedBy("mLock") + void doServiceBoundLocked() { + removeOpTimeOutLocked(); + handleServiceBoundLocked(); + } + + void doCallback(JobCallback cb, boolean reschedule, String reason) { + final long ident = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + if (!verifyCallerLocked(cb)) { + return; + } + doCallbackLocked(reschedule, reason); + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @GuardedBy("mLock") + void doCallbackLocked(boolean reschedule, String reason) { + if (DEBUG) { + Slog.d(TAG, "doCallback of : " + mRunningJob + + " v:" + VERB_STRINGS[mVerb]); + } + removeOpTimeOutLocked(); + + if (mVerb == VERB_STARTING) { + handleStartedLocked(reschedule); + } else if (mVerb == VERB_EXECUTING || + mVerb == VERB_STOPPING) { + handleFinishedLocked(reschedule, reason); + } else { + if (DEBUG) { + Slog.d(TAG, "Unrecognised callback: " + mRunningJob); + } + } + } + + @GuardedBy("mLock") + void doCancelLocked(int arg1, String debugReason) { + if (mVerb == VERB_FINISHED) { + if (DEBUG) { + Slog.d(TAG, + "Trying to process cancel for torn-down context, ignoring."); + } + return; + } + mParams.setStopReason(arg1, debugReason); + if (arg1 == JobParameters.REASON_PREEMPT) { + mPreferredUid = mRunningJob != null ? mRunningJob.getUid() : + NO_PREFERRED_UID; + } + handleCancelLocked(debugReason); + } + + /** Start the job on the service. */ + @GuardedBy("mLock") + private void handleServiceBoundLocked() { + if (DEBUG) { + Slog.d(TAG, "handleServiceBound for " + getRunningJobNameLocked()); + } + if (mVerb != VERB_BINDING) { + Slog.e(TAG, "Sending onStartJob for a job that isn't pending. " + + VERB_STRINGS[mVerb]); + closeAndCleanupJobLocked(false /* reschedule */, "started job not pending"); + return; + } + if (mCancelled) { + if (DEBUG) { + Slog.d(TAG, "Job cancelled while waiting for bind to complete. " + + mRunningJob); + } + closeAndCleanupJobLocked(true /* reschedule */, "cancelled while waiting for bind"); + return; + } + try { + mVerb = VERB_STARTING; + scheduleOpTimeOutLocked(); + service.startJob(mParams); + } catch (Exception e) { + // We catch 'Exception' because client-app malice or bugs might induce a wide + // range of possible exception-throw outcomes from startJob() and its handling + // of the client's ParcelableBundle extras. + Slog.e(TAG, "Error sending onStart message to '" + + mRunningJob.getServiceComponent().getShortClassName() + "' ", e); + } + } + + /** + * State behaviours. + * VERB_STARTING -> Successful start, change job to VERB_EXECUTING and post timeout. + * _PENDING -> Error + * _EXECUTING -> Error + * _STOPPING -> Error + */ + @GuardedBy("mLock") + private void handleStartedLocked(boolean workOngoing) { + switch (mVerb) { + case VERB_STARTING: + mVerb = VERB_EXECUTING; + if (!workOngoing) { + // Job is finished already so fast-forward to handleFinished. + handleFinishedLocked(false, "onStartJob returned false"); + return; + } + if (mCancelled) { + if (DEBUG) { + Slog.d(TAG, "Job cancelled while waiting for onStartJob to complete."); + } + // Cancelled *while* waiting for acknowledgeStartMessage from client. + handleCancelLocked(null); + return; + } + scheduleOpTimeOutLocked(); + break; + default: + Slog.e(TAG, "Handling started job but job wasn't starting! Was " + + VERB_STRINGS[mVerb] + "."); + return; + } + } + + /** + * VERB_EXECUTING -> Client called jobFinished(), clean up and notify done. + * _STOPPING -> Successful finish, clean up and notify done. + * _STARTING -> Error + * _PENDING -> Error + */ + @GuardedBy("mLock") + private void handleFinishedLocked(boolean reschedule, String reason) { + switch (mVerb) { + case VERB_EXECUTING: + case VERB_STOPPING: + closeAndCleanupJobLocked(reschedule, reason); + break; + default: + Slog.e(TAG, "Got an execution complete message for a job that wasn't being" + + "executed. Was " + VERB_STRINGS[mVerb] + "."); + } + } + + /** + * A job can be in various states when a cancel request comes in: + * VERB_BINDING -> Cancelled before bind completed. Mark as cancelled and wait for + * {@link #onServiceConnected(android.content.ComponentName, android.os.IBinder)} + * _STARTING -> Mark as cancelled and wait for + * {@link JobServiceContext#doAcknowledgeStartMessage} + * _EXECUTING -> call {@link #sendStopMessageLocked}}, but only if there are no callbacks + * in the message queue. + * _ENDING -> No point in doing anything here, so we ignore. + */ + @GuardedBy("mLock") + private void handleCancelLocked(String reason) { + if (JobSchedulerService.DEBUG) { + Slog.d(TAG, "Handling cancel for: " + mRunningJob.getJobId() + " " + + VERB_STRINGS[mVerb]); + } + switch (mVerb) { + case VERB_BINDING: + case VERB_STARTING: + mCancelled = true; + applyStoppedReasonLocked(reason); + break; + case VERB_EXECUTING: + sendStopMessageLocked(reason); + break; + case VERB_STOPPING: + // Nada. + break; + default: + Slog.e(TAG, "Cancelling a job without a valid verb: " + mVerb); + break; + } + } + + /** Process MSG_TIMEOUT here. */ + @GuardedBy("mLock") + private void handleOpTimeoutLocked() { + switch (mVerb) { + case VERB_BINDING: + Slog.w(TAG, "Time-out while trying to bind " + getRunningJobNameLocked() + + ", dropping."); + closeAndCleanupJobLocked(false /* needsReschedule */, "timed out while binding"); + break; + case VERB_STARTING: + // Client unresponsive - wedged or failed to respond in time. We don't really + // know what happened so let's log it and notify the JobScheduler + // FINISHED/NO-RETRY. + Slog.w(TAG, "No response from client for onStartJob " + + getRunningJobNameLocked()); + closeAndCleanupJobLocked(false /* needsReschedule */, "timed out while starting"); + break; + case VERB_STOPPING: + // At least we got somewhere, so fail but ask the JobScheduler to reschedule. + Slog.w(TAG, "No response from client for onStopJob " + + getRunningJobNameLocked()); + closeAndCleanupJobLocked(true /* needsReschedule */, "timed out while stopping"); + break; + case VERB_EXECUTING: + // Not an error - client ran out of time. + Slog.i(TAG, "Client timed out while executing (no jobFinished received), " + + "sending onStop: " + getRunningJobNameLocked()); + mParams.setStopReason(JobParameters.REASON_TIMEOUT, "client timed out"); + sendStopMessageLocked("timeout while executing"); + break; + default: + Slog.e(TAG, "Handling timeout for an invalid job state: " + + getRunningJobNameLocked() + ", dropping."); + closeAndCleanupJobLocked(false /* needsReschedule */, "invalid timeout"); + } + } + + /** + * Already running, need to stop. Will switch {@link #mVerb} from VERB_EXECUTING -> + * VERB_STOPPING. + */ + @GuardedBy("mLock") + private void sendStopMessageLocked(String reason) { + removeOpTimeOutLocked(); + if (mVerb != VERB_EXECUTING) { + Slog.e(TAG, "Sending onStopJob for a job that isn't started. " + mRunningJob); + closeAndCleanupJobLocked(false /* reschedule */, reason); + return; + } + try { + applyStoppedReasonLocked(reason); + mVerb = VERB_STOPPING; + scheduleOpTimeOutLocked(); + service.stopJob(mParams); + } catch (RemoteException e) { + Slog.e(TAG, "Error sending onStopJob to client.", e); + // The job's host app apparently crashed during the job, so we should reschedule. + closeAndCleanupJobLocked(true /* reschedule */, "host crashed when trying to stop"); + } + } + + /** + * The provided job has finished, either by calling + * {@link android.app.job.JobService#jobFinished(android.app.job.JobParameters, boolean)} + * or from acknowledging the stop message we sent. Either way, we're done tracking it and + * we want to clean up internally. + */ + @GuardedBy("mLock") + private void closeAndCleanupJobLocked(boolean reschedule, String reason) { + final JobStatus completedJob; + if (mVerb == VERB_FINISHED) { + return; + } + applyStoppedReasonLocked(reason); + completedJob = mRunningJob; + mJobPackageTracker.noteInactive(completedJob, mParams.getStopReason(), reason); + try { + mBatteryStats.noteJobFinish(mRunningJob.getBatteryName(), + mRunningJob.getSourceUid(), mParams.getStopReason(), + mRunningJob.getStandbyBucket(), mRunningJob.getJobId()); + } catch (RemoteException e) { + // Whatever. + } + if (mWakeLock != null) { + mWakeLock.release(); + } + mContext.unbindService(JobServiceContext.this); + mWakeLock = null; + mRunningJob = null; + mRunningCallback = null; + mParams = null; + mVerb = VERB_FINISHED; + mCancelled = false; + service = null; + mAvailable = true; + removeOpTimeOutLocked(); + mCompletedListener.onJobCompletedLocked(completedJob, reschedule); + } + + private void applyStoppedReasonLocked(String reason) { + if (reason != null && mStoppedReason == null) { + mStoppedReason = reason; + mStoppedTime = sElapsedRealtimeClock.millis(); + if (mRunningCallback != null) { + mRunningCallback.mStoppedReason = mStoppedReason; + mRunningCallback.mStoppedTime = mStoppedTime; + } + } + } + + /** + * Called when sending a message to the client, over whose execution we have no control. If + * we haven't received a response in a certain amount of time, we want to give up and carry + * on with life. + */ + private void scheduleOpTimeOutLocked() { + removeOpTimeOutLocked(); + + final long timeoutMillis; + switch (mVerb) { + case VERB_EXECUTING: + timeoutMillis = EXECUTING_TIMESLICE_MILLIS; + break; + + case VERB_BINDING: + timeoutMillis = OP_BIND_TIMEOUT_MILLIS; + break; + + default: + timeoutMillis = OP_TIMEOUT_MILLIS; + break; + } + if (DEBUG) { + Slog.d(TAG, "Scheduling time out for '" + + mRunningJob.getServiceComponent().getShortClassName() + "' jId: " + + mParams.getJobId() + ", in " + (timeoutMillis / 1000) + " s"); + } + Message m = mCallbackHandler.obtainMessage(MSG_TIMEOUT, mRunningCallback); + mCallbackHandler.sendMessageDelayed(m, timeoutMillis); + mTimeoutElapsed = sElapsedRealtimeClock.millis() + timeoutMillis; + } + + + private void removeOpTimeOutLocked() { + mCallbackHandler.removeMessages(MSG_TIMEOUT); + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java new file mode 100644 index 000000000000..c2bdb6caffd3 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java @@ -0,0 +1,1280 @@ +/* + * Copyright (C) 2014 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; + +import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; +import static com.android.server.job.JobSchedulerService.sSystemClock; + +import android.annotation.Nullable; +import android.app.job.JobInfo; +import android.content.ComponentName; +import android.content.Context; +import android.net.NetworkRequest; +import android.os.Environment; +import android.os.Handler; +import android.os.PersistableBundle; +import android.os.Process; +import android.os.SystemClock; +import android.os.UserHandle; +import android.text.format.DateUtils; +import android.util.ArraySet; +import android.util.AtomicFile; +import android.util.Pair; +import android.util.Slog; +import android.util.SparseArray; +import android.util.Xml; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.BitUtils; +import com.android.internal.util.FastXmlSerializer; +import com.android.server.IoThread; +import com.android.server.LocalServices; +import com.android.server.job.JobSchedulerInternal.JobStorePersistStats; +import com.android.server.job.controllers.JobStatus; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * Maintains the master list of jobs that the job scheduler is tracking. These jobs are compared by + * reference, so none of the functions in this class should make a copy. + * Also handles read/write of persisted jobs. + * + * Note on locking: + * All callers to this class must <strong>lock on the class object they are calling</strong>. + * This is important b/c {@link com.android.server.job.JobStore.WriteJobsMapToDiskRunnable} + * and {@link com.android.server.job.JobStore.ReadJobMapFromDiskRunnable} lock on that + * object. + * + * Test: + * atest $ANDROID_BUILD_TOP/frameworks/base/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java + */ +public final class JobStore { + private static final String TAG = "JobStore"; + private static final boolean DEBUG = JobSchedulerService.DEBUG; + + /** Threshold to adjust how often we want to write to the db. */ + private static final long JOB_PERSIST_DELAY = 2000L; + + final Object mLock; + final Object mWriteScheduleLock; // used solely for invariants around write scheduling + final JobSet mJobSet; // per-caller-uid and per-source-uid tracking + final Context mContext; + + // Bookkeeping around incorrect boot-time system clock + private final long mXmlTimestamp; + private boolean mRtcGood; + + @GuardedBy("mWriteScheduleLock") + private boolean mWriteScheduled; + + @GuardedBy("mWriteScheduleLock") + private boolean mWriteInProgress; + + private static final Object sSingletonLock = new Object(); + private final AtomicFile mJobsFile; + /** Handler backed by IoThread for writing to disk. */ + private final Handler mIoHandler = IoThread.getHandler(); + private static JobStore sSingleton; + + private JobStorePersistStats mPersistInfo = new JobStorePersistStats(); + + /** Used by the {@link JobSchedulerService} to instantiate the JobStore. */ + static JobStore initAndGet(JobSchedulerService jobManagerService) { + synchronized (sSingletonLock) { + if (sSingleton == null) { + sSingleton = new JobStore(jobManagerService.getContext(), + jobManagerService.getLock(), Environment.getDataDirectory()); + } + return sSingleton; + } + } + + /** + * @return A freshly initialized job store object, with no loaded jobs. + */ + @VisibleForTesting + public static JobStore initAndGetForTesting(Context context, File dataDir) { + JobStore jobStoreUnderTest = new JobStore(context, new Object(), dataDir); + jobStoreUnderTest.clear(); + return jobStoreUnderTest; + } + + /** + * Construct the instance of the job store. This results in a blocking read from disk. + */ + private JobStore(Context context, Object lock, File dataDir) { + mLock = lock; + mWriteScheduleLock = new Object(); + mContext = context; + + File systemDir = new File(dataDir, "system"); + File jobDir = new File(systemDir, "job"); + jobDir.mkdirs(); + mJobsFile = new AtomicFile(new File(jobDir, "jobs.xml"), "jobs"); + + mJobSet = new JobSet(); + + // If the current RTC is earlier than the timestamp on our persisted jobs file, + // we suspect that the RTC is uninitialized and so we cannot draw conclusions + // about persisted job scheduling. + // + // Note that if the persisted jobs file does not exist, we proceed with the + // assumption that the RTC is good. This is less work and is safe: if the + // clock updates to sanity then we'll be saving the persisted jobs file in that + // correct state, which is normal; or we'll wind up writing the jobs file with + // an incorrect historical timestamp. That's fine; at worst we'll reboot with + // a *correct* timestamp, see a bunch of overdue jobs, and run them; then + // settle into normal operation. + mXmlTimestamp = mJobsFile.getLastModifiedTime(); + mRtcGood = (sSystemClock.millis() > mXmlTimestamp); + + readJobMapFromDisk(mJobSet, mRtcGood); + } + + public boolean jobTimesInflatedValid() { + return mRtcGood; + } + + public boolean clockNowValidToInflate(long now) { + return now >= mXmlTimestamp; + } + + /** + * Find all the jobs that were affected by RTC clock uncertainty at boot time. Returns + * parallel lists of the existing JobStatus objects and of new, equivalent JobStatus instances + * with now-corrected time bounds. + */ + public void getRtcCorrectedJobsLocked(final ArrayList<JobStatus> toAdd, + final ArrayList<JobStatus> toRemove) { + final long elapsedNow = sElapsedRealtimeClock.millis(); + + // Find the jobs that need to be fixed up, collecting them for post-iteration + // replacement with their new versions + forEachJob(job -> { + final Pair<Long, Long> utcTimes = job.getPersistedUtcTimes(); + if (utcTimes != null) { + Pair<Long, Long> elapsedRuntimes = + convertRtcBoundsToElapsed(utcTimes, elapsedNow); + JobStatus newJob = new JobStatus(job, + elapsedRuntimes.first, elapsedRuntimes.second, + 0, job.getLastSuccessfulRunTime(), job.getLastFailedRunTime()); + newJob.prepareLocked(); + toAdd.add(newJob); + toRemove.add(job); + } + }); + } + + /** + * Add a job to the master list, persisting it if necessary. If the JobStatus already exists, + * it will be replaced. + * @param jobStatus Job to add. + * @return Whether or not an equivalent JobStatus was replaced by this operation. + */ + public boolean add(JobStatus jobStatus) { + boolean replaced = mJobSet.remove(jobStatus); + mJobSet.add(jobStatus); + if (jobStatus.isPersisted()) { + maybeWriteStatusToDiskAsync(); + } + if (DEBUG) { + Slog.d(TAG, "Added job status to store: " + jobStatus); + } + return replaced; + } + + boolean containsJob(JobStatus jobStatus) { + return mJobSet.contains(jobStatus); + } + + public int size() { + return mJobSet.size(); + } + + public JobStorePersistStats getPersistStats() { + return mPersistInfo; + } + + public int countJobsForUid(int uid) { + return mJobSet.countJobsForUid(uid); + } + + /** + * Remove the provided job. Will also delete the job if it was persisted. + * @param removeFromPersisted If true, the job will be removed from the persisted job list + * immediately (if it was persisted). + * @return Whether or not the job existed to be removed. + */ + public boolean remove(JobStatus jobStatus, boolean removeFromPersisted) { + boolean removed = mJobSet.remove(jobStatus); + if (!removed) { + if (DEBUG) { + Slog.d(TAG, "Couldn't remove job: didn't exist: " + jobStatus); + } + return false; + } + if (removeFromPersisted && jobStatus.isPersisted()) { + maybeWriteStatusToDiskAsync(); + } + return removed; + } + + /** + * Remove the jobs of users not specified in the whitelist. + * @param whitelist Array of User IDs whose jobs are not to be removed. + */ + public void removeJobsOfNonUsers(int[] whitelist) { + mJobSet.removeJobsOfNonUsers(whitelist); + } + + @VisibleForTesting + public void clear() { + mJobSet.clear(); + maybeWriteStatusToDiskAsync(); + } + + /** + * @param userHandle User for whom we are querying the list of jobs. + * @return A list of all the jobs scheduled for the provided user. Never null. + */ + public List<JobStatus> getJobsByUser(int userHandle) { + return mJobSet.getJobsByUser(userHandle); + } + + /** + * @param uid Uid of the requesting app. + * @return All JobStatus objects for a given uid from the master list. Never null. + */ + public List<JobStatus> getJobsByUid(int uid) { + return mJobSet.getJobsByUid(uid); + } + + /** + * @param uid Uid of the requesting app. + * @param jobId Job id, specified at schedule-time. + * @return the JobStatus that matches the provided uId and jobId, or null if none found. + */ + public JobStatus getJobByUidAndJobId(int uid, int jobId) { + return mJobSet.get(uid, jobId); + } + + /** + * Iterate over the set of all jobs, invoking the supplied functor on each. This is for + * customers who need to examine each job; we'd much rather not have to generate + * transient unified collections for them to iterate over and then discard, or creating + * iterators every time a client needs to perform a sweep. + */ + public void forEachJob(Consumer<JobStatus> functor) { + mJobSet.forEachJob(null, functor); + } + + public void forEachJob(@Nullable Predicate<JobStatus> filterPredicate, + Consumer<JobStatus> functor) { + mJobSet.forEachJob(filterPredicate, functor); + } + + public void forEachJob(int uid, Consumer<JobStatus> functor) { + mJobSet.forEachJob(uid, functor); + } + + public void forEachJobForSourceUid(int sourceUid, Consumer<JobStatus> functor) { + mJobSet.forEachJobForSourceUid(sourceUid, functor); + } + + /** Version of the db schema. */ + private static final int JOBS_FILE_VERSION = 0; + /** Tag corresponds to constraints this job needs. */ + private static final String XML_TAG_PARAMS_CONSTRAINTS = "constraints"; + /** Tag corresponds to execution parameters. */ + private static final String XML_TAG_PERIODIC = "periodic"; + private static final String XML_TAG_ONEOFF = "one-off"; + private static final String XML_TAG_EXTRAS = "extras"; + + /** + * Every time the state changes we write all the jobs in one swath, instead of trying to + * track incremental changes. + */ + private void maybeWriteStatusToDiskAsync() { + synchronized (mWriteScheduleLock) { + if (!mWriteScheduled) { + if (DEBUG) { + Slog.v(TAG, "Scheduling persist of jobs to disk."); + } + mIoHandler.postDelayed(mWriteRunnable, JOB_PERSIST_DELAY); + mWriteScheduled = mWriteInProgress = true; + } + } + } + + @VisibleForTesting + public void readJobMapFromDisk(JobSet jobSet, boolean rtcGood) { + new ReadJobMapFromDiskRunnable(jobSet, rtcGood).run(); + } + + /** Write persisted JobStore state to disk synchronously. Should only be used for testing. */ + @VisibleForTesting + public void writeStatusToDiskForTesting() { + synchronized (mWriteScheduleLock) { + if (mWriteScheduled) { + throw new IllegalStateException("An asynchronous write is already scheduled."); + } + + mWriteScheduled = mWriteInProgress = true; + mWriteRunnable.run(); + } + } + + /** + * Wait for any pending write to the persistent store to clear + * @param maxWaitMillis Maximum time from present to wait + * @return {@code true} if I/O cleared as expected, {@code false} if the wait + * timed out before the pending write completed. + */ + @VisibleForTesting + public boolean waitForWriteToCompleteForTesting(long maxWaitMillis) { + final long start = SystemClock.uptimeMillis(); + final long end = start + maxWaitMillis; + synchronized (mWriteScheduleLock) { + while (mWriteInProgress) { + final long now = SystemClock.uptimeMillis(); + if (now >= end) { + // still not done and we've hit the end; failure + return false; + } + try { + mWriteScheduleLock.wait(now - start + maxWaitMillis); + } catch (InterruptedException e) { + // Spurious; keep waiting + break; + } + } + } + return true; + } + + /** + * Runnable that writes {@link #mJobSet} out to xml. + * NOTE: This Runnable locks on mLock + */ + private final Runnable mWriteRunnable = new Runnable() { + @Override + public void run() { + final long startElapsed = sElapsedRealtimeClock.millis(); + final List<JobStatus> storeCopy = new ArrayList<JobStatus>(); + // Intentionally allow new scheduling of a write operation *before* we clone + // the job set. If we reset it to false after cloning, there's a window in + // which no new write will be scheduled but mLock is not held, i.e. a new + // job might appear and fail to be recognized as needing a persist. The + // potential cost is one redundant write of an identical set of jobs in the + // rare case of that specific race, but by doing it this way we avoid quite + // a bit of lock contention. + synchronized (mWriteScheduleLock) { + mWriteScheduled = false; + } + synchronized (mLock) { + // Clone the jobs so we can release the lock before writing. + mJobSet.forEachJob(null, (job) -> { + if (job.isPersisted()) { + storeCopy.add(new JobStatus(job)); + } + }); + } + writeJobsMapImpl(storeCopy); + if (DEBUG) { + Slog.v(TAG, "Finished writing, took " + (sElapsedRealtimeClock.millis() + - startElapsed) + "ms"); + } + synchronized (mWriteScheduleLock) { + mWriteInProgress = false; + mWriteScheduleLock.notifyAll(); + } + } + + private void writeJobsMapImpl(List<JobStatus> jobList) { + int numJobs = 0; + int numSystemJobs = 0; + int numSyncJobs = 0; + try { + final long startTime = SystemClock.uptimeMillis(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + XmlSerializer out = new FastXmlSerializer(); + out.setOutput(baos, StandardCharsets.UTF_8.name()); + out.startDocument(null, true); + out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); + + out.startTag(null, "job-info"); + out.attribute(null, "version", Integer.toString(JOBS_FILE_VERSION)); + for (int i=0; i<jobList.size(); i++) { + JobStatus jobStatus = jobList.get(i); + if (DEBUG) { + Slog.d(TAG, "Saving job " + jobStatus.getJobId()); + } + out.startTag(null, "job"); + addAttributesToJobTag(out, jobStatus); + writeConstraintsToXml(out, jobStatus); + writeExecutionCriteriaToXml(out, jobStatus); + writeBundleToXml(jobStatus.getJob().getExtras(), out); + out.endTag(null, "job"); + + numJobs++; + if (jobStatus.getUid() == Process.SYSTEM_UID) { + numSystemJobs++; + if (isSyncJob(jobStatus)) { + numSyncJobs++; + } + } + } + out.endTag(null, "job-info"); + out.endDocument(); + + // Write out to disk in one fell swoop. + FileOutputStream fos = mJobsFile.startWrite(startTime); + fos.write(baos.toByteArray()); + mJobsFile.finishWrite(fos); + } catch (IOException e) { + if (DEBUG) { + Slog.v(TAG, "Error writing out job data.", e); + } + } catch (XmlPullParserException e) { + if (DEBUG) { + Slog.d(TAG, "Error persisting bundle.", e); + } + } finally { + mPersistInfo.countAllJobsSaved = numJobs; + mPersistInfo.countSystemServerJobsSaved = numSystemJobs; + mPersistInfo.countSystemSyncManagerJobsSaved = numSyncJobs; + } + } + + /** Write out a tag with data comprising the required fields and priority of this job and + * its client. + */ + private void addAttributesToJobTag(XmlSerializer out, JobStatus jobStatus) + throws IOException { + out.attribute(null, "jobid", Integer.toString(jobStatus.getJobId())); + out.attribute(null, "package", jobStatus.getServiceComponent().getPackageName()); + out.attribute(null, "class", jobStatus.getServiceComponent().getClassName()); + if (jobStatus.getSourcePackageName() != null) { + out.attribute(null, "sourcePackageName", jobStatus.getSourcePackageName()); + } + if (jobStatus.getSourceTag() != null) { + out.attribute(null, "sourceTag", jobStatus.getSourceTag()); + } + out.attribute(null, "sourceUserId", String.valueOf(jobStatus.getSourceUserId())); + out.attribute(null, "uid", Integer.toString(jobStatus.getUid())); + out.attribute(null, "priority", String.valueOf(jobStatus.getPriority())); + out.attribute(null, "flags", String.valueOf(jobStatus.getFlags())); + if (jobStatus.getInternalFlags() != 0) { + out.attribute(null, "internalFlags", String.valueOf(jobStatus.getInternalFlags())); + } + + out.attribute(null, "lastSuccessfulRunTime", + String.valueOf(jobStatus.getLastSuccessfulRunTime())); + out.attribute(null, "lastFailedRunTime", + String.valueOf(jobStatus.getLastFailedRunTime())); + } + + private void writeBundleToXml(PersistableBundle extras, XmlSerializer out) + throws IOException, XmlPullParserException { + out.startTag(null, XML_TAG_EXTRAS); + PersistableBundle extrasCopy = deepCopyBundle(extras, 10); + extrasCopy.saveToXml(out); + out.endTag(null, XML_TAG_EXTRAS); + } + + private PersistableBundle deepCopyBundle(PersistableBundle bundle, int maxDepth) { + if (maxDepth <= 0) { + return null; + } + PersistableBundle copy = (PersistableBundle) bundle.clone(); + Set<String> keySet = bundle.keySet(); + for (String key: keySet) { + Object o = copy.get(key); + if (o instanceof PersistableBundle) { + PersistableBundle bCopy = deepCopyBundle((PersistableBundle) o, maxDepth-1); + copy.putPersistableBundle(key, bCopy); + } + } + return copy; + } + + /** + * Write out a tag with data identifying this job's constraints. If the constraint isn't here + * it doesn't apply. + */ + private void writeConstraintsToXml(XmlSerializer out, JobStatus jobStatus) throws IOException { + out.startTag(null, XML_TAG_PARAMS_CONSTRAINTS); + if (jobStatus.hasConnectivityConstraint()) { + final NetworkRequest network = jobStatus.getJob().getRequiredNetwork(); + out.attribute(null, "net-capabilities", Long.toString( + BitUtils.packBits(network.networkCapabilities.getCapabilities()))); + out.attribute(null, "net-unwanted-capabilities", Long.toString( + BitUtils.packBits(network.networkCapabilities.getUnwantedCapabilities()))); + + out.attribute(null, "net-transport-types", Long.toString( + BitUtils.packBits(network.networkCapabilities.getTransportTypes()))); + } + if (jobStatus.hasIdleConstraint()) { + out.attribute(null, "idle", Boolean.toString(true)); + } + if (jobStatus.hasChargingConstraint()) { + out.attribute(null, "charging", Boolean.toString(true)); + } + if (jobStatus.hasBatteryNotLowConstraint()) { + out.attribute(null, "battery-not-low", Boolean.toString(true)); + } + if (jobStatus.hasStorageNotLowConstraint()) { + out.attribute(null, "storage-not-low", Boolean.toString(true)); + } + out.endTag(null, XML_TAG_PARAMS_CONSTRAINTS); + } + + private void writeExecutionCriteriaToXml(XmlSerializer out, JobStatus jobStatus) + throws IOException { + final JobInfo job = jobStatus.getJob(); + if (jobStatus.getJob().isPeriodic()) { + out.startTag(null, XML_TAG_PERIODIC); + out.attribute(null, "period", Long.toString(job.getIntervalMillis())); + out.attribute(null, "flex", Long.toString(job.getFlexMillis())); + } else { + out.startTag(null, XML_TAG_ONEOFF); + } + + // If we still have the persisted times, we need to record those directly because + // we haven't yet been able to calculate the usual elapsed-timebase bounds + // correctly due to wall-clock uncertainty. + Pair <Long, Long> utcJobTimes = jobStatus.getPersistedUtcTimes(); + if (DEBUG && utcJobTimes != null) { + Slog.i(TAG, "storing original UTC timestamps for " + jobStatus); + } + + final long nowRTC = sSystemClock.millis(); + final long nowElapsed = sElapsedRealtimeClock.millis(); + if (jobStatus.hasDeadlineConstraint()) { + // Wall clock deadline. + final long deadlineWallclock = (utcJobTimes == null) + ? nowRTC + (jobStatus.getLatestRunTimeElapsed() - nowElapsed) + : utcJobTimes.second; + out.attribute(null, "deadline", Long.toString(deadlineWallclock)); + } + if (jobStatus.hasTimingDelayConstraint()) { + final long delayWallclock = (utcJobTimes == null) + ? nowRTC + (jobStatus.getEarliestRunTime() - nowElapsed) + : utcJobTimes.first; + out.attribute(null, "delay", Long.toString(delayWallclock)); + } + + // Only write out back-off policy if it differs from the default. + // This also helps the case where the job is idle -> these aren't allowed to specify + // back-off. + if (jobStatus.getJob().getInitialBackoffMillis() != JobInfo.DEFAULT_INITIAL_BACKOFF_MILLIS + || jobStatus.getJob().getBackoffPolicy() != JobInfo.DEFAULT_BACKOFF_POLICY) { + out.attribute(null, "backoff-policy", Integer.toString(job.getBackoffPolicy())); + out.attribute(null, "initial-backoff", Long.toString(job.getInitialBackoffMillis())); + } + if (job.isPeriodic()) { + out.endTag(null, XML_TAG_PERIODIC); + } else { + out.endTag(null, XML_TAG_ONEOFF); + } + } + }; + + /** + * Translate the supplied RTC times to the elapsed timebase, with clamping appropriate + * to interpreting them as a job's delay + deadline times for alarm-setting purposes. + * @param rtcTimes a Pair<Long, Long> in which {@code first} is the "delay" earliest + * allowable runtime for the job, and {@code second} is the "deadline" time at which + * the job becomes overdue. + */ + private static Pair<Long, Long> convertRtcBoundsToElapsed(Pair<Long, Long> rtcTimes, + long nowElapsed) { + final long nowWallclock = sSystemClock.millis(); + final long earliest = (rtcTimes.first > JobStatus.NO_EARLIEST_RUNTIME) + ? nowElapsed + Math.max(rtcTimes.first - nowWallclock, 0) + : JobStatus.NO_EARLIEST_RUNTIME; + final long latest = (rtcTimes.second < JobStatus.NO_LATEST_RUNTIME) + ? nowElapsed + Math.max(rtcTimes.second - nowWallclock, 0) + : JobStatus.NO_LATEST_RUNTIME; + return Pair.create(earliest, latest); + } + + private static boolean isSyncJob(JobStatus status) { + return com.android.server.content.SyncJobService.class.getName() + .equals(status.getServiceComponent().getClassName()); + } + + /** + * Runnable that reads list of persisted job from xml. This is run once at start up, so doesn't + * need to go through {@link JobStore#add(com.android.server.job.controllers.JobStatus)}. + */ + private final class ReadJobMapFromDiskRunnable implements Runnable { + private final JobSet jobSet; + private final boolean rtcGood; + + /** + * @param jobSet Reference to the (empty) set of JobStatus objects that back the JobStore, + * so that after disk read we can populate it directly. + */ + ReadJobMapFromDiskRunnable(JobSet jobSet, boolean rtcIsGood) { + this.jobSet = jobSet; + this.rtcGood = rtcIsGood; + } + + @Override + public void run() { + int numJobs = 0; + int numSystemJobs = 0; + int numSyncJobs = 0; + try { + List<JobStatus> jobs; + FileInputStream fis = mJobsFile.openRead(); + synchronized (mLock) { + jobs = readJobMapImpl(fis, rtcGood); + if (jobs != null) { + long now = sElapsedRealtimeClock.millis(); + for (int i=0; i<jobs.size(); i++) { + JobStatus js = jobs.get(i); + js.prepareLocked(); + js.enqueueTime = now; + this.jobSet.add(js); + + numJobs++; + if (js.getUid() == Process.SYSTEM_UID) { + numSystemJobs++; + if (isSyncJob(js)) { + numSyncJobs++; + } + } + } + } + } + fis.close(); + } catch (FileNotFoundException e) { + if (DEBUG) { + Slog.d(TAG, "Could not find jobs file, probably there was nothing to load."); + } + } catch (XmlPullParserException | IOException e) { + Slog.wtf(TAG, "Error jobstore xml.", e); + } finally { + if (mPersistInfo.countAllJobsLoaded < 0) { // Only set them once. + mPersistInfo.countAllJobsLoaded = numJobs; + mPersistInfo.countSystemServerJobsLoaded = numSystemJobs; + mPersistInfo.countSystemSyncManagerJobsLoaded = numSyncJobs; + } + } + Slog.i(TAG, "Read " + numJobs + " jobs"); + } + + private List<JobStatus> readJobMapImpl(FileInputStream fis, boolean rtcIsGood) + throws XmlPullParserException, IOException { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(fis, StandardCharsets.UTF_8.name()); + + int eventType = parser.getEventType(); + while (eventType != XmlPullParser.START_TAG && + eventType != XmlPullParser.END_DOCUMENT) { + eventType = parser.next(); + Slog.d(TAG, "Start tag: " + parser.getName()); + } + if (eventType == XmlPullParser.END_DOCUMENT) { + if (DEBUG) { + Slog.d(TAG, "No persisted jobs."); + } + return null; + } + + String tagName = parser.getName(); + if ("job-info".equals(tagName)) { + final List<JobStatus> jobs = new ArrayList<JobStatus>(); + // Read in version info. + try { + int version = Integer.parseInt(parser.getAttributeValue(null, "version")); + if (version != JOBS_FILE_VERSION) { + Slog.d(TAG, "Invalid version number, aborting jobs file read."); + return null; + } + } catch (NumberFormatException e) { + Slog.e(TAG, "Invalid version number, aborting jobs file read."); + return null; + } + eventType = parser.next(); + do { + // Read each <job/> + if (eventType == XmlPullParser.START_TAG) { + tagName = parser.getName(); + // Start reading job. + if ("job".equals(tagName)) { + JobStatus persistedJob = restoreJobFromXml(rtcIsGood, parser); + if (persistedJob != null) { + if (DEBUG) { + Slog.d(TAG, "Read out " + persistedJob); + } + jobs.add(persistedJob); + } else { + Slog.d(TAG, "Error reading job from file."); + } + } + } + eventType = parser.next(); + } while (eventType != XmlPullParser.END_DOCUMENT); + return jobs; + } + return null; + } + + /** + * @param parser Xml parser at the beginning of a "<job/>" tag. The next "parser.next()" call + * will take the parser into the body of the job tag. + * @return Newly instantiated job holding all the information we just read out of the xml tag. + */ + private JobStatus restoreJobFromXml(boolean rtcIsGood, XmlPullParser parser) + throws XmlPullParserException, IOException { + JobInfo.Builder jobBuilder; + int uid, sourceUserId; + long lastSuccessfulRunTime; + long lastFailedRunTime; + int internalFlags = 0; + + // Read out job identifier attributes and priority. + try { + jobBuilder = buildBuilderFromXml(parser); + jobBuilder.setPersisted(true); + uid = Integer.parseInt(parser.getAttributeValue(null, "uid")); + + String val = parser.getAttributeValue(null, "priority"); + if (val != null) { + jobBuilder.setPriority(Integer.parseInt(val)); + } + val = parser.getAttributeValue(null, "flags"); + if (val != null) { + jobBuilder.setFlags(Integer.parseInt(val)); + } + val = parser.getAttributeValue(null, "internalFlags"); + if (val != null) { + internalFlags = Integer.parseInt(val); + } + val = parser.getAttributeValue(null, "sourceUserId"); + sourceUserId = val == null ? -1 : Integer.parseInt(val); + + val = parser.getAttributeValue(null, "lastSuccessfulRunTime"); + lastSuccessfulRunTime = val == null ? 0 : Long.parseLong(val); + + val = parser.getAttributeValue(null, "lastFailedRunTime"); + lastFailedRunTime = val == null ? 0 : Long.parseLong(val); + } catch (NumberFormatException e) { + Slog.e(TAG, "Error parsing job's required fields, skipping"); + return null; + } + + String sourcePackageName = parser.getAttributeValue(null, "sourcePackageName"); + final String sourceTag = parser.getAttributeValue(null, "sourceTag"); + + int eventType; + // Read out constraints tag. + do { + eventType = parser.next(); + } while (eventType == XmlPullParser.TEXT); // Push through to next START_TAG. + + if (!(eventType == XmlPullParser.START_TAG && + XML_TAG_PARAMS_CONSTRAINTS.equals(parser.getName()))) { + // Expecting a <constraints> start tag. + return null; + } + try { + buildConstraintsFromXml(jobBuilder, parser); + } catch (NumberFormatException e) { + Slog.d(TAG, "Error reading constraints, skipping."); + return null; + } + parser.next(); // Consume </constraints> + + // Read out execution parameters tag. + do { + eventType = parser.next(); + } while (eventType == XmlPullParser.TEXT); + if (eventType != XmlPullParser.START_TAG) { + return null; + } + + // Tuple of (earliest runtime, latest runtime) in UTC. + final Pair<Long, Long> rtcRuntimes; + try { + rtcRuntimes = buildRtcExecutionTimesFromXml(parser); + } catch (NumberFormatException e) { + if (DEBUG) { + Slog.d(TAG, "Error parsing execution time parameters, skipping."); + } + return null; + } + + final long elapsedNow = sElapsedRealtimeClock.millis(); + Pair<Long, Long> elapsedRuntimes = convertRtcBoundsToElapsed(rtcRuntimes, elapsedNow); + + if (XML_TAG_PERIODIC.equals(parser.getName())) { + try { + String val = parser.getAttributeValue(null, "period"); + final long periodMillis = Long.parseLong(val); + val = parser.getAttributeValue(null, "flex"); + final long flexMillis = (val != null) ? Long.valueOf(val) : periodMillis; + jobBuilder.setPeriodic(periodMillis, flexMillis); + // As a sanity check, cap the recreated run time to be no later than flex+period + // from now. This is the latest the periodic could be pushed out. This could + // happen if the periodic ran early (at flex time before period), and then the + // device rebooted. + if (elapsedRuntimes.second > elapsedNow + periodMillis + flexMillis) { + final long clampedLateRuntimeElapsed = elapsedNow + flexMillis + + periodMillis; + final long clampedEarlyRuntimeElapsed = clampedLateRuntimeElapsed + - flexMillis; + Slog.w(TAG, + String.format("Periodic job for uid='%d' persisted run-time is" + + " too big [%s, %s]. Clamping to [%s,%s]", + uid, + DateUtils.formatElapsedTime(elapsedRuntimes.first / 1000), + DateUtils.formatElapsedTime(elapsedRuntimes.second / 1000), + DateUtils.formatElapsedTime( + clampedEarlyRuntimeElapsed / 1000), + DateUtils.formatElapsedTime( + clampedLateRuntimeElapsed / 1000)) + ); + elapsedRuntimes = + Pair.create(clampedEarlyRuntimeElapsed, clampedLateRuntimeElapsed); + } + } catch (NumberFormatException e) { + Slog.d(TAG, "Error reading periodic execution criteria, skipping."); + return null; + } + } else if (XML_TAG_ONEOFF.equals(parser.getName())) { + try { + if (elapsedRuntimes.first != JobStatus.NO_EARLIEST_RUNTIME) { + jobBuilder.setMinimumLatency(elapsedRuntimes.first - elapsedNow); + } + if (elapsedRuntimes.second != JobStatus.NO_LATEST_RUNTIME) { + jobBuilder.setOverrideDeadline( + elapsedRuntimes.second - elapsedNow); + } + } catch (NumberFormatException e) { + Slog.d(TAG, "Error reading job execution criteria, skipping."); + return null; + } + } else { + if (DEBUG) { + Slog.d(TAG, "Invalid parameter tag, skipping - " + parser.getName()); + } + // Expecting a parameters start tag. + return null; + } + maybeBuildBackoffPolicyFromXml(jobBuilder, parser); + + parser.nextTag(); // Consume parameters end tag. + + // Read out extras Bundle. + do { + eventType = parser.next(); + } while (eventType == XmlPullParser.TEXT); + if (!(eventType == XmlPullParser.START_TAG + && XML_TAG_EXTRAS.equals(parser.getName()))) { + if (DEBUG) { + Slog.d(TAG, "Error reading extras, skipping."); + } + return null; + } + + PersistableBundle extras = PersistableBundle.restoreFromXml(parser); + jobBuilder.setExtras(extras); + parser.nextTag(); // Consume </extras> + + final JobInfo builtJob; + try { + builtJob = jobBuilder.build(); + } catch (Exception e) { + Slog.w(TAG, "Unable to build job from XML, ignoring: " + + jobBuilder.summarize()); + return null; + } + + // Migrate sync jobs forward from earlier, incomplete representation + if ("android".equals(sourcePackageName) + && extras != null + && extras.getBoolean("SyncManagerJob", false)) { + sourcePackageName = extras.getString("owningPackage", sourcePackageName); + if (DEBUG) { + Slog.i(TAG, "Fixing up sync job source package name from 'android' to '" + + sourcePackageName + "'"); + } + } + + // And now we're done + JobSchedulerInternal service = LocalServices.getService(JobSchedulerInternal.class); + final int appBucket = JobSchedulerService.standbyBucketForPackage(sourcePackageName, + sourceUserId, elapsedNow); + JobStatus js = new JobStatus( + jobBuilder.build(), uid, sourcePackageName, sourceUserId, + appBucket, sourceTag, + elapsedRuntimes.first, elapsedRuntimes.second, + lastSuccessfulRunTime, lastFailedRunTime, + (rtcIsGood) ? null : rtcRuntimes, internalFlags); + return js; + } + + private JobInfo.Builder buildBuilderFromXml(XmlPullParser parser) throws NumberFormatException { + // Pull out required fields from <job> attributes. + int jobId = Integer.parseInt(parser.getAttributeValue(null, "jobid")); + String packageName = parser.getAttributeValue(null, "package"); + String className = parser.getAttributeValue(null, "class"); + ComponentName cname = new ComponentName(packageName, className); + + return new JobInfo.Builder(jobId, cname); + } + + private void buildConstraintsFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) { + String val; + + final String netCapabilities = parser.getAttributeValue(null, "net-capabilities"); + final String netUnwantedCapabilities = parser.getAttributeValue( + null, "net-unwanted-capabilities"); + final String netTransportTypes = parser.getAttributeValue(null, "net-transport-types"); + if (netCapabilities != null && netTransportTypes != null) { + final NetworkRequest request = new NetworkRequest.Builder().build(); + final long unwantedCapabilities = netUnwantedCapabilities != null + ? Long.parseLong(netUnwantedCapabilities) + : BitUtils.packBits(request.networkCapabilities.getUnwantedCapabilities()); + + // We're okay throwing NFE here; caught by caller + request.networkCapabilities.setCapabilities( + BitUtils.unpackBits(Long.parseLong(netCapabilities)), + BitUtils.unpackBits(unwantedCapabilities)); + request.networkCapabilities.setTransportTypes( + BitUtils.unpackBits(Long.parseLong(netTransportTypes))); + jobBuilder.setRequiredNetwork(request); + } else { + // Read legacy values + val = parser.getAttributeValue(null, "connectivity"); + if (val != null) { + jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY); + } + val = parser.getAttributeValue(null, "metered"); + if (val != null) { + jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_METERED); + } + val = parser.getAttributeValue(null, "unmetered"); + if (val != null) { + jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED); + } + val = parser.getAttributeValue(null, "not-roaming"); + if (val != null) { + jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_NOT_ROAMING); + } + } + + val = parser.getAttributeValue(null, "idle"); + if (val != null) { + jobBuilder.setRequiresDeviceIdle(true); + } + val = parser.getAttributeValue(null, "charging"); + if (val != null) { + jobBuilder.setRequiresCharging(true); + } + val = parser.getAttributeValue(null, "battery-not-low"); + if (val != null) { + jobBuilder.setRequiresBatteryNotLow(true); + } + val = parser.getAttributeValue(null, "storage-not-low"); + if (val != null) { + jobBuilder.setRequiresStorageNotLow(true); + } + } + + /** + * Builds the back-off policy out of the params tag. These attributes may not exist, depending + * on whether the back-off was set when the job was first scheduled. + */ + private void maybeBuildBackoffPolicyFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) { + String val = parser.getAttributeValue(null, "initial-backoff"); + if (val != null) { + long initialBackoff = Long.parseLong(val); + val = parser.getAttributeValue(null, "backoff-policy"); + int backoffPolicy = Integer.parseInt(val); // Will throw NFE which we catch higher up. + jobBuilder.setBackoffCriteria(initialBackoff, backoffPolicy); + } + } + + /** + * Extract a job's earliest/latest run time data from XML. These are returned in + * unadjusted UTC wall clock time, because we do not yet know whether the system + * clock is reliable for purposes of calculating deltas from 'now'. + * + * @param parser + * @return A Pair of timestamps in UTC wall-clock time. The first is the earliest + * time at which the job is to become runnable, and the second is the deadline at + * which it becomes overdue to execute. + * @throws NumberFormatException + */ + private Pair<Long, Long> buildRtcExecutionTimesFromXml(XmlPullParser parser) + throws NumberFormatException { + String val; + // Pull out execution time data. + val = parser.getAttributeValue(null, "delay"); + final long earliestRunTimeRtc = (val != null) + ? Long.parseLong(val) + : JobStatus.NO_EARLIEST_RUNTIME; + val = parser.getAttributeValue(null, "deadline"); + final long latestRunTimeRtc = (val != null) + ? Long.parseLong(val) + : JobStatus.NO_LATEST_RUNTIME; + return Pair.create(earliestRunTimeRtc, latestRunTimeRtc); + } + } + + /** Set of all tracked jobs. */ + @VisibleForTesting + public static final class JobSet { + @VisibleForTesting // Key is the getUid() originator of the jobs in each sheaf + final SparseArray<ArraySet<JobStatus>> mJobs; + + @VisibleForTesting // Same data but with the key as getSourceUid() of the jobs in each sheaf + final SparseArray<ArraySet<JobStatus>> mJobsPerSourceUid; + + public JobSet() { + mJobs = new SparseArray<ArraySet<JobStatus>>(); + mJobsPerSourceUid = new SparseArray<>(); + } + + public List<JobStatus> getJobsByUid(int uid) { + ArrayList<JobStatus> matchingJobs = new ArrayList<JobStatus>(); + ArraySet<JobStatus> jobs = mJobs.get(uid); + if (jobs != null) { + matchingJobs.addAll(jobs); + } + return matchingJobs; + } + + // By user, not by uid, so we need to traverse by key and check + public List<JobStatus> getJobsByUser(int userId) { + final ArrayList<JobStatus> result = new ArrayList<JobStatus>(); + for (int i = mJobsPerSourceUid.size() - 1; i >= 0; i--) { + if (UserHandle.getUserId(mJobsPerSourceUid.keyAt(i)) == userId) { + final ArraySet<JobStatus> jobs = mJobsPerSourceUid.valueAt(i); + if (jobs != null) { + result.addAll(jobs); + } + } + } + return result; + } + + public boolean add(JobStatus job) { + final int uid = job.getUid(); + final int sourceUid = job.getSourceUid(); + ArraySet<JobStatus> jobs = mJobs.get(uid); + if (jobs == null) { + jobs = new ArraySet<JobStatus>(); + mJobs.put(uid, jobs); + } + ArraySet<JobStatus> jobsForSourceUid = mJobsPerSourceUid.get(sourceUid); + if (jobsForSourceUid == null) { + jobsForSourceUid = new ArraySet<>(); + mJobsPerSourceUid.put(sourceUid, jobsForSourceUid); + } + final boolean added = jobs.add(job); + final boolean addedInSource = jobsForSourceUid.add(job); + if (added != addedInSource) { + Slog.wtf(TAG, "mJobs and mJobsPerSourceUid mismatch; caller= " + added + + " source= " + addedInSource); + } + return added || addedInSource; + } + + public boolean remove(JobStatus job) { + final int uid = job.getUid(); + final ArraySet<JobStatus> jobs = mJobs.get(uid); + final int sourceUid = job.getSourceUid(); + final ArraySet<JobStatus> jobsForSourceUid = mJobsPerSourceUid.get(sourceUid); + final boolean didRemove = jobs != null && jobs.remove(job); + final boolean sourceRemove = jobsForSourceUid != null && jobsForSourceUid.remove(job); + if (didRemove != sourceRemove) { + Slog.wtf(TAG, "Job presence mismatch; caller=" + didRemove + + " source=" + sourceRemove); + } + if (didRemove || sourceRemove) { + // no more jobs for this uid? let the now-empty set objects be GC'd. + if (jobs != null && jobs.size() == 0) { + mJobs.remove(uid); + } + if (jobsForSourceUid != null && jobsForSourceUid.size() == 0) { + mJobsPerSourceUid.remove(sourceUid); + } + return true; + } + return false; + } + + /** + * Removes the jobs of all users not specified by the whitelist of user ids. + * This will remove jobs scheduled *by* non-existent users as well as jobs scheduled *for* + * non-existent users + */ + public void removeJobsOfNonUsers(final int[] whitelist) { + final Predicate<JobStatus> noSourceUser = + job -> !ArrayUtils.contains(whitelist, job.getSourceUserId()); + final Predicate<JobStatus> noCallingUser = + job -> !ArrayUtils.contains(whitelist, job.getUserId()); + removeAll(noSourceUser.or(noCallingUser)); + } + + private void removeAll(Predicate<JobStatus> predicate) { + for (int jobSetIndex = mJobs.size() - 1; jobSetIndex >= 0; jobSetIndex--) { + final ArraySet<JobStatus> jobs = mJobs.valueAt(jobSetIndex); + for (int jobIndex = jobs.size() - 1; jobIndex >= 0; jobIndex--) { + if (predicate.test(jobs.valueAt(jobIndex))) { + jobs.removeAt(jobIndex); + } + } + if (jobs.size() == 0) { + mJobs.removeAt(jobSetIndex); + } + } + for (int jobSetIndex = mJobsPerSourceUid.size() - 1; jobSetIndex >= 0; jobSetIndex--) { + final ArraySet<JobStatus> jobs = mJobsPerSourceUid.valueAt(jobSetIndex); + for (int jobIndex = jobs.size() - 1; jobIndex >= 0; jobIndex--) { + if (predicate.test(jobs.valueAt(jobIndex))) { + jobs.removeAt(jobIndex); + } + } + if (jobs.size() == 0) { + mJobsPerSourceUid.removeAt(jobSetIndex); + } + } + } + + public boolean contains(JobStatus job) { + final int uid = job.getUid(); + ArraySet<JobStatus> jobs = mJobs.get(uid); + return jobs != null && jobs.contains(job); + } + + public JobStatus get(int uid, int jobId) { + ArraySet<JobStatus> jobs = mJobs.get(uid); + if (jobs != null) { + for (int i = jobs.size() - 1; i >= 0; i--) { + JobStatus job = jobs.valueAt(i); + if (job.getJobId() == jobId) { + return job; + } + } + } + return null; + } + + // Inefficient; use only for testing + public List<JobStatus> getAllJobs() { + ArrayList<JobStatus> allJobs = new ArrayList<JobStatus>(size()); + for (int i = mJobs.size() - 1; i >= 0; i--) { + ArraySet<JobStatus> jobs = mJobs.valueAt(i); + if (jobs != null) { + // Use a for loop over the ArraySet, so we don't need to make its + // optional collection class iterator implementation or have to go + // through a temporary array from toArray(). + for (int j = jobs.size() - 1; j >= 0; j--) { + allJobs.add(jobs.valueAt(j)); + } + } + } + return allJobs; + } + + public void clear() { + mJobs.clear(); + mJobsPerSourceUid.clear(); + } + + public int size() { + int total = 0; + for (int i = mJobs.size() - 1; i >= 0; i--) { + total += mJobs.valueAt(i).size(); + } + return total; + } + + // We only want to count the jobs that this uid has scheduled on its own + // behalf, not those that the app has scheduled on someone else's behalf. + public int countJobsForUid(int uid) { + int total = 0; + ArraySet<JobStatus> jobs = mJobs.get(uid); + if (jobs != null) { + for (int i = jobs.size() - 1; i >= 0; i--) { + JobStatus job = jobs.valueAt(i); + if (job.getUid() == job.getSourceUid()) { + total++; + } + } + } + return total; + } + + public void forEachJob(@Nullable Predicate<JobStatus> filterPredicate, + Consumer<JobStatus> functor) { + for (int uidIndex = mJobs.size() - 1; uidIndex >= 0; uidIndex--) { + ArraySet<JobStatus> jobs = mJobs.valueAt(uidIndex); + if (jobs != null) { + for (int i = jobs.size() - 1; i >= 0; i--) { + final JobStatus jobStatus = jobs.valueAt(i); + if ((filterPredicate == null) || filterPredicate.test(jobStatus)) { + functor.accept(jobStatus); + } + } + } + } + } + + public void forEachJob(int callingUid, Consumer<JobStatus> functor) { + ArraySet<JobStatus> jobs = mJobs.get(callingUid); + if (jobs != null) { + for (int i = jobs.size() - 1; i >= 0; i--) { + functor.accept(jobs.valueAt(i)); + } + } + } + + public void forEachJobForSourceUid(int sourceUid, Consumer<JobStatus> functor) { + final ArraySet<JobStatus> jobs = mJobsPerSourceUid.get(sourceUid); + if (jobs != null) { + for (int i = jobs.size() - 1; i >= 0; i--) { + functor.accept(jobs.valueAt(i)); + } + } + } + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/StateChangedListener.java b/apex/jobscheduler/service/java/com/android/server/job/StateChangedListener.java new file mode 100644 index 000000000000..87bfc27a715f --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/StateChangedListener.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2014 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; + +import com.android.server.job.controllers.JobStatus; + +/** + * Interface through which a {@link com.android.server.job.controllers.StateController} informs + * the {@link com.android.server.job.JobSchedulerService} that there are some tasks potentially + * ready to be run. + */ +public interface StateChangedListener { + /** + * Called by the controller to notify the JobManager that it should check on the state of a + * task. + */ + public void onControllerStateChanged(); + + /** + * Called by the controller to notify the JobManager that regardless of the state of the task, + * it must be run immediately. + * @param jobStatus The state of the task which is to be run immediately. <strong>null + * indicates to the scheduler that any ready jobs should be flushed.</strong> + */ + public void onRunJobNow(JobStatus jobStatus); + + public void onDeviceIdleStateChanged(boolean deviceIdle); +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java new file mode 100644 index 000000000000..8b610061d3aa --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2017 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.os.SystemClock; +import android.os.UserHandle; +import android.util.Log; +import android.util.Slog; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.util.IndentingPrintWriter; +import com.android.internal.util.Preconditions; +import com.android.server.AppStateTracker; +import com.android.server.AppStateTracker.Listener; +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.BackgroundJobsController.TrackedJob; + +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * Tracks the following pieces of JobStatus state: + * + * - the CONSTRAINT_BACKGROUND_NOT_RESTRICTED general constraint bit, which + * is used to selectively permit battery-saver exempted jobs to run; and + * + * - the uid-active boolean state expressed by the AppStateTracker. Jobs in 'active' + * uids are inherently eligible to run jobs regardless of the uid's standby bucket. + */ +public final class BackgroundJobsController extends StateController { + private static final String TAG = "JobScheduler.Background"; + private static final boolean DEBUG = JobSchedulerService.DEBUG + || Log.isLoggable(TAG, Log.DEBUG); + + // Tri-state about possible "is this uid 'active'?" knowledge + static final int UNKNOWN = 0; + static final int KNOWN_ACTIVE = 1; + static final int KNOWN_INACTIVE = 2; + + private final AppStateTracker mAppStateTracker; + + public BackgroundJobsController(JobSchedulerService service) { + super(service); + + mAppStateTracker = Preconditions.checkNotNull( + LocalServices.getService(AppStateTracker.class)); + mAppStateTracker.addListener(mForceAppStandbyListener); + } + + @Override + public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) { + updateSingleJobRestrictionLocked(jobStatus, UNKNOWN); + } + + @Override + public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, + boolean forUpdate) { + } + + @Override + public void dumpControllerStateLocked(final IndentingPrintWriter pw, + final Predicate<JobStatus> predicate) { + mAppStateTracker.dump(pw); + pw.println(); + + mService.getJobStore().forEachJob(predicate, (jobStatus) -> { + final int uid = jobStatus.getSourceUid(); + final String sourcePkg = jobStatus.getSourcePackageName(); + pw.print("#"); + jobStatus.printUniqueId(pw); + pw.print(" from "); + UserHandle.formatUid(pw, uid); + pw.print(mAppStateTracker.isUidActive(uid) ? " active" : " idle"); + if (mAppStateTracker.isUidPowerSaveWhitelisted(uid) || + mAppStateTracker.isUidTempPowerSaveWhitelisted(uid)) { + pw.print(", whitelisted"); + } + pw.print(": "); + pw.print(sourcePkg); + + pw.print(" [RUN_ANY_IN_BACKGROUND "); + pw.print(mAppStateTracker.isRunAnyInBackgroundAppOpsAllowed(uid, sourcePkg) + ? "allowed]" : "disallowed]"); + + if ((jobStatus.satisfiedConstraints + & JobStatus.CONSTRAINT_BACKGROUND_NOT_RESTRICTED) != 0) { + pw.println(" RUNNABLE"); + } else { + pw.println(" WAITING"); + } + }); + } + + @Override + public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, + Predicate<JobStatus> predicate) { + final long token = proto.start(fieldId); + final long mToken = proto.start(StateControllerProto.BACKGROUND); + + mAppStateTracker.dumpProto(proto, + StateControllerProto.BackgroundJobsController.APP_STATE_TRACKER); + + mService.getJobStore().forEachJob(predicate, (jobStatus) -> { + final long jsToken = + proto.start(StateControllerProto.BackgroundJobsController.TRACKED_JOBS); + + jobStatus.writeToShortProto(proto, TrackedJob.INFO); + final int sourceUid = jobStatus.getSourceUid(); + proto.write(TrackedJob.SOURCE_UID, sourceUid); + final String sourcePkg = jobStatus.getSourcePackageName(); + proto.write(TrackedJob.SOURCE_PACKAGE_NAME, sourcePkg); + + proto.write(TrackedJob.IS_IN_FOREGROUND, mAppStateTracker.isUidActive(sourceUid)); + proto.write(TrackedJob.IS_WHITELISTED, + mAppStateTracker.isUidPowerSaveWhitelisted(sourceUid) || + mAppStateTracker.isUidTempPowerSaveWhitelisted(sourceUid)); + + proto.write(TrackedJob.CAN_RUN_ANY_IN_BACKGROUND, + mAppStateTracker.isRunAnyInBackgroundAppOpsAllowed(sourceUid, sourcePkg)); + + proto.write(TrackedJob.ARE_CONSTRAINTS_SATISFIED, + (jobStatus.satisfiedConstraints & + JobStatus.CONSTRAINT_BACKGROUND_NOT_RESTRICTED) != 0); + + proto.end(jsToken); + }); + + proto.end(mToken); + proto.end(token); + } + + private void updateAllJobRestrictionsLocked() { + updateJobRestrictionsLocked(/*filterUid=*/ -1, UNKNOWN); + } + + private void updateJobRestrictionsForUidLocked(int uid, boolean isActive) { + updateJobRestrictionsLocked(uid, (isActive) ? KNOWN_ACTIVE : KNOWN_INACTIVE); + } + + private void updateJobRestrictionsLocked(int filterUid, int newActiveState) { + final UpdateJobFunctor updateTrackedJobs = new UpdateJobFunctor(newActiveState); + + final long start = DEBUG ? SystemClock.elapsedRealtimeNanos() : 0; + + final JobStore store = mService.getJobStore(); + if (filterUid > 0) { + store.forEachJobForSourceUid(filterUid, updateTrackedJobs); + } else { + store.forEachJob(updateTrackedJobs); + } + + final long time = DEBUG ? (SystemClock.elapsedRealtimeNanos() - start) : 0; + if (DEBUG) { + Slog.d(TAG, String.format( + "Job status updated: %d/%d checked/total jobs, %d us", + updateTrackedJobs.mCheckedCount, + updateTrackedJobs.mTotalCount, + (time / 1000) + )); + } + + if (updateTrackedJobs.mChanged) { + mStateChangedListener.onControllerStateChanged(); + } + } + + boolean updateSingleJobRestrictionLocked(JobStatus jobStatus, int activeState) { + + final int uid = jobStatus.getSourceUid(); + final String packageName = jobStatus.getSourcePackageName(); + + final boolean canRun = !mAppStateTracker.areJobsRestricted(uid, packageName, + (jobStatus.getInternalFlags() & JobStatus.INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION) + != 0); + + final boolean isActive; + if (activeState == UNKNOWN) { + isActive = mAppStateTracker.isUidActive(uid); + } else { + isActive = (activeState == KNOWN_ACTIVE); + } + boolean didChange = jobStatus.setBackgroundNotRestrictedConstraintSatisfied(canRun); + didChange |= jobStatus.setUidActive(isActive); + return didChange; + } + + private final class UpdateJobFunctor implements Consumer<JobStatus> { + final int activeState; + boolean mChanged = false; + int mTotalCount = 0; + int mCheckedCount = 0; + + public UpdateJobFunctor(int newActiveState) { + activeState = newActiveState; + } + + @Override + public void accept(JobStatus jobStatus) { + mTotalCount++; + mCheckedCount++; + if (updateSingleJobRestrictionLocked(jobStatus, activeState)) { + mChanged = true; + } + } + } + + private final Listener mForceAppStandbyListener = new Listener() { + @Override + public void updateAllJobs() { + synchronized (mLock) { + updateAllJobRestrictionsLocked(); + } + } + + @Override + public void updateJobsForUid(int uid, boolean isActive) { + synchronized (mLock) { + updateJobRestrictionsForUidLocked(uid, isActive); + } + } + + @Override + public void updateJobsForUidPackage(int uid, String packageName, boolean isActive) { + synchronized (mLock) { + updateJobRestrictionsForUidLocked(uid, isActive); + } + } + }; +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java new file mode 100644 index 000000000000..46658ad33b85 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2014 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 static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.BatteryManager; +import android.os.BatteryManagerInternal; +import android.os.UserHandle; +import android.util.ArraySet; +import android.util.Log; +import android.util.Slog; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.IndentingPrintWriter; +import com.android.server.LocalServices; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.StateControllerProto; + +import java.util.function.Predicate; + +/** + * Simple controller that tracks whether the phone is charging or not. The phone is considered to + * be charging when it's been plugged in for more than two minutes, and the system has broadcast + * ACTION_BATTERY_OK. + */ +public final class BatteryController extends StateController { + private static final String TAG = "JobScheduler.Battery"; + private static final boolean DEBUG = JobSchedulerService.DEBUG + || Log.isLoggable(TAG, Log.DEBUG); + + private final ArraySet<JobStatus> mTrackedTasks = new ArraySet<>(); + private ChargingTracker mChargeTracker; + + @VisibleForTesting + public ChargingTracker getTracker() { + return mChargeTracker; + } + + public BatteryController(JobSchedulerService service) { + super(service); + mChargeTracker = new ChargingTracker(); + mChargeTracker.startTracking(); + } + + @Override + public void maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob) { + if (taskStatus.hasPowerConstraint()) { + mTrackedTasks.add(taskStatus); + taskStatus.setTrackingController(JobStatus.TRACKING_BATTERY); + taskStatus.setChargingConstraintSatisfied(mChargeTracker.isOnStablePower()); + taskStatus.setBatteryNotLowConstraintSatisfied(mChargeTracker.isBatteryNotLow()); + } + } + + @Override + public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, boolean forUpdate) { + if (taskStatus.clearTrackingController(JobStatus.TRACKING_BATTERY)) { + mTrackedTasks.remove(taskStatus); + } + } + + private void maybeReportNewChargingStateLocked() { + final boolean stablePower = mChargeTracker.isOnStablePower(); + final boolean batteryNotLow = mChargeTracker.isBatteryNotLow(); + if (DEBUG) { + Slog.d(TAG, "maybeReportNewChargingStateLocked: " + stablePower); + } + boolean reportChange = false; + for (int i = mTrackedTasks.size() - 1; i >= 0; i--) { + final JobStatus ts = mTrackedTasks.valueAt(i); + boolean previous = ts.setChargingConstraintSatisfied(stablePower); + if (previous != stablePower) { + reportChange = true; + } + previous = ts.setBatteryNotLowConstraintSatisfied(batteryNotLow); + if (previous != batteryNotLow) { + reportChange = true; + } + } + if (stablePower || batteryNotLow) { + // If one of our conditions has been satisfied, always schedule any newly ready jobs. + mStateChangedListener.onRunJobNow(null); + } else if (reportChange) { + // Otherwise, just let the job scheduler know the state has changed and take care of it + // as it thinks is best. + mStateChangedListener.onControllerStateChanged(); + } + } + + public final class ChargingTracker extends BroadcastReceiver { + /** + * Track whether we're "charging", where charging means that we're ready to commit to + * doing work. + */ + private boolean mCharging; + /** Keep track of whether the battery is charged enough that we want to do work. */ + private boolean mBatteryHealthy; + /** Sequence number of last broadcast. */ + private int mLastBatterySeq = -1; + + private BroadcastReceiver mMonitor; + + public ChargingTracker() { + } + + public void startTracking() { + IntentFilter filter = new IntentFilter(); + + // Battery health. + filter.addAction(Intent.ACTION_BATTERY_LOW); + filter.addAction(Intent.ACTION_BATTERY_OKAY); + // Charging/not charging. + filter.addAction(BatteryManager.ACTION_CHARGING); + filter.addAction(BatteryManager.ACTION_DISCHARGING); + mContext.registerReceiver(this, filter); + + // Initialise tracker state. + BatteryManagerInternal batteryManagerInternal = + LocalServices.getService(BatteryManagerInternal.class); + mBatteryHealthy = !batteryManagerInternal.getBatteryLevelLow(); + mCharging = batteryManagerInternal.isPowered(BatteryManager.BATTERY_PLUGGED_ANY); + } + + public void setMonitorBatteryLocked(boolean enabled) { + if (enabled) { + if (mMonitor == null) { + mMonitor = new BroadcastReceiver() { + @Override public void onReceive(Context context, Intent intent) { + ChargingTracker.this.onReceive(context, intent); + } + }; + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_BATTERY_CHANGED); + mContext.registerReceiver(mMonitor, filter); + } + } else { + if (mMonitor != null) { + mContext.unregisterReceiver(mMonitor); + mMonitor = null; + } + } + } + + public boolean isOnStablePower() { + return mCharging && mBatteryHealthy; + } + + public boolean isBatteryNotLow() { + return mBatteryHealthy; + } + + public boolean isMonitoring() { + return mMonitor != null; + } + + public int getSeq() { + return mLastBatterySeq; + } + + @Override + public void onReceive(Context context, Intent intent) { + onReceiveInternal(intent); + } + + @VisibleForTesting + public void onReceiveInternal(Intent intent) { + synchronized (mLock) { + final String action = intent.getAction(); + if (Intent.ACTION_BATTERY_LOW.equals(action)) { + if (DEBUG) { + Slog.d(TAG, "Battery life too low to do work. @ " + + sElapsedRealtimeClock.millis()); + } + // If we get this action, the battery is discharging => it isn't plugged in so + // there's no work to cancel. We track this variable for the case where it is + // charging, but hasn't been for long enough to be healthy. + mBatteryHealthy = false; + maybeReportNewChargingStateLocked(); + } else if (Intent.ACTION_BATTERY_OKAY.equals(action)) { + if (DEBUG) { + Slog.d(TAG, "Battery life healthy enough to do work. @ " + + sElapsedRealtimeClock.millis()); + } + mBatteryHealthy = true; + maybeReportNewChargingStateLocked(); + } else if (BatteryManager.ACTION_CHARGING.equals(action)) { + if (DEBUG) { + Slog.d(TAG, "Received charging intent, fired @ " + + sElapsedRealtimeClock.millis()); + } + mCharging = true; + maybeReportNewChargingStateLocked(); + } else if (BatteryManager.ACTION_DISCHARGING.equals(action)) { + if (DEBUG) { + Slog.d(TAG, "Disconnected from power."); + } + mCharging = false; + maybeReportNewChargingStateLocked(); + } + mLastBatterySeq = intent.getIntExtra(BatteryManager.EXTRA_SEQUENCE, + mLastBatterySeq); + } + } + } + + @Override + public void dumpControllerStateLocked(IndentingPrintWriter pw, + Predicate<JobStatus> predicate) { + pw.println("Stable power: " + mChargeTracker.isOnStablePower()); + pw.println("Not low: " + mChargeTracker.isBatteryNotLow()); + + if (mChargeTracker.isMonitoring()) { + pw.print("MONITORING: seq="); + pw.println(mChargeTracker.getSeq()); + } + pw.println(); + + for (int i = 0; i < mTrackedTasks.size(); i++) { + final JobStatus js = mTrackedTasks.valueAt(i); + if (!predicate.test(js)) { + continue; + } + pw.print("#"); + js.printUniqueId(pw); + pw.print(" from "); + UserHandle.formatUid(pw, js.getSourceUid()); + pw.println(); + } + } + + @Override + public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, + Predicate<JobStatus> predicate) { + final long token = proto.start(fieldId); + final long mToken = proto.start(StateControllerProto.BATTERY); + + proto.write(StateControllerProto.BatteryController.IS_ON_STABLE_POWER, + mChargeTracker.isOnStablePower()); + proto.write(StateControllerProto.BatteryController.IS_BATTERY_NOT_LOW, + mChargeTracker.isBatteryNotLow()); + + proto.write(StateControllerProto.BatteryController.IS_MONITORING, + mChargeTracker.isMonitoring()); + proto.write(StateControllerProto.BatteryController.LAST_BROADCAST_SEQUENCE_NUMBER, + mChargeTracker.getSeq()); + + for (int i = 0; i < mTrackedTasks.size(); i++) { + final JobStatus js = mTrackedTasks.valueAt(i); + if (!predicate.test(js)) { + continue; + } + final long jsToken = proto.start(StateControllerProto.BatteryController.TRACKED_JOBS); + js.writeToShortProto(proto, StateControllerProto.BatteryController.TrackedJob.INFO); + proto.write(StateControllerProto.BatteryController.TrackedJob.SOURCE_UID, + js.getSourceUid()); + proto.end(jsToken); + } + + proto.end(mToken); + proto.end(token); + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java new file mode 100644 index 000000000000..43e8dfb6bab6 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java @@ -0,0 +1,672 @@ +/* + * Copyright (C) 2014 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 static android.net.NetworkCapabilities.LINK_BANDWIDTH_UNSPECIFIED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; + +import android.app.job.JobInfo; +import android.net.ConnectivityManager; +import android.net.ConnectivityManager.NetworkCallback; +import android.net.INetworkPolicyListener; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.net.NetworkPolicyManager; +import android.net.NetworkRequest; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.UserHandle; +import android.text.format.DateUtils; +import android.util.ArraySet; +import android.util.DataUnit; +import android.util.Log; +import android.util.Slog; +import android.util.SparseArray; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.IndentingPrintWriter; +import com.android.server.LocalServices; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.JobSchedulerService.Constants; +import com.android.server.job.StateControllerProto; +import com.android.server.net.NetworkPolicyManagerInternal; + +import java.util.Objects; +import java.util.function.Predicate; + +/** + * Handles changes in connectivity. + * <p> + * Each app can have a different default networks or different connectivity + * status due to user-requested network policies, so we need to check + * constraints on a per-UID basis. + * + * Test: atest com.android.server.job.controllers.ConnectivityControllerTest + */ +public final class ConnectivityController extends StateController implements + ConnectivityManager.OnNetworkActiveListener { + private static final String TAG = "JobScheduler.Connectivity"; + private static final boolean DEBUG = JobSchedulerService.DEBUG + || Log.isLoggable(TAG, Log.DEBUG); + + private final ConnectivityManager mConnManager; + private final NetworkPolicyManager mNetPolicyManager; + private final NetworkPolicyManagerInternal mNetPolicyManagerInternal; + + /** List of tracked jobs keyed by source UID. */ + @GuardedBy("mLock") + private final SparseArray<ArraySet<JobStatus>> mTrackedJobs = new SparseArray<>(); + + /** + * Keep track of all the UID's jobs that the controller has requested that NetworkPolicyManager + * grant an exception to in the app standby chain. + */ + @GuardedBy("mLock") + private final SparseArray<ArraySet<JobStatus>> mRequestedWhitelistJobs = new SparseArray<>(); + + /** List of currently available networks. */ + @GuardedBy("mLock") + private final ArraySet<Network> mAvailableNetworks = new ArraySet<>(); + + private static final int MSG_DATA_SAVER_TOGGLED = 0; + private static final int MSG_UID_RULES_CHANGES = 1; + private static final int MSG_REEVALUATE_JOBS = 2; + + private final Handler mHandler; + + public ConnectivityController(JobSchedulerService service) { + super(service); + mHandler = new CcHandler(mContext.getMainLooper()); + + mConnManager = mContext.getSystemService(ConnectivityManager.class); + mNetPolicyManager = mContext.getSystemService(NetworkPolicyManager.class); + mNetPolicyManagerInternal = LocalServices.getService(NetworkPolicyManagerInternal.class); + + // We're interested in all network changes; internally we match these + // network changes against the active network for each UID with jobs. + final NetworkRequest request = new NetworkRequest.Builder().clearCapabilities().build(); + mConnManager.registerNetworkCallback(request, mNetworkCallback); + + mNetPolicyManager.registerListener(mNetPolicyListener); + } + + @GuardedBy("mLock") + @Override + public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) { + if (jobStatus.hasConnectivityConstraint()) { + updateConstraintsSatisfied(jobStatus); + ArraySet<JobStatus> jobs = mTrackedJobs.get(jobStatus.getSourceUid()); + if (jobs == null) { + jobs = new ArraySet<>(); + mTrackedJobs.put(jobStatus.getSourceUid(), jobs); + } + jobs.add(jobStatus); + jobStatus.setTrackingController(JobStatus.TRACKING_CONNECTIVITY); + } + } + + @GuardedBy("mLock") + @Override + public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, + boolean forUpdate) { + if (jobStatus.clearTrackingController(JobStatus.TRACKING_CONNECTIVITY)) { + ArraySet<JobStatus> jobs = mTrackedJobs.get(jobStatus.getSourceUid()); + if (jobs != null) { + jobs.remove(jobStatus); + } + maybeRevokeStandbyExceptionLocked(jobStatus); + } + } + + /** + * Returns true if the job's requested network is available. This DOES NOT necesarilly mean + * that the UID has been granted access to the network. + */ + public boolean isNetworkAvailable(JobStatus job) { + synchronized (mLock) { + for (int i = 0; i < mAvailableNetworks.size(); ++i) { + final Network network = mAvailableNetworks.valueAt(i); + final NetworkCapabilities capabilities = mConnManager.getNetworkCapabilities( + network); + final boolean satisfied = isSatisfied(job, network, capabilities, mConstants); + if (DEBUG) { + Slog.v(TAG, "isNetworkAvailable(" + job + ") with network " + network + + " and capabilities " + capabilities + ". Satisfied=" + satisfied); + } + if (satisfied) { + return true; + } + } + return false; + } + } + + /** + * Request that NetworkPolicyManager grant an exception to the uid from its standby policy + * chain. + */ + @VisibleForTesting + @GuardedBy("mLock") + void requestStandbyExceptionLocked(JobStatus job) { + final int uid = job.getSourceUid(); + // Need to call this before adding the job. + final boolean isExceptionRequested = isStandbyExceptionRequestedLocked(uid); + ArraySet<JobStatus> jobs = mRequestedWhitelistJobs.get(uid); + if (jobs == null) { + jobs = new ArraySet<JobStatus>(); + mRequestedWhitelistJobs.put(uid, jobs); + } + if (!jobs.add(job) || isExceptionRequested) { + if (DEBUG) { + Slog.i(TAG, "requestStandbyExceptionLocked found exception already requested."); + } + return; + } + if (DEBUG) Slog.i(TAG, "Requesting standby exception for UID: " + uid); + mNetPolicyManagerInternal.setAppIdleWhitelist(uid, true); + } + + /** Returns whether a standby exception has been requested for the UID. */ + @VisibleForTesting + @GuardedBy("mLock") + boolean isStandbyExceptionRequestedLocked(final int uid) { + ArraySet jobs = mRequestedWhitelistJobs.get(uid); + return jobs != null && jobs.size() > 0; + } + + @VisibleForTesting + @GuardedBy("mLock") + boolean wouldBeReadyWithConnectivityLocked(JobStatus jobStatus) { + final boolean networkAvailable = isNetworkAvailable(jobStatus); + if (DEBUG) { + Slog.v(TAG, "wouldBeReadyWithConnectivityLocked: " + jobStatus.toShortString() + + " networkAvailable=" + networkAvailable); + } + // If the network isn't available, then requesting an exception won't help. + + return networkAvailable && wouldBeReadyWithConstraintLocked(jobStatus, + JobStatus.CONSTRAINT_CONNECTIVITY); + } + + /** + * Tell NetworkPolicyManager not to block a UID's network connection if that's the only + * thing stopping a job from running. + */ + @GuardedBy("mLock") + @Override + public void evaluateStateLocked(JobStatus jobStatus) { + if (!jobStatus.hasConnectivityConstraint()) { + return; + } + + // Always check the full job readiness stat in case the component has been disabled. + if (wouldBeReadyWithConnectivityLocked(jobStatus)) { + if (DEBUG) { + Slog.i(TAG, "evaluateStateLocked finds job " + jobStatus + " would be ready."); + } + requestStandbyExceptionLocked(jobStatus); + } else { + if (DEBUG) { + Slog.i(TAG, "evaluateStateLocked finds job " + jobStatus + " would not be ready."); + } + maybeRevokeStandbyExceptionLocked(jobStatus); + } + } + + @GuardedBy("mLock") + @Override + public void reevaluateStateLocked(final int uid) { + // Check if we still need a connectivity exception in case the JobService was disabled. + ArraySet<JobStatus> jobs = mTrackedJobs.get(uid); + if (jobs == null) { + return; + } + for (int i = jobs.size() - 1; i >= 0; i--) { + evaluateStateLocked(jobs.valueAt(i)); + } + } + + /** Cancel the requested standby exception if none of the jobs would be ready to run anyway. */ + @VisibleForTesting + @GuardedBy("mLock") + void maybeRevokeStandbyExceptionLocked(final JobStatus job) { + final int uid = job.getSourceUid(); + if (!isStandbyExceptionRequestedLocked(uid)) { + return; + } + ArraySet<JobStatus> jobs = mRequestedWhitelistJobs.get(uid); + if (jobs == null) { + Slog.wtf(TAG, + "maybeRevokeStandbyExceptionLocked found null jobs array even though a " + + "standby exception has been requested."); + return; + } + if (!jobs.remove(job) || jobs.size() > 0) { + if (DEBUG) { + Slog.i(TAG, + "maybeRevokeStandbyExceptionLocked not revoking because there are still " + + jobs.size() + " jobs left."); + } + return; + } + // No more jobs that need an exception. + revokeStandbyExceptionLocked(uid); + } + + /** + * Tell NetworkPolicyManager to revoke any exception it granted from its standby policy chain + * for the uid. + */ + @GuardedBy("mLock") + private void revokeStandbyExceptionLocked(final int uid) { + if (DEBUG) Slog.i(TAG, "Revoking standby exception for UID: " + uid); + mNetPolicyManagerInternal.setAppIdleWhitelist(uid, false); + mRequestedWhitelistJobs.remove(uid); + } + + @GuardedBy("mLock") + @Override + public void onAppRemovedLocked(String pkgName, int uid) { + mTrackedJobs.delete(uid); + } + + /** + * Test to see if running the given job on the given network is insane. + * <p> + * For example, if a job is trying to send 10MB over a 128Kbps EDGE + * connection, it would take 10.4 minutes, and has no chance of succeeding + * before the job times out, so we'd be insane to try running it. + */ + private boolean isInsane(JobStatus jobStatus, Network network, + NetworkCapabilities capabilities, Constants constants) { + final long maxJobExecutionTimeMs = mService.getMaxJobExecutionTimeMs(jobStatus); + + final long downloadBytes = jobStatus.getEstimatedNetworkDownloadBytes(); + if (downloadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) { + final long bandwidth = capabilities.getLinkDownstreamBandwidthKbps(); + // If we don't know the bandwidth, all we can do is hope the job finishes in time. + if (bandwidth != LINK_BANDWIDTH_UNSPECIFIED) { + // Divide by 8 to convert bits to bytes. + final long estimatedMillis = ((downloadBytes * DateUtils.SECOND_IN_MILLIS) + / (DataUnit.KIBIBYTES.toBytes(bandwidth) / 8)); + if (estimatedMillis > maxJobExecutionTimeMs) { + // If we'd never finish before the timeout, we'd be insane! + Slog.w(TAG, "Estimated " + downloadBytes + " download bytes over " + bandwidth + + " kbps network would take " + estimatedMillis + "ms and job has " + + maxJobExecutionTimeMs + "ms to run; that's insane!"); + return true; + } + } + } + + final long uploadBytes = jobStatus.getEstimatedNetworkUploadBytes(); + if (uploadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) { + final long bandwidth = capabilities.getLinkUpstreamBandwidthKbps(); + // If we don't know the bandwidth, all we can do is hope the job finishes in time. + if (bandwidth != LINK_BANDWIDTH_UNSPECIFIED) { + // Divide by 8 to convert bits to bytes. + final long estimatedMillis = ((uploadBytes * DateUtils.SECOND_IN_MILLIS) + / (DataUnit.KIBIBYTES.toBytes(bandwidth) / 8)); + if (estimatedMillis > maxJobExecutionTimeMs) { + // If we'd never finish before the timeout, we'd be insane! + Slog.w(TAG, "Estimated " + uploadBytes + " upload bytes over " + bandwidth + + " kbps network would take " + estimatedMillis + "ms and job has " + + maxJobExecutionTimeMs + "ms to run; that's insane!"); + return true; + } + } + } + + return false; + } + + private static boolean isCongestionDelayed(JobStatus jobStatus, Network network, + NetworkCapabilities capabilities, Constants constants) { + // If network is congested, and job is less than 50% through the + // developer-requested window, then we're okay delaying the job. + if (!capabilities.hasCapability(NET_CAPABILITY_NOT_CONGESTED)) { + return jobStatus.getFractionRunTime() < constants.CONN_CONGESTION_DELAY_FRAC; + } else { + return false; + } + } + + private static boolean isStrictSatisfied(JobStatus jobStatus, Network network, + NetworkCapabilities capabilities, Constants constants) { + return jobStatus.getJob().getRequiredNetwork().networkCapabilities + .satisfiedByNetworkCapabilities(capabilities); + } + + private static boolean isRelaxedSatisfied(JobStatus jobStatus, Network network, + NetworkCapabilities capabilities, Constants constants) { + // Only consider doing this for prefetching jobs + if (!jobStatus.getJob().isPrefetch()) { + return false; + } + + // See if we match after relaxing any unmetered request + final NetworkCapabilities relaxed = new NetworkCapabilities( + jobStatus.getJob().getRequiredNetwork().networkCapabilities) + .removeCapability(NET_CAPABILITY_NOT_METERED); + if (relaxed.satisfiedByNetworkCapabilities(capabilities)) { + // TODO: treat this as "maybe" response; need to check quotas + return jobStatus.getFractionRunTime() > constants.CONN_PREFETCH_RELAX_FRAC; + } else { + return false; + } + } + + @VisibleForTesting + boolean isSatisfied(JobStatus jobStatus, Network network, + NetworkCapabilities capabilities, Constants constants) { + // Zeroth, we gotta have a network to think about being satisfied + if (network == null || capabilities == null) return false; + + // First, are we insane? + if (isInsane(jobStatus, network, capabilities, constants)) return false; + + // Second, is the network congested? + if (isCongestionDelayed(jobStatus, network, capabilities, constants)) return false; + + // Third, is the network a strict match? + if (isStrictSatisfied(jobStatus, network, capabilities, constants)) return true; + + // Third, is the network a relaxed match? + if (isRelaxedSatisfied(jobStatus, network, capabilities, constants)) return true; + + return false; + } + + private boolean updateConstraintsSatisfied(JobStatus jobStatus) { + final Network network = mConnManager.getActiveNetworkForUid(jobStatus.getSourceUid()); + final NetworkCapabilities capabilities = mConnManager.getNetworkCapabilities(network); + return updateConstraintsSatisfied(jobStatus, network, capabilities); + } + + private boolean updateConstraintsSatisfied(JobStatus jobStatus, Network network, + NetworkCapabilities capabilities) { + // TODO: consider matching against non-active networks + + final boolean ignoreBlocked = (jobStatus.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0; + final NetworkInfo info = mConnManager.getNetworkInfoForUid(network, + jobStatus.getSourceUid(), ignoreBlocked); + + final boolean connected = (info != null) && info.isConnected(); + final boolean satisfied = isSatisfied(jobStatus, network, capabilities, mConstants); + + final boolean changed = jobStatus + .setConnectivityConstraintSatisfied(connected && satisfied); + + // Pass along the evaluated network for job to use; prevents race + // conditions as default routes change over time, and opens the door to + // using non-default routes. + jobStatus.network = network; + + if (DEBUG) { + Slog.i(TAG, "Connectivity " + (changed ? "CHANGED" : "unchanged") + + " for " + jobStatus + ": connected=" + connected + + " satisfied=" + satisfied); + } + return changed; + } + + /** + * Update any jobs tracked by this controller that match given filters. + * + * @param filterUid only update jobs belonging to this UID, or {@code -1} to + * update all tracked jobs. + * @param filterNetwork only update jobs that would use this + * {@link Network}, or {@code null} to update all tracked jobs. + */ + private void updateTrackedJobs(int filterUid, Network filterNetwork) { + synchronized (mLock) { + // Since this is a really hot codepath, temporarily cache any + // answers that we get from ConnectivityManager. + final SparseArray<NetworkCapabilities> networkToCapabilities = new SparseArray<>(); + + boolean changed = false; + if (filterUid == -1) { + for (int i = mTrackedJobs.size() - 1; i >= 0; i--) { + changed |= updateTrackedJobsLocked(mTrackedJobs.valueAt(i), + filterNetwork, networkToCapabilities); + } + } else { + changed = updateTrackedJobsLocked(mTrackedJobs.get(filterUid), + filterNetwork, networkToCapabilities); + } + if (changed) { + mStateChangedListener.onControllerStateChanged(); + } + } + } + + private boolean updateTrackedJobsLocked(ArraySet<JobStatus> jobs, Network filterNetwork, + SparseArray<NetworkCapabilities> networkToCapabilities) { + if (jobs == null || jobs.size() == 0) { + return false; + } + + final Network network = mConnManager.getActiveNetworkForUid(jobs.valueAt(0).getSourceUid()); + final int netId = network != null ? network.netId : -1; + NetworkCapabilities capabilities = networkToCapabilities.get(netId); + if (capabilities == null) { + capabilities = mConnManager.getNetworkCapabilities(network); + networkToCapabilities.put(netId, capabilities); + } + final boolean networkMatch = (filterNetwork == null + || Objects.equals(filterNetwork, network)); + + boolean changed = false; + for (int i = jobs.size() - 1; i >= 0; i--) { + final JobStatus js = jobs.valueAt(i); + + // Update either when we have a network match, or when the + // job hasn't yet been evaluated against the currently + // active network; typically when we just lost a network. + if (networkMatch || !Objects.equals(js.network, network)) { + changed |= updateConstraintsSatisfied(js, network, capabilities); + } + } + return changed; + } + + /** + * We know the network has just come up. We want to run any jobs that are ready. + */ + @Override + public void onNetworkActive() { + synchronized (mLock) { + for (int i = mTrackedJobs.size()-1; i >= 0; i--) { + final ArraySet<JobStatus> jobs = mTrackedJobs.valueAt(i); + for (int j = jobs.size() - 1; j >= 0; j--) { + final JobStatus js = jobs.valueAt(j); + if (js.isReady()) { + if (DEBUG) { + Slog.d(TAG, "Running " + js + " due to network activity."); + } + mStateChangedListener.onRunJobNow(js); + } + } + } + } + } + + private final NetworkCallback mNetworkCallback = new NetworkCallback() { + @Override + public void onAvailable(Network network) { + if (DEBUG) Slog.v(TAG, "onAvailable: " + network); + synchronized (mLock) { + mAvailableNetworks.add(network); + } + } + + @Override + public void onCapabilitiesChanged(Network network, NetworkCapabilities capabilities) { + if (DEBUG) { + Slog.v(TAG, "onCapabilitiesChanged: " + network); + } + updateTrackedJobs(-1, network); + } + + @Override + public void onLost(Network network) { + if (DEBUG) { + Slog.v(TAG, "onLost: " + network); + } + synchronized (mLock) { + mAvailableNetworks.remove(network); + } + updateTrackedJobs(-1, network); + } + }; + + private final INetworkPolicyListener mNetPolicyListener = new NetworkPolicyManager.Listener() { + @Override + public void onRestrictBackgroundChanged(boolean restrictBackground) { + if (DEBUG) { + Slog.v(TAG, "onRestrictBackgroundChanged: " + restrictBackground); + } + mHandler.obtainMessage(MSG_DATA_SAVER_TOGGLED).sendToTarget(); + } + + @Override + public void onUidRulesChanged(int uid, int uidRules) { + if (DEBUG) { + Slog.v(TAG, "onUidRulesChanged: " + uid); + } + mHandler.obtainMessage(MSG_UID_RULES_CHANGES, uid, 0).sendToTarget(); + } + }; + + private class CcHandler extends Handler { + CcHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + synchronized (mLock) { + switch (msg.what) { + case MSG_DATA_SAVER_TOGGLED: + updateTrackedJobs(-1, null); + break; + case MSG_UID_RULES_CHANGES: + updateTrackedJobs(msg.arg1, null); + break; + case MSG_REEVALUATE_JOBS: + updateTrackedJobs(-1, null); + break; + } + } + } + }; + + @GuardedBy("mLock") + @Override + public void dumpControllerStateLocked(IndentingPrintWriter pw, + Predicate<JobStatus> predicate) { + + if (mRequestedWhitelistJobs.size() > 0) { + pw.print("Requested standby exceptions:"); + for (int i = 0; i < mRequestedWhitelistJobs.size(); i++) { + pw.print(" "); + pw.print(mRequestedWhitelistJobs.keyAt(i)); + pw.print(" ("); + pw.print(mRequestedWhitelistJobs.valueAt(i).size()); + pw.print(" jobs)"); + } + pw.println(); + } + if (mAvailableNetworks.size() > 0) { + pw.println("Available networks:"); + pw.increaseIndent(); + for (int i = 0; i < mAvailableNetworks.size(); i++) { + pw.println(mAvailableNetworks.valueAt(i)); + } + pw.decreaseIndent(); + } else { + pw.println("No available networks"); + } + for (int i = 0; i < mTrackedJobs.size(); i++) { + final ArraySet<JobStatus> jobs = mTrackedJobs.valueAt(i); + for (int j = 0; j < jobs.size(); j++) { + final JobStatus js = jobs.valueAt(j); + if (!predicate.test(js)) { + continue; + } + pw.print("#"); + js.printUniqueId(pw); + pw.print(" from "); + UserHandle.formatUid(pw, js.getSourceUid()); + pw.print(": "); + pw.print(js.getJob().getRequiredNetwork()); + pw.println(); + } + } + } + + @GuardedBy("mLock") + @Override + public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, + Predicate<JobStatus> predicate) { + final long token = proto.start(fieldId); + final long mToken = proto.start(StateControllerProto.CONNECTIVITY); + + for (int i = 0; i < mRequestedWhitelistJobs.size(); i++) { + proto.write( + StateControllerProto.ConnectivityController.REQUESTED_STANDBY_EXCEPTION_UIDS, + mRequestedWhitelistJobs.keyAt(i)); + } + for (int i = 0; i < mAvailableNetworks.size(); i++) { + Network network = mAvailableNetworks.valueAt(i); + if (network != null) { + network.writeToProto(proto, + StateControllerProto.ConnectivityController.AVAILABLE_NETWORKS); + } + } + for (int i = 0; i < mTrackedJobs.size(); i++) { + final ArraySet<JobStatus> jobs = mTrackedJobs.valueAt(i); + for (int j = 0; j < jobs.size(); j++) { + final JobStatus js = jobs.valueAt(j); + if (!predicate.test(js)) { + continue; + } + final long jsToken = proto.start( + StateControllerProto.ConnectivityController.TRACKED_JOBS); + js.writeToShortProto(proto, + StateControllerProto.ConnectivityController.TrackedJob.INFO); + proto.write(StateControllerProto.ConnectivityController.TrackedJob.SOURCE_UID, + js.getSourceUid()); + NetworkRequest rn = js.getJob().getRequiredNetwork(); + if (rn != null) { + rn.writeToProto(proto, + StateControllerProto.ConnectivityController.TrackedJob + .REQUIRED_NETWORK); + } + proto.end(jsToken); + } + } + + proto.end(mToken); + proto.end(token); + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/ContentObserverController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/ContentObserverController.java new file mode 100644 index 000000000000..a775cf5a671c --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ContentObserverController.java @@ -0,0 +1,544 @@ +/* + * 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.annotation.UserIdInt; +import android.app.job.JobInfo; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.os.UserHandle; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Log; +import android.util.Slog; +import android.util.SparseArray; +import android.util.TimeUtils; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.util.IndentingPrintWriter; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.StateControllerProto; +import com.android.server.job.StateControllerProto.ContentObserverController.Observer.TriggerContentData; + +import java.util.ArrayList; +import java.util.function.Predicate; + +/** + * Controller for monitoring changes to content URIs through a ContentObserver. + */ +public final class ContentObserverController extends StateController { + private static final String TAG = "JobScheduler.ContentObserver"; + private static final boolean DEBUG = JobSchedulerService.DEBUG + || Log.isLoggable(TAG, Log.DEBUG); + + /** + * Maximum number of changing URIs we will batch together to report. + * XXX Should be smarter about this, restricting it by the maximum number + * of characters we will retain. + */ + private static final int MAX_URIS_REPORTED = 50; + + /** + * At this point we consider it urgent to schedule the job ASAP. + */ + private static final int URIS_URGENT_THRESHOLD = 40; + + final private ArraySet<JobStatus> mTrackedTasks = new ArraySet<>(); + /** + * Per-userid {@link JobInfo.TriggerContentUri} keyed ContentObserver cache. + */ + final SparseArray<ArrayMap<JobInfo.TriggerContentUri, ObserverInstance>> mObservers = + new SparseArray<>(); + final Handler mHandler; + + public ContentObserverController(JobSchedulerService service) { + super(service); + mHandler = new Handler(mContext.getMainLooper()); + } + + @Override + public void maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob) { + if (taskStatus.hasContentTriggerConstraint()) { + if (taskStatus.contentObserverJobInstance == null) { + taskStatus.contentObserverJobInstance = new JobInstance(taskStatus); + } + if (DEBUG) { + Slog.i(TAG, "Tracking content-trigger job " + taskStatus); + } + mTrackedTasks.add(taskStatus); + taskStatus.setTrackingController(JobStatus.TRACKING_CONTENT); + boolean havePendingUris = false; + // If there is a previous job associated with the new job, propagate over + // any pending content URI trigger reports. + if (taskStatus.contentObserverJobInstance.mChangedAuthorities != null) { + havePendingUris = true; + } + // If we have previously reported changed authorities/uris, then we failed + // to complete the job with them so will re-record them to report again. + if (taskStatus.changedAuthorities != null) { + havePendingUris = true; + if (taskStatus.contentObserverJobInstance.mChangedAuthorities == null) { + taskStatus.contentObserverJobInstance.mChangedAuthorities + = new ArraySet<>(); + } + for (String auth : taskStatus.changedAuthorities) { + taskStatus.contentObserverJobInstance.mChangedAuthorities.add(auth); + } + if (taskStatus.changedUris != null) { + if (taskStatus.contentObserverJobInstance.mChangedUris == null) { + taskStatus.contentObserverJobInstance.mChangedUris = new ArraySet<>(); + } + for (Uri uri : taskStatus.changedUris) { + taskStatus.contentObserverJobInstance.mChangedUris.add(uri); + } + } + taskStatus.changedAuthorities = null; + taskStatus.changedUris = null; + } + taskStatus.changedAuthorities = null; + taskStatus.changedUris = null; + taskStatus.setContentTriggerConstraintSatisfied(havePendingUris); + } + if (lastJob != null && lastJob.contentObserverJobInstance != null) { + // And now we can detach the instance state from the last job. + lastJob.contentObserverJobInstance.detachLocked(); + lastJob.contentObserverJobInstance = null; + } + } + + @Override + public void prepareForExecutionLocked(JobStatus taskStatus) { + if (taskStatus.hasContentTriggerConstraint()) { + if (taskStatus.contentObserverJobInstance != null) { + taskStatus.changedUris = taskStatus.contentObserverJobInstance.mChangedUris; + taskStatus.changedAuthorities + = taskStatus.contentObserverJobInstance.mChangedAuthorities; + taskStatus.contentObserverJobInstance.mChangedUris = null; + taskStatus.contentObserverJobInstance.mChangedAuthorities = null; + } + } + } + + @Override + public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, + boolean forUpdate) { + if (taskStatus.clearTrackingController(JobStatus.TRACKING_CONTENT)) { + mTrackedTasks.remove(taskStatus); + if (taskStatus.contentObserverJobInstance != null) { + taskStatus.contentObserverJobInstance.unscheduleLocked(); + if (incomingJob != null) { + if (taskStatus.contentObserverJobInstance != null + && taskStatus.contentObserverJobInstance.mChangedAuthorities != null) { + // We are stopping this job, but it is going to be replaced by this given + // incoming job. We want to propagate our state over to it, so we don't + // lose any content changes that had happened since the last one started. + // If there is a previous job associated with the new job, propagate over + // any pending content URI trigger reports. + if (incomingJob.contentObserverJobInstance == null) { + incomingJob.contentObserverJobInstance = new JobInstance(incomingJob); + } + incomingJob.contentObserverJobInstance.mChangedAuthorities + = taskStatus.contentObserverJobInstance.mChangedAuthorities; + incomingJob.contentObserverJobInstance.mChangedUris + = taskStatus.contentObserverJobInstance.mChangedUris; + taskStatus.contentObserverJobInstance.mChangedAuthorities = null; + taskStatus.contentObserverJobInstance.mChangedUris = null; + } + // We won't detach the content observers here, because we want to + // allow them to continue monitoring so we don't miss anything... and + // since we are giving an incomingJob here, we know this will be + // immediately followed by a start tracking of that job. + } else { + // But here there is no incomingJob, so nothing coming up, so time to detach. + taskStatus.contentObserverJobInstance.detachLocked(); + taskStatus.contentObserverJobInstance = null; + } + } + if (DEBUG) { + Slog.i(TAG, "No longer tracking job " + taskStatus); + } + } + } + + @Override + public void rescheduleForFailureLocked(JobStatus newJob, JobStatus failureToReschedule) { + if (failureToReschedule.hasContentTriggerConstraint() + && newJob.hasContentTriggerConstraint()) { + // Our job has failed, and we are scheduling a new job for it. + // Copy the last reported content changes in to the new job, so when + // we schedule the new one we will pick them up and report them again. + newJob.changedAuthorities = failureToReschedule.changedAuthorities; + newJob.changedUris = failureToReschedule.changedUris; + } + } + + final class ObserverInstance extends ContentObserver { + final JobInfo.TriggerContentUri mUri; + final @UserIdInt int mUserId; + final ArraySet<JobInstance> mJobs = new ArraySet<>(); + + public ObserverInstance(Handler handler, JobInfo.TriggerContentUri uri, + @UserIdInt int userId) { + super(handler); + mUri = uri; + mUserId = userId; + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + if (DEBUG) { + Slog.i(TAG, "onChange(self=" + selfChange + ") for " + uri + + " when mUri=" + mUri + " mUserId=" + mUserId); + } + synchronized (mLock) { + final int N = mJobs.size(); + for (int i=0; i<N; i++) { + JobInstance inst = mJobs.valueAt(i); + if (inst.mChangedUris == null) { + inst.mChangedUris = new ArraySet<>(); + } + if (inst.mChangedUris.size() < MAX_URIS_REPORTED) { + inst.mChangedUris.add(uri); + } + if (inst.mChangedAuthorities == null) { + inst.mChangedAuthorities = new ArraySet<>(); + } + inst.mChangedAuthorities.add(uri.getAuthority()); + inst.scheduleLocked(); + } + } + } + } + + static final class TriggerRunnable implements Runnable { + final JobInstance mInstance; + + TriggerRunnable(JobInstance instance) { + mInstance = instance; + } + + @Override public void run() { + mInstance.trigger(); + } + } + + final class JobInstance { + final ArrayList<ObserverInstance> mMyObservers = new ArrayList<>(); + final JobStatus mJobStatus; + final Runnable mExecuteRunner; + final Runnable mTimeoutRunner; + ArraySet<Uri> mChangedUris; + ArraySet<String> mChangedAuthorities; + + boolean mTriggerPending; + + // This constructor must be called with the master job scheduler lock held. + JobInstance(JobStatus jobStatus) { + mJobStatus = jobStatus; + mExecuteRunner = new TriggerRunnable(this); + mTimeoutRunner = new TriggerRunnable(this); + final JobInfo.TriggerContentUri[] uris = jobStatus.getJob().getTriggerContentUris(); + final int sourceUserId = jobStatus.getSourceUserId(); + ArrayMap<JobInfo.TriggerContentUri, ObserverInstance> observersOfUser = + mObservers.get(sourceUserId); + if (observersOfUser == null) { + observersOfUser = new ArrayMap<>(); + mObservers.put(sourceUserId, observersOfUser); + } + if (uris != null) { + for (JobInfo.TriggerContentUri uri : uris) { + ObserverInstance obs = observersOfUser.get(uri); + if (obs == null) { + obs = new ObserverInstance(mHandler, uri, jobStatus.getSourceUserId()); + observersOfUser.put(uri, obs); + final boolean andDescendants = (uri.getFlags() & + JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS) != 0; + if (DEBUG) { + Slog.v(TAG, "New observer " + obs + " for " + uri.getUri() + + " andDescendants=" + andDescendants + + " sourceUserId=" + sourceUserId); + } + mContext.getContentResolver().registerContentObserver( + uri.getUri(), + andDescendants, + obs, + sourceUserId + ); + } else { + if (DEBUG) { + final boolean andDescendants = (uri.getFlags() & + JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS) != 0; + Slog.v(TAG, "Reusing existing observer " + obs + " for " + uri.getUri() + + " andDescendants=" + andDescendants); + } + } + obs.mJobs.add(this); + mMyObservers.add(obs); + } + } + } + + void trigger() { + boolean reportChange = false; + synchronized (mLock) { + if (mTriggerPending) { + if (mJobStatus.setContentTriggerConstraintSatisfied(true)) { + reportChange = true; + } + unscheduleLocked(); + } + } + // Let the scheduler know that state has changed. This may or may not result in an + // execution. + if (reportChange) { + mStateChangedListener.onControllerStateChanged(); + } + } + + void scheduleLocked() { + if (!mTriggerPending) { + mTriggerPending = true; + mHandler.postDelayed(mTimeoutRunner, mJobStatus.getTriggerContentMaxDelay()); + } + mHandler.removeCallbacks(mExecuteRunner); + if (mChangedUris.size() >= URIS_URGENT_THRESHOLD) { + // If we start getting near the limit, GO NOW! + mHandler.post(mExecuteRunner); + } else { + mHandler.postDelayed(mExecuteRunner, mJobStatus.getTriggerContentUpdateDelay()); + } + } + + void unscheduleLocked() { + if (mTriggerPending) { + mHandler.removeCallbacks(mExecuteRunner); + mHandler.removeCallbacks(mTimeoutRunner); + mTriggerPending = false; + } + } + + void detachLocked() { + final int N = mMyObservers.size(); + for (int i=0; i<N; i++) { + final ObserverInstance obs = mMyObservers.get(i); + obs.mJobs.remove(this); + if (obs.mJobs.size() == 0) { + if (DEBUG) { + Slog.i(TAG, "Unregistering observer " + obs + " for " + obs.mUri.getUri()); + } + mContext.getContentResolver().unregisterContentObserver(obs); + ArrayMap<JobInfo.TriggerContentUri, ObserverInstance> observerOfUser = + mObservers.get(obs.mUserId); + if (observerOfUser != null) { + observerOfUser.remove(obs.mUri); + } + } + } + } + } + + @Override + public void dumpControllerStateLocked(IndentingPrintWriter pw, + Predicate<JobStatus> predicate) { + for (int i = 0; i < mTrackedTasks.size(); i++) { + JobStatus js = mTrackedTasks.valueAt(i); + if (!predicate.test(js)) { + continue; + } + pw.print("#"); + js.printUniqueId(pw); + pw.print(" from "); + UserHandle.formatUid(pw, js.getSourceUid()); + pw.println(); + } + pw.println(); + + int N = mObservers.size(); + if (N > 0) { + pw.println("Observers:"); + pw.increaseIndent(); + for (int userIdx = 0; userIdx < N; userIdx++) { + final int userId = mObservers.keyAt(userIdx); + ArrayMap<JobInfo.TriggerContentUri, ObserverInstance> observersOfUser = + mObservers.get(userId); + int numbOfObserversPerUser = observersOfUser.size(); + for (int observerIdx = 0 ; observerIdx < numbOfObserversPerUser; observerIdx++) { + ObserverInstance obs = observersOfUser.valueAt(observerIdx); + int M = obs.mJobs.size(); + boolean shouldDump = false; + for (int j = 0; j < M; j++) { + JobInstance inst = obs.mJobs.valueAt(j); + if (predicate.test(inst.mJobStatus)) { + shouldDump = true; + break; + } + } + if (!shouldDump) { + continue; + } + JobInfo.TriggerContentUri trigger = observersOfUser.keyAt(observerIdx); + pw.print(trigger.getUri()); + pw.print(" 0x"); + pw.print(Integer.toHexString(trigger.getFlags())); + pw.print(" ("); + pw.print(System.identityHashCode(obs)); + pw.println("):"); + pw.increaseIndent(); + pw.println("Jobs:"); + pw.increaseIndent(); + for (int j = 0; j < M; j++) { + JobInstance inst = obs.mJobs.valueAt(j); + pw.print("#"); + inst.mJobStatus.printUniqueId(pw); + pw.print(" from "); + UserHandle.formatUid(pw, inst.mJobStatus.getSourceUid()); + if (inst.mChangedAuthorities != null) { + pw.println(":"); + pw.increaseIndent(); + if (inst.mTriggerPending) { + pw.print("Trigger pending: update="); + TimeUtils.formatDuration( + inst.mJobStatus.getTriggerContentUpdateDelay(), pw); + pw.print(", max="); + TimeUtils.formatDuration( + inst.mJobStatus.getTriggerContentMaxDelay(), pw); + pw.println(); + } + pw.println("Changed Authorities:"); + for (int k = 0; k < inst.mChangedAuthorities.size(); k++) { + pw.println(inst.mChangedAuthorities.valueAt(k)); + } + if (inst.mChangedUris != null) { + pw.println(" Changed URIs:"); + for (int k = 0; k < inst.mChangedUris.size(); k++) { + pw.println(inst.mChangedUris.valueAt(k)); + } + } + pw.decreaseIndent(); + } else { + pw.println(); + } + } + pw.decreaseIndent(); + pw.decreaseIndent(); + } + } + pw.decreaseIndent(); + } + } + + @Override + public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, + Predicate<JobStatus> predicate) { + final long token = proto.start(fieldId); + final long mToken = proto.start(StateControllerProto.CONTENT_OBSERVER); + + for (int i = 0; i < mTrackedTasks.size(); i++) { + JobStatus js = mTrackedTasks.valueAt(i); + if (!predicate.test(js)) { + continue; + } + final long jsToken = + proto.start(StateControllerProto.ContentObserverController.TRACKED_JOBS); + js.writeToShortProto(proto, + StateControllerProto.ContentObserverController.TrackedJob.INFO); + proto.write(StateControllerProto.ContentObserverController.TrackedJob.SOURCE_UID, + js.getSourceUid()); + proto.end(jsToken); + } + + final int n = mObservers.size(); + for (int userIdx = 0; userIdx < n; userIdx++) { + final long oToken = + proto.start(StateControllerProto.ContentObserverController.OBSERVERS); + final int userId = mObservers.keyAt(userIdx); + + proto.write(StateControllerProto.ContentObserverController.Observer.USER_ID, userId); + + ArrayMap<JobInfo.TriggerContentUri, ObserverInstance> observersOfUser = + mObservers.get(userId); + int numbOfObserversPerUser = observersOfUser.size(); + for (int observerIdx = 0 ; observerIdx < numbOfObserversPerUser; observerIdx++) { + ObserverInstance obs = observersOfUser.valueAt(observerIdx); + int m = obs.mJobs.size(); + boolean shouldDump = false; + for (int j = 0; j < m; j++) { + JobInstance inst = obs.mJobs.valueAt(j); + if (predicate.test(inst.mJobStatus)) { + shouldDump = true; + break; + } + } + if (!shouldDump) { + continue; + } + final long tToken = proto.start( + StateControllerProto.ContentObserverController.Observer.TRIGGERS); + + JobInfo.TriggerContentUri trigger = observersOfUser.keyAt(observerIdx); + Uri u = trigger.getUri(); + if (u != null) { + proto.write(TriggerContentData.URI, u.toString()); + } + proto.write(TriggerContentData.FLAGS, trigger.getFlags()); + + for (int j = 0; j < m; j++) { + final long jToken = proto.start(TriggerContentData.JOBS); + JobInstance inst = obs.mJobs.valueAt(j); + + inst.mJobStatus.writeToShortProto(proto, TriggerContentData.JobInstance.INFO); + proto.write(TriggerContentData.JobInstance.SOURCE_UID, + inst.mJobStatus.getSourceUid()); + + if (inst.mChangedAuthorities == null) { + proto.end(jToken); + continue; + } + if (inst.mTriggerPending) { + proto.write(TriggerContentData.JobInstance.TRIGGER_CONTENT_UPDATE_DELAY_MS, + inst.mJobStatus.getTriggerContentUpdateDelay()); + proto.write(TriggerContentData.JobInstance.TRIGGER_CONTENT_MAX_DELAY_MS, + inst.mJobStatus.getTriggerContentMaxDelay()); + } + for (int k = 0; k < inst.mChangedAuthorities.size(); k++) { + proto.write(TriggerContentData.JobInstance.CHANGED_AUTHORITIES, + inst.mChangedAuthorities.valueAt(k)); + } + if (inst.mChangedUris != null) { + for (int k = 0; k < inst.mChangedUris.size(); k++) { + u = inst.mChangedUris.valueAt(k); + if (u != null) { + proto.write(TriggerContentData.JobInstance.CHANGED_URIS, + u.toString()); + } + } + } + + proto.end(jToken); + } + + proto.end(tToken); + } + + proto.end(oToken); + } + + proto.end(mToken); + proto.end(token); + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java new file mode 100644 index 000000000000..01f5fa62f889 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java @@ -0,0 +1,313 @@ +/* + * 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.Log; +import android.util.Slog; +import android.util.SparseBooleanArray; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.IndentingPrintWriter; +import com.android.server.DeviceIdleInternal; +import com.android.server.LocalServices; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.StateControllerProto; +import com.android.server.job.StateControllerProto.DeviceIdleJobsController.TrackedJob; + +import java.util.Arrays; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * 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 TAG = "JobScheduler.DeviceIdle"; + private static final boolean DEBUG = JobSchedulerService.DEBUG + || Log.isLoggable(TAG, Log.DEBUG); + + private static final long BACKGROUND_JOBS_DELAY = 3000; + + static final int PROCESS_BACKGROUND_JOBS = 1; + + /** + * 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 PowerManager mPowerManager; + private final DeviceIdleInternal 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; + + // 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 (DEBUG) { + Slog.d(TAG, "Got whitelist " + + Arrays.toString(mDeviceIdleWhitelistAppIds)); + } + } + break; + case PowerManager.ACTION_POWER_SAVE_TEMP_WHITELIST_CHANGED: + synchronized (mLock) { + mPowerSaveTempWhitelistAppIds = + mLocalDeviceIdleController.getPowerSaveTempWhitelistAppIds(); + if (DEBUG) { + Slog.d(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; + } + } + }; + + public DeviceIdleJobsController(JobSchedulerService service) { + super(service); + + mHandler = new DeviceIdleJobsDelayHandler(mContext.getMainLooper()); + // Register for device idle mode changes + mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + mLocalDeviceIdleController = + LocalServices.getService(DeviceIdleInternal.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 (DEBUG) Slog.d(TAG, "mDeviceIdleMode=" + mDeviceIdleMode); + if (enabled) { + mHandler.removeMessages(PROCESS_BACKGROUND_JOBS); + mService.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)) { + mService.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 (DEBUG) { + Slog.d(TAG, "uid " + uid + " going " + (active ? "active" : "inactive")); + } + mForegroundUids.put(uid, active); + mDeviceIdleUpdateFunctor.mChanged = false; + mService.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 IndentingPrintWriter pw, + final Predicate<JobStatus> predicate) { + pw.println("Idle mode: " + mDeviceIdleMode); + pw.println(); + + mService.getJobStore().forEachJob(predicate, (jobStatus) -> { + 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, + Predicate<JobStatus> predicate) { + final long token = proto.start(fieldId); + final long mToken = proto.start(StateControllerProto.DEVICE_IDLE); + + proto.write(StateControllerProto.DeviceIdleJobsController.IS_DEVICE_IDLE_MODE, + mDeviceIdleMode); + mService.getJobStore().forEachJob(predicate, (jobStatus) -> { + 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 Consumer<JobStatus> { + boolean mChanged; + + @Override + public void accept(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; + mService.getJobStore().forEachJob(mDeviceIdleUpdateFunctor); + if (mDeviceIdleUpdateFunctor.mChanged) { + mStateChangedListener.onControllerStateChanged(); + } + } + break; + } + } + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java new file mode 100644 index 000000000000..d355715920b6 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2014 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.content.Context; +import android.content.pm.PackageManager; +import android.os.UserHandle; +import android.util.ArraySet; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.util.IndentingPrintWriter; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.StateControllerProto; +import com.android.server.job.controllers.idle.CarIdlenessTracker; +import com.android.server.job.controllers.idle.DeviceIdlenessTracker; +import com.android.server.job.controllers.idle.IdlenessListener; +import com.android.server.job.controllers.idle.IdlenessTracker; + +import java.util.function.Predicate; + +public final class IdleController extends StateController implements IdlenessListener { + private static final String TAG = "JobScheduler.IdleController"; + // Policy: we decide that we're "idle" if the device has been unused / + // screen off or dreaming or wireless charging dock idle for at least this long + final ArraySet<JobStatus> mTrackedTasks = new ArraySet<>(); + IdlenessTracker mIdleTracker; + + public IdleController(JobSchedulerService service) { + super(service); + initIdleStateTracking(mContext); + } + + /** + * StateController interface + */ + @Override + public void maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob) { + if (taskStatus.hasIdleConstraint()) { + mTrackedTasks.add(taskStatus); + taskStatus.setTrackingController(JobStatus.TRACKING_IDLE); + taskStatus.setIdleConstraintSatisfied(mIdleTracker.isIdle()); + } + } + + @Override + public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, + boolean forUpdate) { + if (taskStatus.clearTrackingController(JobStatus.TRACKING_IDLE)) { + mTrackedTasks.remove(taskStatus); + } + } + + /** + * State-change notifications from the idleness tracker + */ + @Override + public void reportNewIdleState(boolean isIdle) { + synchronized (mLock) { + for (int i = mTrackedTasks.size()-1; i >= 0; i--) { + mTrackedTasks.valueAt(i).setIdleConstraintSatisfied(isIdle); + } + } + mStateChangedListener.onControllerStateChanged(); + } + + /** + * Idle state tracking, and messaging with the task manager when + * significant state changes occur + */ + private void initIdleStateTracking(Context ctx) { + final boolean isCar = mContext.getPackageManager().hasSystemFeature( + PackageManager.FEATURE_AUTOMOTIVE); + if (isCar) { + mIdleTracker = new CarIdlenessTracker(); + } else { + mIdleTracker = new DeviceIdlenessTracker(); + } + mIdleTracker.startTracking(ctx, this); + } + + @Override + public void dumpControllerStateLocked(IndentingPrintWriter pw, + Predicate<JobStatus> predicate) { + pw.println("Currently idle: " + mIdleTracker.isIdle()); + pw.println("Idleness tracker:"); mIdleTracker.dump(pw); + pw.println(); + + for (int i = 0; i < mTrackedTasks.size(); i++) { + final JobStatus js = mTrackedTasks.valueAt(i); + if (!predicate.test(js)) { + continue; + } + pw.print("#"); + js.printUniqueId(pw); + pw.print(" from "); + UserHandle.formatUid(pw, js.getSourceUid()); + pw.println(); + } + } + + @Override + public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, + Predicate<JobStatus> predicate) { + final long token = proto.start(fieldId); + final long mToken = proto.start(StateControllerProto.IDLE); + + proto.write(StateControllerProto.IdleController.IS_IDLE, mIdleTracker.isIdle()); + mIdleTracker.dump(proto, StateControllerProto.IdleController.IDLENESS_TRACKER); + + for (int i = 0; i < mTrackedTasks.size(); i++) { + final JobStatus js = mTrackedTasks.valueAt(i); + if (!predicate.test(js)) { + continue; + } + final long jsToken = proto.start(StateControllerProto.IdleController.TRACKED_JOBS); + js.writeToShortProto(proto, StateControllerProto.IdleController.TrackedJob.INFO); + proto.write(StateControllerProto.IdleController.TrackedJob.SOURCE_UID, + js.getSourceUid()); + proto.end(jsToken); + } + + proto.end(mToken); + proto.end(token); + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java new file mode 100644 index 000000000000..adb43141b9c1 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java @@ -0,0 +1,1863 @@ +/* + * Copyright (C) 2014 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 static com.android.server.job.JobSchedulerService.ACTIVE_INDEX; +import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; + +import android.app.AppGlobals; +import android.app.job.JobInfo; +import android.app.job.JobWorkItem; +import android.content.ClipData; +import android.content.ComponentName; +import android.net.Network; +import android.net.Uri; +import android.os.RemoteException; +import android.os.UserHandle; +import android.text.format.TimeMigrationUtils; +import android.util.ArraySet; +import android.util.Pair; +import android.util.Slog; +import android.util.StatsLog; +import android.util.TimeUtils; +import android.util.proto.ProtoOutputStream; + +import com.android.server.LocalServices; +import com.android.server.job.GrantedUriPermissions; +import com.android.server.job.JobSchedulerInternal; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.JobServerProtoEnums; +import com.android.server.job.JobStatusDumpProto; +import com.android.server.job.JobStatusShortInfoProto; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.function.Predicate; + +/** + * Uniquely identifies a job internally. + * Created from the public {@link android.app.job.JobInfo} object when it lands on the scheduler. + * Contains current state of the requirements of the job, as well as a function to evaluate + * whether it's ready to run. + * This object is shared among the various controllers - hence why the different fields are atomic. + * This isn't strictly necessary because each controller is only interested in a specific field, + * and the receivers that are listening for global state change will all run on the main looper, + * but we don't enforce that so this is safer. + * + * Test: atest com.android.server.job.controllers.JobStatusTest + * @hide + */ +public final class JobStatus { + static final String TAG = "JobSchedulerService"; + static final boolean DEBUG = JobSchedulerService.DEBUG; + + public static final long NO_LATEST_RUNTIME = Long.MAX_VALUE; + public static final long NO_EARLIEST_RUNTIME = 0L; + + static final int CONSTRAINT_CHARGING = JobInfo.CONSTRAINT_FLAG_CHARGING; // 1 < 0 + static final int CONSTRAINT_IDLE = JobInfo.CONSTRAINT_FLAG_DEVICE_IDLE; // 1 << 2 + static final int CONSTRAINT_BATTERY_NOT_LOW = JobInfo.CONSTRAINT_FLAG_BATTERY_NOT_LOW; // 1 << 1 + static final int CONSTRAINT_STORAGE_NOT_LOW = JobInfo.CONSTRAINT_FLAG_STORAGE_NOT_LOW; // 1 << 3 + static final int CONSTRAINT_TIMING_DELAY = 1<<31; + static final int CONSTRAINT_DEADLINE = 1<<30; + static final int CONSTRAINT_CONNECTIVITY = 1<<28; + static final int CONSTRAINT_CONTENT_TRIGGER = 1<<26; + static final int CONSTRAINT_DEVICE_NOT_DOZING = 1 << 25; // Implicit constraint + static final int CONSTRAINT_WITHIN_QUOTA = 1 << 24; // Implicit constraint + static final int CONSTRAINT_BACKGROUND_NOT_RESTRICTED = 1 << 22; // Implicit constraint + + /** + * The constraints that we want to log to statsd. + * + * Constraints that can be inferred from other atoms have been excluded to avoid logging too + * much information and to reduce redundancy: + * + * * CONSTRAINT_CHARGING can be inferred with PluggedStateChanged (Atom #32) + * * CONSTRAINT_BATTERY_NOT_LOW can be inferred with BatteryLevelChanged (Atom #30) + * * CONSTRAINT_CONNECTIVITY can be partially inferred with ConnectivityStateChanged + * (Atom #98) and BatterySaverModeStateChanged (Atom #20). + * * CONSTRAINT_DEVICE_NOT_DOZING can be mostly inferred with DeviceIdleModeStateChanged + * (Atom #21) + * * CONSTRAINT_BACKGROUND_NOT_RESTRICTED can be inferred with BatterySaverModeStateChanged + * (Atom #20) + */ + private static final int STATSD_CONSTRAINTS_TO_LOG = CONSTRAINT_CONTENT_TRIGGER + | CONSTRAINT_DEADLINE + | CONSTRAINT_IDLE + | CONSTRAINT_STORAGE_NOT_LOW + | CONSTRAINT_TIMING_DELAY + | CONSTRAINT_WITHIN_QUOTA; + + // TODO(b/129954980) + private static final boolean STATS_LOG_ENABLED = false; + + // Soft override: ignore constraints like time that don't affect API availability + public static final int OVERRIDE_SOFT = 1; + // Full override: ignore all constraints including API-affecting like connectivity + public static final int OVERRIDE_FULL = 2; + + /** If not specified, trigger update delay is 10 seconds. */ + public static final long DEFAULT_TRIGGER_UPDATE_DELAY = 10*1000; + + /** The minimum possible update delay is 1/2 second. */ + public static final long MIN_TRIGGER_UPDATE_DELAY = 500; + + /** If not specified, trigger maxumum delay is 2 minutes. */ + public static final long DEFAULT_TRIGGER_MAX_DELAY = 2*60*1000; + + /** The minimum possible update delay is 1 second. */ + public static final long MIN_TRIGGER_MAX_DELAY = 1000; + + final JobInfo job; + /** + * Uid of the package requesting this job. This can differ from the "source" + * uid when the job was scheduled on the app's behalf, such as with the jobs + * that underly Sync Manager operation. + */ + final int callingUid; + final String batteryName; + + /** + * Identity of the app in which the job is hosted. + */ + final String sourcePackageName; + final int sourceUserId; + final int sourceUid; + final String sourceTag; + + final String tag; + + private GrantedUriPermissions uriPerms; + private boolean prepared; + + static final boolean DEBUG_PREPARE = true; + private Throwable unpreparedPoint = null; + + /** + * Earliest point in the future at which this job will be eligible to run. A value of 0 + * indicates there is no delay constraint. See {@link #hasTimingDelayConstraint()}. + */ + private final long earliestRunTimeElapsedMillis; + /** + * Latest point in the future at which this job must be run. A value of {@link Long#MAX_VALUE} + * indicates there is no deadline constraint. See {@link #hasDeadlineConstraint()}. + */ + private final long latestRunTimeElapsedMillis; + + /** + * Valid only for periodic jobs. The original latest point in the future at which this + * job was expected to run. + */ + private long mOriginalLatestRunTimeElapsedMillis; + + /** How many times this job has failed, used to compute back-off. */ + private final int numFailures; + + /** + * Which app standby bucket this job's app is in. Updated when the app is moved to a + * different bucket. + */ + private int standbyBucket; + + /** + * Debugging: timestamp if we ever defer this job based on standby bucketing, this + * is when we did so. + */ + private long whenStandbyDeferred; + + /** The first time this job was force batched. */ + private long mFirstForceBatchedTimeElapsed; + + // Constraints. + final int requiredConstraints; + private final int mRequiredConstraintsOfInterest; + int satisfiedConstraints = 0; + private int mSatisfiedConstraintsOfInterest = 0; + + // Set to true if doze constraint was satisfied due to app being whitelisted. + public boolean dozeWhitelisted; + + // Set to true when the app is "active" per AppStateTracker + public boolean uidActive; + + /** + * Flag for {@link #trackingControllers}: the battery controller is currently tracking this job. + */ + public static final int TRACKING_BATTERY = 1<<0; + /** + * Flag for {@link #trackingControllers}: the network connectivity controller is currently + * tracking this job. + */ + public static final int TRACKING_CONNECTIVITY = 1<<1; + /** + * Flag for {@link #trackingControllers}: the content observer controller is currently + * tracking this job. + */ + public static final int TRACKING_CONTENT = 1<<2; + /** + * Flag for {@link #trackingControllers}: the idle controller is currently tracking this job. + */ + public static final int TRACKING_IDLE = 1<<3; + /** + * Flag for {@link #trackingControllers}: the storage controller is currently tracking this job. + */ + public static final int TRACKING_STORAGE = 1<<4; + /** + * Flag for {@link #trackingControllers}: the time controller is currently tracking this job. + */ + public static final int TRACKING_TIME = 1<<5; + /** + * Flag for {@link #trackingControllers}: the quota controller is currently tracking this job. + */ + public static final int TRACKING_QUOTA = 1 << 6; + + /** + * Bit mask of controllers that are currently tracking the job. + */ + private int trackingControllers; + + /** + * Flag for {@link #mInternalFlags}: this job was scheduled when the app that owns the job + * service (not necessarily the caller) was in the foreground and the job has no time + * constraints, which makes it exempted from the battery saver job restriction. + * + * @hide + */ + public static final int INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION = 1 << 0; + + /** + * Versatile, persistable flags for a job that's updated within the system server, + * as opposed to {@link JobInfo#flags} that's set by callers. + */ + private int mInternalFlags; + + // These are filled in by controllers when preparing for execution. + public ArraySet<Uri> changedUris; + public ArraySet<String> changedAuthorities; + public Network network; + + public int lastEvaluatedPriority; + + // If non-null, this is work that has been enqueued for the job. + public ArrayList<JobWorkItem> pendingWork; + + // If non-null, this is work that is currently being executed. + public ArrayList<JobWorkItem> executingWork; + + public int nextPendingWorkId = 1; + + // Used by shell commands + public int overrideState = 0; + + // When this job was enqueued, for ordering. (in elapsedRealtimeMillis) + public long enqueueTime; + + // Metrics about queue latency. (in uptimeMillis) + public long madePending; + public long madeActive; + + /** + * Last time a job finished successfully for a periodic job, in the currentTimeMillis time, + * for dumpsys. + */ + private long mLastSuccessfulRunTime; + + /** + * Last time a job finished unsuccessfully, in the currentTimeMillis time, for dumpsys. + */ + private long mLastFailedRunTime; + + /** + * Transient: when a job is inflated from disk before we have a reliable RTC clock time, + * we retain the canonical (delay, deadline) scheduling tuple read out of the persistent + * store in UTC so that we can fix up the job's scheduling criteria once we get a good + * wall-clock time. If we have to persist the job again before the clock has been updated, + * we record these times again rather than calculating based on the earliest/latest elapsed + * time base figures. + * + * 'first' is the earliest/delay time, and 'second' is the latest/deadline time. + */ + private Pair<Long, Long> mPersistedUtcTimes; + + /** + * For use only by ContentObserverController: state it is maintaining about content URIs + * being observed. + */ + ContentObserverController.JobInstance contentObserverJobInstance; + + private long mTotalNetworkDownloadBytes = JobInfo.NETWORK_BYTES_UNKNOWN; + private long mTotalNetworkUploadBytes = JobInfo.NETWORK_BYTES_UNKNOWN; + + /////// Booleans that track if a job is ready to run. They should be updated whenever dependent + /////// states change. + + /** + * The deadline for the job has passed. This is only good for non-periodic jobs. A periodic job + * should only run if its constraints are satisfied. + * Computed as: NOT periodic AND has deadline constraint AND deadline constraint satisfied. + */ + private boolean mReadyDeadlineSatisfied; + + /** + * The device isn't Dozing or this job will be in the foreground. This implicit constraint must + * be satisfied. + */ + private boolean mReadyNotDozing; + + /** + * The job is not restricted from running in the background (due to Battery Saver). This + * implicit constraint must be satisfied. + */ + private boolean mReadyNotRestrictedInBg; + + /** The job is within its quota based on its standby bucket. */ + private boolean mReadyWithinQuota; + + /** Provide a handle to the service that this job will be run on. */ + public int getServiceToken() { + return callingUid; + } + + /** + * Core constructor for JobStatus instances. All other ctors funnel down to this one. + * + * @param job The actual requested parameters for the job + * @param callingUid Identity of the app that is scheduling the job. This may not be the + * app in which the job is implemented; such as with sync jobs. + * @param sourcePackageName The package name of the app in which the job will run. + * @param sourceUserId The user in which the job will run + * @param standbyBucket The standby bucket that the source package is currently assigned to, + * cached here for speed of handling during runnability evaluations (and updated when bucket + * assignments are changed) + * @param tag A string associated with the job for debugging/logging purposes. + * @param numFailures Count of how many times this job has requested a reschedule because + * its work was not yet finished. + * @param earliestRunTimeElapsedMillis Milestone: earliest point in time at which the job + * is to be considered runnable + * @param latestRunTimeElapsedMillis Milestone: point in time at which the job will be + * considered overdue + * @param lastSuccessfulRunTime When did we last run this job to completion? + * @param lastFailedRunTime When did we last run this job only to have it stop incomplete? + * @param internalFlags Non-API property flags about this job + */ + private JobStatus(JobInfo job, int callingUid, String sourcePackageName, + int sourceUserId, int standbyBucket, String tag, int numFailures, + long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis, + long lastSuccessfulRunTime, long lastFailedRunTime, int internalFlags) { + this.job = job; + this.callingUid = callingUid; + this.standbyBucket = standbyBucket; + + int tempSourceUid = -1; + if (sourceUserId != -1 && sourcePackageName != null) { + try { + tempSourceUid = AppGlobals.getPackageManager().getPackageUid(sourcePackageName, 0, + sourceUserId); + } catch (RemoteException ex) { + // Can't happen, PackageManager runs in the same process. + } + } + if (tempSourceUid == -1) { + this.sourceUid = callingUid; + this.sourceUserId = UserHandle.getUserId(callingUid); + this.sourcePackageName = job.getService().getPackageName(); + this.sourceTag = null; + } else { + this.sourceUid = tempSourceUid; + this.sourceUserId = sourceUserId; + this.sourcePackageName = sourcePackageName; + this.sourceTag = tag; + } + + this.batteryName = this.sourceTag != null + ? this.sourceTag + ":" + job.getService().getPackageName() + : job.getService().flattenToShortString(); + this.tag = "*job*/" + this.batteryName; + + this.earliestRunTimeElapsedMillis = earliestRunTimeElapsedMillis; + this.latestRunTimeElapsedMillis = latestRunTimeElapsedMillis; + this.mOriginalLatestRunTimeElapsedMillis = latestRunTimeElapsedMillis; + this.numFailures = numFailures; + + int requiredConstraints = job.getConstraintFlags(); + if (job.getRequiredNetwork() != null) { + requiredConstraints |= CONSTRAINT_CONNECTIVITY; + } + if (earliestRunTimeElapsedMillis != NO_EARLIEST_RUNTIME) { + requiredConstraints |= CONSTRAINT_TIMING_DELAY; + } + if (latestRunTimeElapsedMillis != NO_LATEST_RUNTIME) { + requiredConstraints |= CONSTRAINT_DEADLINE; + } + if (job.getTriggerContentUris() != null) { + requiredConstraints |= CONSTRAINT_CONTENT_TRIGGER; + } + this.requiredConstraints = requiredConstraints; + mRequiredConstraintsOfInterest = requiredConstraints & CONSTRAINTS_OF_INTEREST; + mReadyNotDozing = (job.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0; + + mLastSuccessfulRunTime = lastSuccessfulRunTime; + mLastFailedRunTime = lastFailedRunTime; + + mInternalFlags = internalFlags; + + updateEstimatedNetworkBytesLocked(); + + if (job.getRequiredNetwork() != null) { + // Later, when we check if a given network satisfies the required + // network, we need to know the UID that is requesting it, so push + // our source UID into place. + job.getRequiredNetwork().networkCapabilities.setSingleUid(this.sourceUid); + } + } + + /** Copy constructor: used specifically when cloning JobStatus objects for persistence, + * so we preserve RTC window bounds if the source object has them. */ + public JobStatus(JobStatus jobStatus) { + this(jobStatus.getJob(), jobStatus.getUid(), + jobStatus.getSourcePackageName(), jobStatus.getSourceUserId(), + jobStatus.getStandbyBucket(), + jobStatus.getSourceTag(), jobStatus.getNumFailures(), + jobStatus.getEarliestRunTime(), jobStatus.getLatestRunTimeElapsed(), + jobStatus.getLastSuccessfulRunTime(), jobStatus.getLastFailedRunTime(), + jobStatus.getInternalFlags()); + mPersistedUtcTimes = jobStatus.mPersistedUtcTimes; + if (jobStatus.mPersistedUtcTimes != null) { + if (DEBUG) { + Slog.i(TAG, "Cloning job with persisted run times", new RuntimeException("here")); + } + } + } + + /** + * Create a new JobStatus that was loaded from disk. We ignore the provided + * {@link android.app.job.JobInfo} time criteria because we can load a persisted periodic job + * from the {@link com.android.server.job.JobStore} and still want to respect its + * wallclock runtime rather than resetting it on every boot. + * We consider a freshly loaded job to no longer be in back-off, and the associated + * standby bucket is whatever the OS thinks it should be at this moment. + */ + public JobStatus(JobInfo job, int callingUid, String sourcePkgName, int sourceUserId, + int standbyBucket, String sourceTag, + long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis, + long lastSuccessfulRunTime, long lastFailedRunTime, + Pair<Long, Long> persistedExecutionTimesUTC, + int innerFlags) { + this(job, callingUid, sourcePkgName, sourceUserId, + standbyBucket, + sourceTag, 0, + earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis, + lastSuccessfulRunTime, lastFailedRunTime, innerFlags); + + // Only during initial inflation do we record the UTC-timebase execution bounds + // read from the persistent store. If we ever have to recreate the JobStatus on + // the fly, it means we're rescheduling the job; and this means that the calculated + // elapsed timebase bounds intrinsically become correct. + this.mPersistedUtcTimes = persistedExecutionTimesUTC; + if (persistedExecutionTimesUTC != null) { + if (DEBUG) { + Slog.i(TAG, "+ restored job with RTC times because of bad boot clock"); + } + } + } + + /** Create a new job to be rescheduled with the provided parameters. */ + public JobStatus(JobStatus rescheduling, + long newEarliestRuntimeElapsedMillis, + long newLatestRuntimeElapsedMillis, int backoffAttempt, + long lastSuccessfulRunTime, long lastFailedRunTime) { + this(rescheduling.job, rescheduling.getUid(), + rescheduling.getSourcePackageName(), rescheduling.getSourceUserId(), + rescheduling.getStandbyBucket(), + rescheduling.getSourceTag(), backoffAttempt, newEarliestRuntimeElapsedMillis, + newLatestRuntimeElapsedMillis, + lastSuccessfulRunTime, lastFailedRunTime, rescheduling.getInternalFlags()); + } + + /** + * Create a newly scheduled job. + * @param callingUid Uid of the package that scheduled this job. + * @param sourcePkg Package name of the app that will actually run the job. Null indicates + * that the calling package is the source. + * @param sourceUserId User id for whom this job is scheduled. -1 indicates this is same as the + * caller. + */ + public static JobStatus createFromJobInfo(JobInfo job, int callingUid, String sourcePkg, + int sourceUserId, String tag) { + final long elapsedNow = sElapsedRealtimeClock.millis(); + final long earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis; + if (job.isPeriodic()) { + // Make sure period is in the interval [min_possible_period, max_possible_period]. + final long period = Math.max(JobInfo.getMinPeriodMillis(), + Math.min(JobSchedulerService.MAX_ALLOWED_PERIOD_MS, job.getIntervalMillis())); + latestRunTimeElapsedMillis = elapsedNow + period; + earliestRunTimeElapsedMillis = latestRunTimeElapsedMillis + // Make sure flex is in the interval [min_possible_flex, period]. + - Math.max(JobInfo.getMinFlexMillis(), Math.min(period, job.getFlexMillis())); + } else { + earliestRunTimeElapsedMillis = job.hasEarlyConstraint() ? + elapsedNow + job.getMinLatencyMillis() : NO_EARLIEST_RUNTIME; + latestRunTimeElapsedMillis = job.hasLateConstraint() ? + elapsedNow + job.getMaxExecutionDelayMillis() : NO_LATEST_RUNTIME; + } + String jobPackage = (sourcePkg != null) ? sourcePkg : job.getService().getPackageName(); + + int standbyBucket = JobSchedulerService.standbyBucketForPackage(jobPackage, + sourceUserId, elapsedNow); + JobSchedulerInternal js = LocalServices.getService(JobSchedulerInternal.class); + return new JobStatus(job, callingUid, sourcePkg, sourceUserId, + standbyBucket, tag, 0, + earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis, + 0 /* lastSuccessfulRunTime */, 0 /* lastFailedRunTime */, + /*innerFlags=*/ 0); + } + + public void enqueueWorkLocked(JobWorkItem work) { + if (pendingWork == null) { + pendingWork = new ArrayList<>(); + } + work.setWorkId(nextPendingWorkId); + nextPendingWorkId++; + if (work.getIntent() != null + && GrantedUriPermissions.checkGrantFlags(work.getIntent().getFlags())) { + work.setGrants(GrantedUriPermissions.createFromIntent(work.getIntent(), sourceUid, + sourcePackageName, sourceUserId, toShortString())); + } + pendingWork.add(work); + updateEstimatedNetworkBytesLocked(); + } + + public JobWorkItem dequeueWorkLocked() { + if (pendingWork != null && pendingWork.size() > 0) { + JobWorkItem work = pendingWork.remove(0); + if (work != null) { + if (executingWork == null) { + executingWork = new ArrayList<>(); + } + executingWork.add(work); + work.bumpDeliveryCount(); + } + updateEstimatedNetworkBytesLocked(); + return work; + } + return null; + } + + public boolean hasWorkLocked() { + return (pendingWork != null && pendingWork.size() > 0) || hasExecutingWorkLocked(); + } + + public boolean hasExecutingWorkLocked() { + return executingWork != null && executingWork.size() > 0; + } + + private static void ungrantWorkItem(JobWorkItem work) { + if (work.getGrants() != null) { + ((GrantedUriPermissions)work.getGrants()).revoke(); + } + } + + public boolean completeWorkLocked(int workId) { + if (executingWork != null) { + final int N = executingWork.size(); + for (int i = 0; i < N; i++) { + JobWorkItem work = executingWork.get(i); + if (work.getWorkId() == workId) { + executingWork.remove(i); + ungrantWorkItem(work); + return true; + } + } + } + return false; + } + + private static void ungrantWorkList(ArrayList<JobWorkItem> list) { + if (list != null) { + final int N = list.size(); + for (int i = 0; i < N; i++) { + ungrantWorkItem(list.get(i)); + } + } + } + + public void stopTrackingJobLocked(JobStatus incomingJob) { + if (incomingJob != null) { + // We are replacing with a new job -- transfer the work! We do any executing + // work first, since that was originally at the front of the pending work. + if (executingWork != null && executingWork.size() > 0) { + incomingJob.pendingWork = executingWork; + } + if (incomingJob.pendingWork == null) { + incomingJob.pendingWork = pendingWork; + } else if (pendingWork != null && pendingWork.size() > 0) { + incomingJob.pendingWork.addAll(pendingWork); + } + pendingWork = null; + executingWork = null; + incomingJob.nextPendingWorkId = nextPendingWorkId; + incomingJob.updateEstimatedNetworkBytesLocked(); + } else { + // We are completely stopping the job... need to clean up work. + ungrantWorkList(pendingWork); + pendingWork = null; + ungrantWorkList(executingWork); + executingWork = null; + } + updateEstimatedNetworkBytesLocked(); + } + + public void prepareLocked() { + if (prepared) { + Slog.wtf(TAG, "Already prepared: " + this); + return; + } + prepared = true; + if (DEBUG_PREPARE) { + unpreparedPoint = null; + } + final ClipData clip = job.getClipData(); + if (clip != null) { + uriPerms = GrantedUriPermissions.createFromClip(clip, sourceUid, sourcePackageName, + sourceUserId, job.getClipGrantFlags(), toShortString()); + } + } + + public void unprepareLocked() { + if (!prepared) { + Slog.wtf(TAG, "Hasn't been prepared: " + this); + if (DEBUG_PREPARE && unpreparedPoint != null) { + Slog.e(TAG, "Was already unprepared at ", unpreparedPoint); + } + return; + } + prepared = false; + if (DEBUG_PREPARE) { + unpreparedPoint = new Throwable().fillInStackTrace(); + } + if (uriPerms != null) { + uriPerms.revoke(); + uriPerms = null; + } + } + + public boolean isPreparedLocked() { + return prepared; + } + + public JobInfo getJob() { + return job; + } + + public int getJobId() { + return job.getId(); + } + + public void printUniqueId(PrintWriter pw) { + UserHandle.formatUid(pw, callingUid); + pw.print("/"); + pw.print(job.getId()); + } + + public int getNumFailures() { + return numFailures; + } + + public ComponentName getServiceComponent() { + return job.getService(); + } + + public String getSourcePackageName() { + return sourcePackageName; + } + + public int getSourceUid() { + return sourceUid; + } + + public int getSourceUserId() { + return sourceUserId; + } + + public int getUserId() { + return UserHandle.getUserId(callingUid); + } + + /** + * Returns an appropriate standby bucket for the job, taking into account any standby + * exemptions. + */ + public int getEffectiveStandbyBucket() { + if (uidActive || getJob().isExemptedFromAppStandby()) { + // Treat these cases as if they're in the ACTIVE bucket so that they get throttled + // like other ACTIVE apps. + return ACTIVE_INDEX; + } + return getStandbyBucket(); + } + + /** Returns the real standby bucket of the job. */ + public int getStandbyBucket() { + return standbyBucket; + } + + public void setStandbyBucket(int newBucket) { + standbyBucket = newBucket; + } + + // Called only by the standby monitoring code + public long getWhenStandbyDeferred() { + return whenStandbyDeferred; + } + + // Called only by the standby monitoring code + public void setWhenStandbyDeferred(long now) { + whenStandbyDeferred = now; + } + + /** + * Returns the first time this job was force batched, in the elapsed realtime timebase. Will be + * 0 if this job was never force batched. + */ + public long getFirstForceBatchedTimeElapsed() { + return mFirstForceBatchedTimeElapsed; + } + + public void setFirstForceBatchedTimeElapsed(long now) { + mFirstForceBatchedTimeElapsed = now; + } + + public String getSourceTag() { + return sourceTag; + } + + public int getUid() { + return callingUid; + } + + public String getBatteryName() { + return batteryName; + } + + public String getTag() { + return tag; + } + + public int getPriority() { + return job.getPriority(); + } + + public int getFlags() { + return job.getFlags(); + } + + public int getInternalFlags() { + return mInternalFlags; + } + + public void addInternalFlags(int flags) { + mInternalFlags |= flags; + } + + public int getSatisfiedConstraintFlags() { + return satisfiedConstraints; + } + + public void maybeAddForegroundExemption(Predicate<Integer> uidForegroundChecker) { + // Jobs with time constraints shouldn't be exempted. + if (job.hasEarlyConstraint() || job.hasLateConstraint()) { + return; + } + // Already exempted, skip the foreground check. + if ((mInternalFlags & INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION) != 0) { + return; + } + if (uidForegroundChecker.test(getSourceUid())) { + addInternalFlags(INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION); + } + } + + private void updateEstimatedNetworkBytesLocked() { + mTotalNetworkDownloadBytes = job.getEstimatedNetworkDownloadBytes(); + mTotalNetworkUploadBytes = job.getEstimatedNetworkUploadBytes(); + + if (pendingWork != null) { + for (int i = 0; i < pendingWork.size(); i++) { + if (mTotalNetworkDownloadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) { + // If any component of the job has unknown usage, we don't have a + // complete picture of what data will be used, and we have to treat the + // entire up/download as unknown. + long downloadBytes = pendingWork.get(i).getEstimatedNetworkDownloadBytes(); + if (downloadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) { + mTotalNetworkDownloadBytes += downloadBytes; + } + } + if (mTotalNetworkUploadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) { + // If any component of the job has unknown usage, we don't have a + // complete picture of what data will be used, and we have to treat the + // entire up/download as unknown. + long uploadBytes = pendingWork.get(i).getEstimatedNetworkUploadBytes(); + if (uploadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) { + mTotalNetworkUploadBytes += uploadBytes; + } + } + } + } + } + + public long getEstimatedNetworkDownloadBytes() { + return mTotalNetworkDownloadBytes; + } + + public long getEstimatedNetworkUploadBytes() { + return mTotalNetworkUploadBytes; + } + + /** Does this job have any sort of networking constraint? */ + public boolean hasConnectivityConstraint() { + return (requiredConstraints&CONSTRAINT_CONNECTIVITY) != 0; + } + + public boolean hasChargingConstraint() { + return (requiredConstraints&CONSTRAINT_CHARGING) != 0; + } + + public boolean hasBatteryNotLowConstraint() { + return (requiredConstraints&CONSTRAINT_BATTERY_NOT_LOW) != 0; + } + + public boolean hasPowerConstraint() { + return (requiredConstraints&(CONSTRAINT_CHARGING|CONSTRAINT_BATTERY_NOT_LOW)) != 0; + } + + public boolean hasStorageNotLowConstraint() { + return (requiredConstraints&CONSTRAINT_STORAGE_NOT_LOW) != 0; + } + + public boolean hasTimingDelayConstraint() { + return (requiredConstraints&CONSTRAINT_TIMING_DELAY) != 0; + } + + public boolean hasDeadlineConstraint() { + return (requiredConstraints&CONSTRAINT_DEADLINE) != 0; + } + + public boolean hasIdleConstraint() { + return (requiredConstraints&CONSTRAINT_IDLE) != 0; + } + + public boolean hasContentTriggerConstraint() { + return (requiredConstraints&CONSTRAINT_CONTENT_TRIGGER) != 0; + } + + public long getTriggerContentUpdateDelay() { + long time = job.getTriggerContentUpdateDelay(); + if (time < 0) { + return DEFAULT_TRIGGER_UPDATE_DELAY; + } + return Math.max(time, MIN_TRIGGER_UPDATE_DELAY); + } + + public long getTriggerContentMaxDelay() { + long time = job.getTriggerContentMaxDelay(); + if (time < 0) { + return DEFAULT_TRIGGER_MAX_DELAY; + } + return Math.max(time, MIN_TRIGGER_MAX_DELAY); + } + + public boolean isPersisted() { + return job.isPersisted(); + } + + public long getEarliestRunTime() { + return earliestRunTimeElapsedMillis; + } + + public long getLatestRunTimeElapsed() { + return latestRunTimeElapsedMillis; + } + + public long getOriginalLatestRunTimeElapsed() { + return mOriginalLatestRunTimeElapsedMillis; + } + + public void setOriginalLatestRunTimeElapsed(long latestRunTimeElapsed) { + mOriginalLatestRunTimeElapsedMillis = latestRunTimeElapsed; + } + + /** + * Return the fractional position of "now" within the "run time" window of + * this job. + * <p> + * For example, if the earliest run time was 10 minutes ago, and the latest + * run time is 30 minutes from now, this would return 0.25. + * <p> + * If the job has no window defined, returns 1. When only an earliest or + * latest time is defined, it's treated as an infinitely small window at + * that time. + */ + public float getFractionRunTime() { + final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); + if (earliestRunTimeElapsedMillis == 0 && latestRunTimeElapsedMillis == Long.MAX_VALUE) { + return 1; + } else if (earliestRunTimeElapsedMillis == 0) { + return now >= latestRunTimeElapsedMillis ? 1 : 0; + } else if (latestRunTimeElapsedMillis == Long.MAX_VALUE) { + return now >= earliestRunTimeElapsedMillis ? 1 : 0; + } else { + if (now <= earliestRunTimeElapsedMillis) { + return 0; + } else if (now >= latestRunTimeElapsedMillis) { + return 1; + } else { + return (float) (now - earliestRunTimeElapsedMillis) + / (float) (latestRunTimeElapsedMillis - earliestRunTimeElapsedMillis); + } + } + } + + public Pair<Long, Long> getPersistedUtcTimes() { + return mPersistedUtcTimes; + } + + public void clearPersistedUtcTimes() { + mPersistedUtcTimes = null; + } + + /** @return true if the constraint was changed, false otherwise. */ + boolean setChargingConstraintSatisfied(boolean state) { + return setConstraintSatisfied(CONSTRAINT_CHARGING, state); + } + + /** @return true if the constraint was changed, false otherwise. */ + boolean setBatteryNotLowConstraintSatisfied(boolean state) { + return setConstraintSatisfied(CONSTRAINT_BATTERY_NOT_LOW, state); + } + + /** @return true if the constraint was changed, false otherwise. */ + boolean setStorageNotLowConstraintSatisfied(boolean state) { + return setConstraintSatisfied(CONSTRAINT_STORAGE_NOT_LOW, state); + } + + /** @return true if the constraint was changed, false otherwise. */ + boolean setTimingDelayConstraintSatisfied(boolean state) { + return setConstraintSatisfied(CONSTRAINT_TIMING_DELAY, state); + } + + /** @return true if the constraint was changed, false otherwise. */ + boolean setDeadlineConstraintSatisfied(boolean state) { + if (setConstraintSatisfied(CONSTRAINT_DEADLINE, state)) { + // The constraint was changed. Update the ready flag. + mReadyDeadlineSatisfied = !job.isPeriodic() && hasDeadlineConstraint() && state; + return true; + } + return false; + } + + /** @return true if the constraint was changed, false otherwise. */ + boolean setIdleConstraintSatisfied(boolean state) { + return setConstraintSatisfied(CONSTRAINT_IDLE, state); + } + + /** @return true if the constraint was changed, false otherwise. */ + boolean setConnectivityConstraintSatisfied(boolean state) { + return setConstraintSatisfied(CONSTRAINT_CONNECTIVITY, state); + } + + /** @return true if the constraint was changed, false otherwise. */ + boolean setContentTriggerConstraintSatisfied(boolean state) { + return setConstraintSatisfied(CONSTRAINT_CONTENT_TRIGGER, state); + } + + /** @return true if the constraint was changed, false otherwise. */ + boolean setDeviceNotDozingConstraintSatisfied(boolean state, boolean whitelisted) { + dozeWhitelisted = whitelisted; + if (setConstraintSatisfied(CONSTRAINT_DEVICE_NOT_DOZING, state)) { + // The constraint was changed. Update the ready flag. + mReadyNotDozing = state || (job.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0; + return true; + } + return false; + } + + /** @return true if the constraint was changed, false otherwise. */ + boolean setBackgroundNotRestrictedConstraintSatisfied(boolean state) { + if (setConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED, state)) { + // The constraint was changed. Update the ready flag. + mReadyNotRestrictedInBg = state; + return true; + } + return false; + } + + /** @return true if the constraint was changed, false otherwise. */ + boolean setQuotaConstraintSatisfied(boolean state) { + if (setConstraintSatisfied(CONSTRAINT_WITHIN_QUOTA, state)) { + // The constraint was changed. Update the ready flag. + mReadyWithinQuota = state; + return true; + } + return false; + } + + /** @return true if the state was changed, false otherwise. */ + boolean setUidActive(final boolean newActiveState) { + if (newActiveState != uidActive) { + uidActive = newActiveState; + return true; + } + return false; /* unchanged */ + } + + /** @return true if the constraint was changed, false otherwise. */ + boolean setConstraintSatisfied(int constraint, boolean state) { + boolean old = (satisfiedConstraints&constraint) != 0; + if (old == state) { + return false; + } + satisfiedConstraints = (satisfiedConstraints&~constraint) | (state ? constraint : 0); + mSatisfiedConstraintsOfInterest = satisfiedConstraints & CONSTRAINTS_OF_INTEREST; + if (STATS_LOG_ENABLED && (STATSD_CONSTRAINTS_TO_LOG & constraint) != 0) { + StatsLog.write_non_chained(StatsLog.SCHEDULED_JOB_CONSTRAINT_CHANGED, + sourceUid, null, getBatteryName(), getProtoConstraint(constraint), + state ? StatsLog.SCHEDULED_JOB_CONSTRAINT_CHANGED__STATE__SATISFIED + : StatsLog.SCHEDULED_JOB_CONSTRAINT_CHANGED__STATE__UNSATISFIED); + } + return true; + } + + boolean isConstraintSatisfied(int constraint) { + return (satisfiedConstraints&constraint) != 0; + } + + boolean clearTrackingController(int which) { + if ((trackingControllers&which) != 0) { + trackingControllers &= ~which; + return true; + } + return false; + } + + void setTrackingController(int which) { + trackingControllers |= which; + } + + public long getLastSuccessfulRunTime() { + return mLastSuccessfulRunTime; + } + + public long getLastFailedRunTime() { + return mLastFailedRunTime; + } + + /** + * @return Whether or not this job is ready to run, based on its requirements. + */ + public boolean isReady() { + return isReady(mSatisfiedConstraintsOfInterest); + } + + /** + * @return Whether or not this job would be ready to run if it had the specified constraint + * granted, based on its requirements. + */ + boolean wouldBeReadyWithConstraint(int constraint) { + boolean oldValue = false; + int satisfied = mSatisfiedConstraintsOfInterest; + switch (constraint) { + case CONSTRAINT_BACKGROUND_NOT_RESTRICTED: + oldValue = mReadyNotRestrictedInBg; + mReadyNotRestrictedInBg = true; + break; + case CONSTRAINT_DEADLINE: + oldValue = mReadyDeadlineSatisfied; + mReadyDeadlineSatisfied = true; + break; + case CONSTRAINT_DEVICE_NOT_DOZING: + oldValue = mReadyNotDozing; + mReadyNotDozing = true; + break; + case CONSTRAINT_WITHIN_QUOTA: + oldValue = mReadyWithinQuota; + mReadyWithinQuota = true; + break; + default: + satisfied |= constraint; + break; + } + + boolean toReturn = isReady(satisfied); + + switch (constraint) { + case CONSTRAINT_BACKGROUND_NOT_RESTRICTED: + mReadyNotRestrictedInBg = oldValue; + break; + case CONSTRAINT_DEADLINE: + mReadyDeadlineSatisfied = oldValue; + break; + case CONSTRAINT_DEVICE_NOT_DOZING: + mReadyNotDozing = oldValue; + break; + case CONSTRAINT_WITHIN_QUOTA: + mReadyWithinQuota = oldValue; + break; + } + return toReturn; + } + + private boolean isReady(int satisfiedConstraints) { + // Quota constraints trumps all other constraints. + if (!mReadyWithinQuota) { + return false; + } + // Deadline constraint trumps other constraints besides quota (except for periodic jobs + // where deadline is an implementation detail. A periodic job should only run if its + // constraints are satisfied). + // DeviceNotDozing implicit constraint must be satisfied + // NotRestrictedInBackground implicit constraint must be satisfied + return mReadyNotDozing && mReadyNotRestrictedInBg && (mReadyDeadlineSatisfied + || isConstraintsSatisfied(satisfiedConstraints)); + } + + static final int CONSTRAINTS_OF_INTEREST = CONSTRAINT_CHARGING | CONSTRAINT_BATTERY_NOT_LOW + | CONSTRAINT_STORAGE_NOT_LOW | CONSTRAINT_TIMING_DELAY | CONSTRAINT_CONNECTIVITY + | CONSTRAINT_IDLE | CONSTRAINT_CONTENT_TRIGGER; + + // Soft override covers all non-"functional" constraints + static final int SOFT_OVERRIDE_CONSTRAINTS = + CONSTRAINT_CHARGING | CONSTRAINT_BATTERY_NOT_LOW | CONSTRAINT_STORAGE_NOT_LOW + | CONSTRAINT_TIMING_DELAY | CONSTRAINT_IDLE; + + /** + * @return Whether the constraints set on this job are satisfied. + */ + public boolean isConstraintsSatisfied() { + return isConstraintsSatisfied(mSatisfiedConstraintsOfInterest); + } + + private boolean isConstraintsSatisfied(int satisfiedConstraints) { + if (overrideState == OVERRIDE_FULL) { + // force override: the job is always runnable + return true; + } + + int sat = satisfiedConstraints; + if (overrideState == OVERRIDE_SOFT) { + // override: pretend all 'soft' requirements are satisfied + sat |= (requiredConstraints & SOFT_OVERRIDE_CONSTRAINTS); + } + + return (sat & mRequiredConstraintsOfInterest) == mRequiredConstraintsOfInterest; + } + + public boolean matches(int uid, int jobId) { + return this.job.getId() == jobId && this.callingUid == uid; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(128); + sb.append("JobStatus{"); + sb.append(Integer.toHexString(System.identityHashCode(this))); + sb.append(" #"); + UserHandle.formatUid(sb, callingUid); + sb.append("/"); + sb.append(job.getId()); + sb.append(' '); + sb.append(batteryName); + sb.append(" u="); + sb.append(getUserId()); + sb.append(" s="); + sb.append(getSourceUid()); + if (earliestRunTimeElapsedMillis != NO_EARLIEST_RUNTIME + || latestRunTimeElapsedMillis != NO_LATEST_RUNTIME) { + long now = sElapsedRealtimeClock.millis(); + sb.append(" TIME="); + formatRunTime(sb, earliestRunTimeElapsedMillis, NO_EARLIEST_RUNTIME, now); + sb.append(":"); + formatRunTime(sb, latestRunTimeElapsedMillis, NO_LATEST_RUNTIME, now); + } + if (job.getRequiredNetwork() != null) { + sb.append(" NET"); + } + if (job.isRequireCharging()) { + sb.append(" CHARGING"); + } + if (job.isRequireBatteryNotLow()) { + sb.append(" BATNOTLOW"); + } + if (job.isRequireStorageNotLow()) { + sb.append(" STORENOTLOW"); + } + if (job.isRequireDeviceIdle()) { + sb.append(" IDLE"); + } + if (job.isPeriodic()) { + sb.append(" PERIODIC"); + } + if (job.isPersisted()) { + sb.append(" PERSISTED"); + } + if ((satisfiedConstraints&CONSTRAINT_DEVICE_NOT_DOZING) == 0) { + sb.append(" WAIT:DEV_NOT_DOZING"); + } + if (job.getTriggerContentUris() != null) { + sb.append(" URIS="); + sb.append(Arrays.toString(job.getTriggerContentUris())); + } + if (numFailures != 0) { + sb.append(" failures="); + sb.append(numFailures); + } + if (isReady()) { + sb.append(" READY"); + } + sb.append("}"); + return sb.toString(); + } + + private void formatRunTime(PrintWriter pw, long runtime, long defaultValue, long now) { + if (runtime == defaultValue) { + pw.print("none"); + } else { + TimeUtils.formatDuration(runtime - now, pw); + } + } + + private void formatRunTime(StringBuilder sb, long runtime, long defaultValue, long now) { + if (runtime == defaultValue) { + sb.append("none"); + } else { + TimeUtils.formatDuration(runtime - now, sb); + } + } + + /** + * Convenience function to identify a job uniquely without pulling all the data that + * {@link #toString()} returns. + */ + public String toShortString() { + StringBuilder sb = new StringBuilder(); + sb.append(Integer.toHexString(System.identityHashCode(this))); + sb.append(" #"); + UserHandle.formatUid(sb, callingUid); + sb.append("/"); + sb.append(job.getId()); + sb.append(' '); + sb.append(batteryName); + return sb.toString(); + } + + /** + * Convenience function to identify a job uniquely without pulling all the data that + * {@link #toString()} returns. + */ + public String toShortStringExceptUniqueId() { + StringBuilder sb = new StringBuilder(); + sb.append(Integer.toHexString(System.identityHashCode(this))); + sb.append(' '); + sb.append(batteryName); + return sb.toString(); + } + + /** + * Convenience function to dump data that identifies a job uniquely to proto. This is intended + * to mimic {@link #toShortString}. + */ + public void writeToShortProto(ProtoOutputStream proto, long fieldId) { + final long token = proto.start(fieldId); + + proto.write(JobStatusShortInfoProto.CALLING_UID, callingUid); + proto.write(JobStatusShortInfoProto.JOB_ID, job.getId()); + proto.write(JobStatusShortInfoProto.BATTERY_NAME, batteryName); + + proto.end(token); + } + + void dumpConstraints(PrintWriter pw, int constraints) { + if ((constraints&CONSTRAINT_CHARGING) != 0) { + pw.print(" CHARGING"); + } + if ((constraints& CONSTRAINT_BATTERY_NOT_LOW) != 0) { + pw.print(" BATTERY_NOT_LOW"); + } + if ((constraints& CONSTRAINT_STORAGE_NOT_LOW) != 0) { + pw.print(" STORAGE_NOT_LOW"); + } + if ((constraints&CONSTRAINT_TIMING_DELAY) != 0) { + pw.print(" TIMING_DELAY"); + } + if ((constraints&CONSTRAINT_DEADLINE) != 0) { + pw.print(" DEADLINE"); + } + if ((constraints&CONSTRAINT_IDLE) != 0) { + pw.print(" IDLE"); + } + if ((constraints&CONSTRAINT_CONNECTIVITY) != 0) { + pw.print(" CONNECTIVITY"); + } + if ((constraints&CONSTRAINT_CONTENT_TRIGGER) != 0) { + pw.print(" CONTENT_TRIGGER"); + } + if ((constraints&CONSTRAINT_DEVICE_NOT_DOZING) != 0) { + pw.print(" DEVICE_NOT_DOZING"); + } + if ((constraints&CONSTRAINT_BACKGROUND_NOT_RESTRICTED) != 0) { + pw.print(" BACKGROUND_NOT_RESTRICTED"); + } + if ((constraints & CONSTRAINT_WITHIN_QUOTA) != 0) { + pw.print(" WITHIN_QUOTA"); + } + if (constraints != 0) { + pw.print(" [0x"); + pw.print(Integer.toHexString(constraints)); + pw.print("]"); + } + } + + /** Returns a {@link JobServerProtoEnums.Constraint} enum value for the given constraint. */ + private int getProtoConstraint(int constraint) { + switch (constraint) { + case CONSTRAINT_BACKGROUND_NOT_RESTRICTED: + return JobServerProtoEnums.CONSTRAINT_BACKGROUND_NOT_RESTRICTED; + case CONSTRAINT_BATTERY_NOT_LOW: + return JobServerProtoEnums.CONSTRAINT_BATTERY_NOT_LOW; + case CONSTRAINT_CHARGING: + return JobServerProtoEnums.CONSTRAINT_CHARGING; + case CONSTRAINT_CONNECTIVITY: + return JobServerProtoEnums.CONSTRAINT_CONNECTIVITY; + case CONSTRAINT_CONTENT_TRIGGER: + return JobServerProtoEnums.CONSTRAINT_CONTENT_TRIGGER; + case CONSTRAINT_DEADLINE: + return JobServerProtoEnums.CONSTRAINT_DEADLINE; + case CONSTRAINT_DEVICE_NOT_DOZING: + return JobServerProtoEnums.CONSTRAINT_DEVICE_NOT_DOZING; + case CONSTRAINT_IDLE: + return JobServerProtoEnums.CONSTRAINT_IDLE; + case CONSTRAINT_STORAGE_NOT_LOW: + return JobServerProtoEnums.CONSTRAINT_STORAGE_NOT_LOW; + case CONSTRAINT_TIMING_DELAY: + return JobServerProtoEnums.CONSTRAINT_TIMING_DELAY; + case CONSTRAINT_WITHIN_QUOTA: + return JobServerProtoEnums.CONSTRAINT_WITHIN_QUOTA; + default: + return JobServerProtoEnums.CONSTRAINT_UNKNOWN; + } + } + + /** Writes constraints to the given repeating proto field. */ + void dumpConstraints(ProtoOutputStream proto, long fieldId, int constraints) { + if ((constraints & CONSTRAINT_CHARGING) != 0) { + proto.write(fieldId, JobServerProtoEnums.CONSTRAINT_CHARGING); + } + if ((constraints & CONSTRAINT_BATTERY_NOT_LOW) != 0) { + proto.write(fieldId, JobServerProtoEnums.CONSTRAINT_BATTERY_NOT_LOW); + } + if ((constraints & CONSTRAINT_STORAGE_NOT_LOW) != 0) { + proto.write(fieldId, JobServerProtoEnums.CONSTRAINT_STORAGE_NOT_LOW); + } + if ((constraints & CONSTRAINT_TIMING_DELAY) != 0) { + proto.write(fieldId, JobServerProtoEnums.CONSTRAINT_TIMING_DELAY); + } + if ((constraints & CONSTRAINT_DEADLINE) != 0) { + proto.write(fieldId, JobServerProtoEnums.CONSTRAINT_DEADLINE); + } + if ((constraints & CONSTRAINT_IDLE) != 0) { + proto.write(fieldId, JobServerProtoEnums.CONSTRAINT_IDLE); + } + if ((constraints & CONSTRAINT_CONNECTIVITY) != 0) { + proto.write(fieldId, JobServerProtoEnums.CONSTRAINT_CONNECTIVITY); + } + if ((constraints & CONSTRAINT_CONTENT_TRIGGER) != 0) { + proto.write(fieldId, JobServerProtoEnums.CONSTRAINT_CONTENT_TRIGGER); + } + if ((constraints & CONSTRAINT_DEVICE_NOT_DOZING) != 0) { + proto.write(fieldId, JobServerProtoEnums.CONSTRAINT_DEVICE_NOT_DOZING); + } + if ((constraints & CONSTRAINT_WITHIN_QUOTA) != 0) { + proto.write(fieldId, JobServerProtoEnums.CONSTRAINT_WITHIN_QUOTA); + } + if ((constraints & CONSTRAINT_BACKGROUND_NOT_RESTRICTED) != 0) { + proto.write(fieldId, JobServerProtoEnums.CONSTRAINT_BACKGROUND_NOT_RESTRICTED); + } + } + + private void dumpJobWorkItem(PrintWriter pw, String prefix, JobWorkItem work, int index) { + pw.print(prefix); pw.print(" #"); pw.print(index); pw.print(": #"); + pw.print(work.getWorkId()); pw.print(" "); pw.print(work.getDeliveryCount()); + pw.print("x "); pw.println(work.getIntent()); + if (work.getGrants() != null) { + pw.print(prefix); pw.println(" URI grants:"); + ((GrantedUriPermissions)work.getGrants()).dump(pw, prefix + " "); + } + } + + private void dumpJobWorkItem(ProtoOutputStream proto, long fieldId, JobWorkItem work) { + final long token = proto.start(fieldId); + + proto.write(JobStatusDumpProto.JobWorkItem.WORK_ID, work.getWorkId()); + proto.write(JobStatusDumpProto.JobWorkItem.DELIVERY_COUNT, work.getDeliveryCount()); + if (work.getIntent() != null) { + work.getIntent().writeToProto(proto, JobStatusDumpProto.JobWorkItem.INTENT); + } + Object grants = work.getGrants(); + if (grants != null) { + ((GrantedUriPermissions) grants).dump(proto, JobStatusDumpProto.JobWorkItem.URI_GRANTS); + } + + proto.end(token); + } + + /** + * Returns a bucket name based on the normalized bucket indices, not the AppStandby constants. + */ + String getBucketName() { + return bucketName(standbyBucket); + } + + /** + * Returns a bucket name based on the normalized bucket indices, not the AppStandby constants. + */ + static String bucketName(int standbyBucket) { + switch (standbyBucket) { + case 0: return "ACTIVE"; + case 1: return "WORKING_SET"; + case 2: return "FREQUENT"; + case 3: return "RARE"; + case 4: return "NEVER"; + default: + return "Unknown: " + standbyBucket; + } + } + + // Dumpsys infrastructure + public void dump(PrintWriter pw, String prefix, boolean full, long elapsedRealtimeMillis) { + pw.print(prefix); UserHandle.formatUid(pw, callingUid); + pw.print(" tag="); pw.println(tag); + pw.print(prefix); + pw.print("Source: uid="); UserHandle.formatUid(pw, getSourceUid()); + pw.print(" user="); pw.print(getSourceUserId()); + pw.print(" pkg="); pw.println(getSourcePackageName()); + if (full) { + pw.print(prefix); pw.println("JobInfo:"); + pw.print(prefix); pw.print(" Service: "); + pw.println(job.getService().flattenToShortString()); + if (job.isPeriodic()) { + pw.print(prefix); pw.print(" PERIODIC: interval="); + TimeUtils.formatDuration(job.getIntervalMillis(), pw); + pw.print(" flex="); TimeUtils.formatDuration(job.getFlexMillis(), pw); + pw.println(); + } + if (job.isPersisted()) { + pw.print(prefix); pw.println(" PERSISTED"); + } + if (job.getPriority() != 0) { + pw.print(prefix); pw.print(" Priority: "); + pw.println(JobInfo.getPriorityString(job.getPriority())); + } + if (job.getFlags() != 0) { + pw.print(prefix); pw.print(" Flags: "); + pw.println(Integer.toHexString(job.getFlags())); + } + if (getInternalFlags() != 0) { + pw.print(prefix); pw.print(" Internal flags: "); + pw.print(Integer.toHexString(getInternalFlags())); + + if ((getInternalFlags()&INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION) != 0) { + pw.print(" HAS_FOREGROUND_EXEMPTION"); + } + pw.println(); + } + pw.print(prefix); pw.print(" Requires: charging="); + pw.print(job.isRequireCharging()); pw.print(" batteryNotLow="); + pw.print(job.isRequireBatteryNotLow()); pw.print(" deviceIdle="); + pw.println(job.isRequireDeviceIdle()); + if (job.getTriggerContentUris() != null) { + pw.print(prefix); pw.println(" Trigger content URIs:"); + for (int i = 0; i < job.getTriggerContentUris().length; i++) { + JobInfo.TriggerContentUri trig = job.getTriggerContentUris()[i]; + pw.print(prefix); pw.print(" "); + pw.print(Integer.toHexString(trig.getFlags())); + pw.print(' '); pw.println(trig.getUri()); + } + if (job.getTriggerContentUpdateDelay() >= 0) { + pw.print(prefix); pw.print(" Trigger update delay: "); + TimeUtils.formatDuration(job.getTriggerContentUpdateDelay(), pw); + pw.println(); + } + if (job.getTriggerContentMaxDelay() >= 0) { + pw.print(prefix); pw.print(" Trigger max delay: "); + TimeUtils.formatDuration(job.getTriggerContentMaxDelay(), pw); + pw.println(); + } + } + if (job.getExtras() != null && !job.getExtras().maybeIsEmpty()) { + pw.print(prefix); pw.print(" Extras: "); + pw.println(job.getExtras().toShortString()); + } + if (job.getTransientExtras() != null && !job.getTransientExtras().maybeIsEmpty()) { + pw.print(prefix); pw.print(" Transient extras: "); + pw.println(job.getTransientExtras().toShortString()); + } + if (job.getClipData() != null) { + pw.print(prefix); pw.print(" Clip data: "); + StringBuilder b = new StringBuilder(128); + job.getClipData().toShortString(b); + pw.println(b); + } + if (uriPerms != null) { + pw.print(prefix); pw.println(" Granted URI permissions:"); + uriPerms.dump(pw, prefix + " "); + } + if (job.getRequiredNetwork() != null) { + pw.print(prefix); pw.print(" Network type: "); + pw.println(job.getRequiredNetwork()); + } + if (mTotalNetworkDownloadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) { + pw.print(prefix); pw.print(" Network download bytes: "); + pw.println(mTotalNetworkDownloadBytes); + } + if (mTotalNetworkUploadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) { + pw.print(prefix); pw.print(" Network upload bytes: "); + pw.println(mTotalNetworkUploadBytes); + } + if (job.getMinLatencyMillis() != 0) { + pw.print(prefix); pw.print(" Minimum latency: "); + TimeUtils.formatDuration(job.getMinLatencyMillis(), pw); + pw.println(); + } + if (job.getMaxExecutionDelayMillis() != 0) { + pw.print(prefix); pw.print(" Max execution delay: "); + TimeUtils.formatDuration(job.getMaxExecutionDelayMillis(), pw); + pw.println(); + } + pw.print(prefix); pw.print(" Backoff: policy="); pw.print(job.getBackoffPolicy()); + pw.print(" initial="); TimeUtils.formatDuration(job.getInitialBackoffMillis(), pw); + pw.println(); + if (job.hasEarlyConstraint()) { + pw.print(prefix); pw.println(" Has early constraint"); + } + if (job.hasLateConstraint()) { + pw.print(prefix); pw.println(" Has late constraint"); + } + } + pw.print(prefix); pw.print("Required constraints:"); + dumpConstraints(pw, requiredConstraints); + pw.println(); + if (full) { + pw.print(prefix); pw.print("Satisfied constraints:"); + dumpConstraints(pw, satisfiedConstraints); + pw.println(); + pw.print(prefix); pw.print("Unsatisfied constraints:"); + dumpConstraints(pw, + ((requiredConstraints | CONSTRAINT_WITHIN_QUOTA) & ~satisfiedConstraints)); + pw.println(); + if (dozeWhitelisted) { + pw.print(prefix); pw.println("Doze whitelisted: true"); + } + if (uidActive) { + pw.print(prefix); pw.println("Uid: active"); + } + if (job.isExemptedFromAppStandby()) { + pw.print(prefix); pw.println("Is exempted from app standby"); + } + } + if (trackingControllers != 0) { + pw.print(prefix); pw.print("Tracking:"); + if ((trackingControllers&TRACKING_BATTERY) != 0) pw.print(" BATTERY"); + if ((trackingControllers&TRACKING_CONNECTIVITY) != 0) pw.print(" CONNECTIVITY"); + if ((trackingControllers&TRACKING_CONTENT) != 0) pw.print(" CONTENT"); + if ((trackingControllers&TRACKING_IDLE) != 0) pw.print(" IDLE"); + if ((trackingControllers&TRACKING_STORAGE) != 0) pw.print(" STORAGE"); + if ((trackingControllers&TRACKING_TIME) != 0) pw.print(" TIME"); + if ((trackingControllers & TRACKING_QUOTA) != 0) pw.print(" QUOTA"); + pw.println(); + } + + pw.print(prefix); pw.println("Implicit constraints:"); + pw.print(prefix); pw.print(" readyNotDozing: "); + pw.println(mReadyNotDozing); + pw.print(prefix); pw.print(" readyNotRestrictedInBg: "); + pw.println(mReadyNotRestrictedInBg); + if (!job.isPeriodic() && hasDeadlineConstraint()) { + pw.print(prefix); pw.print(" readyDeadlineSatisfied: "); + pw.println(mReadyDeadlineSatisfied); + } + + if (changedAuthorities != null) { + pw.print(prefix); pw.println("Changed authorities:"); + for (int i=0; i<changedAuthorities.size(); i++) { + pw.print(prefix); pw.print(" "); pw.println(changedAuthorities.valueAt(i)); + } + } + if (changedUris != null) { + pw.print(prefix); + pw.println("Changed URIs:"); + for (int i = 0; i < changedUris.size(); i++) { + pw.print(prefix); + pw.print(" "); + pw.println(changedUris.valueAt(i)); + } + } + if (network != null) { + pw.print(prefix); pw.print("Network: "); pw.println(network); + } + if (pendingWork != null && pendingWork.size() > 0) { + pw.print(prefix); pw.println("Pending work:"); + for (int i = 0; i < pendingWork.size(); i++) { + dumpJobWorkItem(pw, prefix, pendingWork.get(i), i); + } + } + if (executingWork != null && executingWork.size() > 0) { + pw.print(prefix); pw.println("Executing work:"); + for (int i = 0; i < executingWork.size(); i++) { + dumpJobWorkItem(pw, prefix, executingWork.get(i), i); + } + } + pw.print(prefix); pw.print("Standby bucket: "); + pw.println(getBucketName()); + if (whenStandbyDeferred != 0) { + pw.print(prefix); pw.print(" Deferred since: "); + TimeUtils.formatDuration(whenStandbyDeferred, elapsedRealtimeMillis, pw); + pw.println(); + } + if (mFirstForceBatchedTimeElapsed != 0) { + pw.print(prefix); + pw.print(" Time since first force batch attempt: "); + TimeUtils.formatDuration(mFirstForceBatchedTimeElapsed, elapsedRealtimeMillis, pw); + pw.println(); + } + pw.print(prefix); pw.print("Enqueue time: "); + TimeUtils.formatDuration(enqueueTime, elapsedRealtimeMillis, pw); + pw.println(); + pw.print(prefix); pw.print("Run time: earliest="); + formatRunTime(pw, earliestRunTimeElapsedMillis, NO_EARLIEST_RUNTIME, elapsedRealtimeMillis); + pw.print(", latest="); + formatRunTime(pw, latestRunTimeElapsedMillis, NO_LATEST_RUNTIME, elapsedRealtimeMillis); + pw.print(", original latest="); + formatRunTime(pw, mOriginalLatestRunTimeElapsedMillis, + NO_LATEST_RUNTIME, elapsedRealtimeMillis); + pw.println(); + if (numFailures != 0) { + pw.print(prefix); pw.print("Num failures: "); pw.println(numFailures); + } + if (mLastSuccessfulRunTime != 0) { + pw.print(prefix); pw.print("Last successful run: "); + pw.println(TimeMigrationUtils.formatMillisWithFixedFormat(mLastSuccessfulRunTime)); + } + if (mLastFailedRunTime != 0) { + pw.print(prefix); pw.print("Last failed run: "); + pw.println(TimeMigrationUtils.formatMillisWithFixedFormat(mLastFailedRunTime)); + } + } + + public void dump(ProtoOutputStream proto, long fieldId, boolean full, long elapsedRealtimeMillis) { + final long token = proto.start(fieldId); + + proto.write(JobStatusDumpProto.CALLING_UID, callingUid); + proto.write(JobStatusDumpProto.TAG, tag); + proto.write(JobStatusDumpProto.SOURCE_UID, getSourceUid()); + proto.write(JobStatusDumpProto.SOURCE_USER_ID, getSourceUserId()); + proto.write(JobStatusDumpProto.SOURCE_PACKAGE_NAME, getSourcePackageName()); + + if (full) { + final long jiToken = proto.start(JobStatusDumpProto.JOB_INFO); + + job.getService().writeToProto(proto, JobStatusDumpProto.JobInfo.SERVICE); + + proto.write(JobStatusDumpProto.JobInfo.IS_PERIODIC, job.isPeriodic()); + proto.write(JobStatusDumpProto.JobInfo.PERIOD_INTERVAL_MS, job.getIntervalMillis()); + proto.write(JobStatusDumpProto.JobInfo.PERIOD_FLEX_MS, job.getFlexMillis()); + + proto.write(JobStatusDumpProto.JobInfo.IS_PERSISTED, job.isPersisted()); + proto.write(JobStatusDumpProto.JobInfo.PRIORITY, job.getPriority()); + proto.write(JobStatusDumpProto.JobInfo.FLAGS, job.getFlags()); + proto.write(JobStatusDumpProto.INTERNAL_FLAGS, getInternalFlags()); + // Foreground exemption can be determined from internal flags value. + + proto.write(JobStatusDumpProto.JobInfo.REQUIRES_CHARGING, job.isRequireCharging()); + proto.write(JobStatusDumpProto.JobInfo.REQUIRES_BATTERY_NOT_LOW, job.isRequireBatteryNotLow()); + proto.write(JobStatusDumpProto.JobInfo.REQUIRES_DEVICE_IDLE, job.isRequireDeviceIdle()); + + if (job.getTriggerContentUris() != null) { + for (int i = 0; i < job.getTriggerContentUris().length; i++) { + final long tcuToken = proto.start(JobStatusDumpProto.JobInfo.TRIGGER_CONTENT_URIS); + JobInfo.TriggerContentUri trig = job.getTriggerContentUris()[i]; + + proto.write(JobStatusDumpProto.JobInfo.TriggerContentUri.FLAGS, trig.getFlags()); + Uri u = trig.getUri(); + if (u != null) { + proto.write(JobStatusDumpProto.JobInfo.TriggerContentUri.URI, u.toString()); + } + + proto.end(tcuToken); + } + if (job.getTriggerContentUpdateDelay() >= 0) { + proto.write(JobStatusDumpProto.JobInfo.TRIGGER_CONTENT_UPDATE_DELAY_MS, + job.getTriggerContentUpdateDelay()); + } + if (job.getTriggerContentMaxDelay() >= 0) { + proto.write(JobStatusDumpProto.JobInfo.TRIGGER_CONTENT_MAX_DELAY_MS, + job.getTriggerContentMaxDelay()); + } + } + if (job.getExtras() != null && !job.getExtras().maybeIsEmpty()) { + job.getExtras().writeToProto(proto, JobStatusDumpProto.JobInfo.EXTRAS); + } + if (job.getTransientExtras() != null && !job.getTransientExtras().maybeIsEmpty()) { + job.getTransientExtras().writeToProto(proto, JobStatusDumpProto.JobInfo.TRANSIENT_EXTRAS); + } + if (job.getClipData() != null) { + job.getClipData().writeToProto(proto, JobStatusDumpProto.JobInfo.CLIP_DATA); + } + if (uriPerms != null) { + uriPerms.dump(proto, JobStatusDumpProto.JobInfo.GRANTED_URI_PERMISSIONS); + } + if (job.getRequiredNetwork() != null) { + job.getRequiredNetwork().writeToProto(proto, JobStatusDumpProto.JobInfo.REQUIRED_NETWORK); + } + if (mTotalNetworkDownloadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) { + proto.write(JobStatusDumpProto.JobInfo.TOTAL_NETWORK_DOWNLOAD_BYTES, + mTotalNetworkDownloadBytes); + } + if (mTotalNetworkUploadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) { + proto.write(JobStatusDumpProto.JobInfo.TOTAL_NETWORK_UPLOAD_BYTES, + mTotalNetworkUploadBytes); + } + proto.write(JobStatusDumpProto.JobInfo.MIN_LATENCY_MS, job.getMinLatencyMillis()); + proto.write(JobStatusDumpProto.JobInfo.MAX_EXECUTION_DELAY_MS, job.getMaxExecutionDelayMillis()); + + final long bpToken = proto.start(JobStatusDumpProto.JobInfo.BACKOFF_POLICY); + proto.write(JobStatusDumpProto.JobInfo.Backoff.POLICY, job.getBackoffPolicy()); + proto.write(JobStatusDumpProto.JobInfo.Backoff.INITIAL_BACKOFF_MS, + job.getInitialBackoffMillis()); + proto.end(bpToken); + + proto.write(JobStatusDumpProto.JobInfo.HAS_EARLY_CONSTRAINT, job.hasEarlyConstraint()); + proto.write(JobStatusDumpProto.JobInfo.HAS_LATE_CONSTRAINT, job.hasLateConstraint()); + + proto.end(jiToken); + } + + dumpConstraints(proto, JobStatusDumpProto.REQUIRED_CONSTRAINTS, requiredConstraints); + if (full) { + dumpConstraints(proto, JobStatusDumpProto.SATISFIED_CONSTRAINTS, satisfiedConstraints); + dumpConstraints(proto, JobStatusDumpProto.UNSATISFIED_CONSTRAINTS, + ((requiredConstraints | CONSTRAINT_WITHIN_QUOTA) & ~satisfiedConstraints)); + proto.write(JobStatusDumpProto.IS_DOZE_WHITELISTED, dozeWhitelisted); + proto.write(JobStatusDumpProto.IS_UID_ACTIVE, uidActive); + proto.write(JobStatusDumpProto.IS_EXEMPTED_FROM_APP_STANDBY, + job.isExemptedFromAppStandby()); + } + + // Tracking controllers + if ((trackingControllers&TRACKING_BATTERY) != 0) { + proto.write(JobStatusDumpProto.TRACKING_CONTROLLERS, + JobStatusDumpProto.TRACKING_BATTERY); + } + if ((trackingControllers&TRACKING_CONNECTIVITY) != 0) { + proto.write(JobStatusDumpProto.TRACKING_CONTROLLERS, + JobStatusDumpProto.TRACKING_CONNECTIVITY); + } + if ((trackingControllers&TRACKING_CONTENT) != 0) { + proto.write(JobStatusDumpProto.TRACKING_CONTROLLERS, + JobStatusDumpProto.TRACKING_CONTENT); + } + if ((trackingControllers&TRACKING_IDLE) != 0) { + proto.write(JobStatusDumpProto.TRACKING_CONTROLLERS, + JobStatusDumpProto.TRACKING_IDLE); + } + if ((trackingControllers&TRACKING_STORAGE) != 0) { + proto.write(JobStatusDumpProto.TRACKING_CONTROLLERS, + JobStatusDumpProto.TRACKING_STORAGE); + } + if ((trackingControllers&TRACKING_TIME) != 0) { + proto.write(JobStatusDumpProto.TRACKING_CONTROLLERS, + JobStatusDumpProto.TRACKING_TIME); + } + if ((trackingControllers & TRACKING_QUOTA) != 0) { + proto.write(JobStatusDumpProto.TRACKING_CONTROLLERS, + JobStatusDumpProto.TRACKING_QUOTA); + } + + // Implicit constraints + final long icToken = proto.start(JobStatusDumpProto.IMPLICIT_CONSTRAINTS); + proto.write(JobStatusDumpProto.ImplicitConstraints.IS_NOT_DOZING, mReadyNotDozing); + proto.write(JobStatusDumpProto.ImplicitConstraints.IS_NOT_RESTRICTED_IN_BG, + mReadyNotRestrictedInBg); + // mReadyDeadlineSatisfied isn't an implicit constraint...and can be determined from other + // field values. + proto.end(icToken); + + if (changedAuthorities != null) { + for (int k = 0; k < changedAuthorities.size(); k++) { + proto.write(JobStatusDumpProto.CHANGED_AUTHORITIES, changedAuthorities.valueAt(k)); + } + } + if (changedUris != null) { + for (int i = 0; i < changedUris.size(); i++) { + Uri u = changedUris.valueAt(i); + proto.write(JobStatusDumpProto.CHANGED_URIS, u.toString()); + } + } + + if (network != null) { + network.writeToProto(proto, JobStatusDumpProto.NETWORK); + } + + if (pendingWork != null) { + for (int i = 0; i < pendingWork.size(); i++) { + dumpJobWorkItem(proto, JobStatusDumpProto.PENDING_WORK, pendingWork.get(i)); + } + } + if (executingWork != null) { + for (int i = 0; i < executingWork.size(); i++) { + dumpJobWorkItem(proto, JobStatusDumpProto.EXECUTING_WORK, executingWork.get(i)); + } + } + + proto.write(JobStatusDumpProto.STANDBY_BUCKET, standbyBucket); + proto.write(JobStatusDumpProto.ENQUEUE_DURATION_MS, elapsedRealtimeMillis - enqueueTime); + proto.write(JobStatusDumpProto.TIME_SINCE_FIRST_DEFERRAL_MS, + whenStandbyDeferred == 0 ? 0 : elapsedRealtimeMillis - whenStandbyDeferred); + proto.write(JobStatusDumpProto.TIME_SINCE_FIRST_FORCE_BATCH_ATTEMPT_MS, + mFirstForceBatchedTimeElapsed == 0 + ? 0 : elapsedRealtimeMillis - mFirstForceBatchedTimeElapsed); + if (earliestRunTimeElapsedMillis == NO_EARLIEST_RUNTIME) { + proto.write(JobStatusDumpProto.TIME_UNTIL_EARLIEST_RUNTIME_MS, 0); + } else { + proto.write(JobStatusDumpProto.TIME_UNTIL_EARLIEST_RUNTIME_MS, + earliestRunTimeElapsedMillis - elapsedRealtimeMillis); + } + if (latestRunTimeElapsedMillis == NO_LATEST_RUNTIME) { + proto.write(JobStatusDumpProto.TIME_UNTIL_LATEST_RUNTIME_MS, 0); + } else { + proto.write(JobStatusDumpProto.TIME_UNTIL_LATEST_RUNTIME_MS, + latestRunTimeElapsedMillis - elapsedRealtimeMillis); + } + proto.write(JobStatusDumpProto.ORIGINAL_LATEST_RUNTIME_ELAPSED, + mOriginalLatestRunTimeElapsedMillis); + + proto.write(JobStatusDumpProto.NUM_FAILURES, numFailures); + proto.write(JobStatusDumpProto.LAST_SUCCESSFUL_RUN_TIME, mLastSuccessfulRunTime); + proto.write(JobStatusDumpProto.LAST_FAILED_RUN_TIME, mLastFailedRunTime); + + proto.end(token); + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java new file mode 100644 index 000000000000..831be0bf213a --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java @@ -0,0 +1,2795 @@ +/* + * Copyright (C) 2018 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 static android.text.format.DateUtils.HOUR_IN_MILLIS; +import static android.text.format.DateUtils.MINUTE_IN_MILLIS; +import static android.text.format.DateUtils.SECOND_IN_MILLIS; + +import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX; +import static com.android.server.job.JobSchedulerService.FREQUENT_INDEX; +import static com.android.server.job.JobSchedulerService.NEVER_INDEX; +import static com.android.server.job.JobSchedulerService.RARE_INDEX; +import static com.android.server.job.JobSchedulerService.WORKING_INDEX; +import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.app.ActivityManager; +import android.app.ActivityManagerInternal; +import android.app.AlarmManager; +import android.app.AppGlobals; +import android.app.IUidObserver; +import android.app.usage.UsageStatsManagerInternal; +import android.app.usage.UsageStatsManagerInternal.AppIdleStateChangeListener; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.BatteryManager; +import android.os.BatteryManagerInternal; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.KeyValueListParser; +import android.util.Log; +import android.util.Slog; +import android.util.SparseArray; +import android.util.SparseBooleanArray; +import android.util.SparseSetArray; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.os.BackgroundThread; +import com.android.internal.util.IndentingPrintWriter; +import com.android.server.LocalServices; +import com.android.server.job.ConstantsProto; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.JobServiceContext; +import com.android.server.job.StateControllerProto; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * Controller that tracks whether an app has exceeded its standby bucket quota. + * + * With initial defaults, each app in each bucket is given 10 minutes to run within its respective + * time window. Active jobs can run indefinitely, working set jobs can run for 10 minutes within a + * 2 hour window, frequent jobs get to run 10 minutes in an 8 hour window, and rare jobs get to run + * 10 minutes in a 24 hour window. The windows are rolling, so as soon as a job would have some + * quota based on its bucket, it will be eligible to run. When a job's bucket changes, its new + * quota is immediately applied to it. + * + * Job and session count limits are included to prevent abuse/spam. Each bucket has its own limit on + * the number of jobs or sessions that can run within the window. Regardless of bucket, apps will + * not be allowed to run more than 20 jobs within the past 10 minutes. + * + * Jobs are throttled while an app is not in a foreground state. All jobs are allowed to run + * freely when an app enters the foreground state and are restricted when the app leaves the + * foreground state. However, jobs that are started while the app is in the TOP state do not count + * towards any quota and are not restricted regardless of the app's state change. + * + * Jobs will not be throttled when the device is charging. The device is considered to be charging + * once the {@link BatteryManager#ACTION_CHARGING} intent has been broadcast. + * + * Note: all limits are enforced per bucket window unless explicitly stated otherwise. + * All stated values are configurable and subject to change. See {@link QcConstants} for current + * defaults. + * + * Test: atest com.android.server.job.controllers.QuotaControllerTest + */ +public final class QuotaController extends StateController { + private static final String TAG = "JobScheduler.Quota"; + private static final boolean DEBUG = JobSchedulerService.DEBUG + || Log.isLoggable(TAG, Log.DEBUG); + + private static final String ALARM_TAG_CLEANUP = "*job.cleanup*"; + private static final String ALARM_TAG_QUOTA_CHECK = "*job.quota_check*"; + + /** + * A sparse array of ArrayMaps, which is suitable for holding (userId, packageName)->object + * associations. + */ + private static class UserPackageMap<T> { + private final SparseArray<ArrayMap<String, T>> mData = new SparseArray<>(); + + public void add(int userId, @NonNull String packageName, @Nullable T obj) { + ArrayMap<String, T> data = mData.get(userId); + if (data == null) { + data = new ArrayMap<String, T>(); + mData.put(userId, data); + } + data.put(packageName, obj); + } + + public void clear() { + for (int i = 0; i < mData.size(); ++i) { + mData.valueAt(i).clear(); + } + } + + /** Removes all the data for the user, if there was any. */ + public void delete(int userId) { + mData.delete(userId); + } + + /** Removes the data for the user and package, if there was any. */ + public void delete(int userId, @NonNull String packageName) { + ArrayMap<String, T> data = mData.get(userId); + if (data != null) { + data.remove(packageName); + } + } + + @Nullable + public T get(int userId, @NonNull String packageName) { + ArrayMap<String, T> data = mData.get(userId); + if (data != null) { + return data.get(packageName); + } + return null; + } + + /** @see SparseArray#indexOfKey */ + public int indexOfKey(int userId) { + return mData.indexOfKey(userId); + } + + /** Returns the userId at the given index. */ + public int keyAt(int index) { + return mData.keyAt(index); + } + + /** Returns the package name at the given index. */ + @NonNull + public String keyAt(int userIndex, int packageIndex) { + return mData.valueAt(userIndex).keyAt(packageIndex); + } + + /** Returns the size of the outer (userId) array. */ + public int numUsers() { + return mData.size(); + } + + public int numPackagesForUser(int userId) { + ArrayMap<String, T> data = mData.get(userId); + return data == null ? 0 : data.size(); + } + + /** Returns the value T at the given user and index. */ + @Nullable + public T valueAt(int userIndex, int packageIndex) { + return mData.valueAt(userIndex).valueAt(packageIndex); + } + + public void forEach(Consumer<T> consumer) { + for (int i = numUsers() - 1; i >= 0; --i) { + ArrayMap<String, T> data = mData.valueAt(i); + for (int j = data.size() - 1; j >= 0; --j) { + consumer.accept(data.valueAt(j)); + } + } + } + } + + /** + * Standardize the output of userId-packageName combo. + */ + private static String string(int userId, String packageName) { + return "<" + userId + ">" + packageName; + } + + private static final class Package { + public final String packageName; + public final int userId; + + Package(int userId, String packageName) { + this.userId = userId; + this.packageName = packageName; + } + + @Override + public String toString() { + return string(userId, packageName); + } + + public void writeToProto(ProtoOutputStream proto, long fieldId) { + final long token = proto.start(fieldId); + + proto.write(StateControllerProto.QuotaController.Package.USER_ID, userId); + proto.write(StateControllerProto.QuotaController.Package.NAME, packageName); + + proto.end(token); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Package) { + Package other = (Package) obj; + return userId == other.userId && Objects.equals(packageName, other.packageName); + } else { + return false; + } + } + + @Override + public int hashCode() { + return packageName.hashCode() + userId; + } + } + + private static int hashLong(long val) { + return (int) (val ^ (val >>> 32)); + } + + @VisibleForTesting + static class ExecutionStats { + /** + * The time after which this record should be considered invalid (out of date), in the + * elapsed realtime timebase. + */ + public long expirationTimeElapsed; + + public long windowSizeMs; + public int jobCountLimit; + public int sessionCountLimit; + + /** The total amount of time the app ran in its respective bucket window size. */ + public long executionTimeInWindowMs; + public int bgJobCountInWindow; + + /** The total amount of time the app ran in the last {@link #MAX_PERIOD_MS}. */ + public long executionTimeInMaxPeriodMs; + public int bgJobCountInMaxPeriod; + + /** + * The number of {@link TimingSession}s within the bucket window size. This will include + * sessions that started before the window as long as they end within the window. + */ + public int sessionCountInWindow; + + /** + * The time after which the app will be under the bucket quota and can start running jobs + * again. This is only valid if + * {@link #executionTimeInWindowMs} >= {@link #mAllowedTimePerPeriodMs}, + * {@link #executionTimeInMaxPeriodMs} >= {@link #mMaxExecutionTimeMs}, + * {@link #bgJobCountInWindow} >= {@link #jobCountLimit}, or + * {@link #sessionCountInWindow} >= {@link #sessionCountLimit}. + */ + public long inQuotaTimeElapsed; + + /** + * The time after which {@link #jobCountInRateLimitingWindow} should be considered invalid, + * in the elapsed realtime timebase. + */ + public long jobRateLimitExpirationTimeElapsed; + + /** + * The number of jobs that ran in at least the last {@link #mRateLimitingWindowMs}. + * It may contain a few stale entries since cleanup won't happen exactly every + * {@link #mRateLimitingWindowMs}. + */ + public int jobCountInRateLimitingWindow; + + /** + * The time after which {@link #sessionCountInRateLimitingWindow} should be considered + * invalid, in the elapsed realtime timebase. + */ + public long sessionRateLimitExpirationTimeElapsed; + + /** + * The number of {@link TimingSession}s that ran in at least the last + * {@link #mRateLimitingWindowMs}. It may contain a few stale entries since cleanup won't + * happen exactly every {@link #mRateLimitingWindowMs}. This should only be considered + * valid before elapsed realtime has reached {@link #sessionRateLimitExpirationTimeElapsed}. + */ + public int sessionCountInRateLimitingWindow; + + @Override + public String toString() { + return "expirationTime=" + expirationTimeElapsed + ", " + + "windowSizeMs=" + windowSizeMs + ", " + + "jobCountLimit=" + jobCountLimit + ", " + + "sessionCountLimit=" + sessionCountLimit + ", " + + "executionTimeInWindow=" + executionTimeInWindowMs + ", " + + "bgJobCountInWindow=" + bgJobCountInWindow + ", " + + "executionTimeInMaxPeriod=" + executionTimeInMaxPeriodMs + ", " + + "bgJobCountInMaxPeriod=" + bgJobCountInMaxPeriod + ", " + + "sessionCountInWindow=" + sessionCountInWindow + ", " + + "inQuotaTime=" + inQuotaTimeElapsed + ", " + + "jobCountExpirationTime=" + jobRateLimitExpirationTimeElapsed + ", " + + "jobCountInRateLimitingWindow=" + jobCountInRateLimitingWindow + ", " + + "sessionCountExpirationTime=" + sessionRateLimitExpirationTimeElapsed + ", " + + "sessionCountInRateLimitingWindow=" + sessionCountInRateLimitingWindow; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ExecutionStats) { + ExecutionStats other = (ExecutionStats) obj; + return this.expirationTimeElapsed == other.expirationTimeElapsed + && this.windowSizeMs == other.windowSizeMs + && this.jobCountLimit == other.jobCountLimit + && this.sessionCountLimit == other.sessionCountLimit + && this.executionTimeInWindowMs == other.executionTimeInWindowMs + && this.bgJobCountInWindow == other.bgJobCountInWindow + && this.executionTimeInMaxPeriodMs == other.executionTimeInMaxPeriodMs + && this.sessionCountInWindow == other.sessionCountInWindow + && this.bgJobCountInMaxPeriod == other.bgJobCountInMaxPeriod + && this.inQuotaTimeElapsed == other.inQuotaTimeElapsed + && this.jobRateLimitExpirationTimeElapsed + == other.jobRateLimitExpirationTimeElapsed + && this.jobCountInRateLimitingWindow == other.jobCountInRateLimitingWindow + && this.sessionRateLimitExpirationTimeElapsed + == other.sessionRateLimitExpirationTimeElapsed + && this.sessionCountInRateLimitingWindow + == other.sessionCountInRateLimitingWindow; + } else { + return false; + } + } + + @Override + public int hashCode() { + int result = 0; + result = 31 * result + hashLong(expirationTimeElapsed); + result = 31 * result + hashLong(windowSizeMs); + result = 31 * result + hashLong(jobCountLimit); + result = 31 * result + hashLong(sessionCountLimit); + result = 31 * result + hashLong(executionTimeInWindowMs); + result = 31 * result + bgJobCountInWindow; + result = 31 * result + hashLong(executionTimeInMaxPeriodMs); + result = 31 * result + bgJobCountInMaxPeriod; + result = 31 * result + sessionCountInWindow; + result = 31 * result + hashLong(inQuotaTimeElapsed); + result = 31 * result + hashLong(jobRateLimitExpirationTimeElapsed); + result = 31 * result + jobCountInRateLimitingWindow; + result = 31 * result + hashLong(sessionRateLimitExpirationTimeElapsed); + result = 31 * result + sessionCountInRateLimitingWindow; + return result; + } + } + + /** List of all tracked jobs keyed by source package-userId combo. */ + private final UserPackageMap<ArraySet<JobStatus>> mTrackedJobs = new UserPackageMap<>(); + + /** Timer for each package-userId combo. */ + private final UserPackageMap<Timer> mPkgTimers = new UserPackageMap<>(); + + /** List of all timing sessions for a package-userId combo, in chronological order. */ + private final UserPackageMap<List<TimingSession>> mTimingSessions = new UserPackageMap<>(); + + /** + * List of alarm listeners for each package that listen for when each package comes back within + * quota. + */ + private final UserPackageMap<QcAlarmListener> mInQuotaAlarmListeners = new UserPackageMap<>(); + + /** Cached calculation results for each app, with the standby buckets as the array indices. */ + private final UserPackageMap<ExecutionStats[]> mExecutionStatsCache = new UserPackageMap<>(); + + /** List of UIDs currently in the foreground. */ + private final SparseBooleanArray mForegroundUids = new SparseBooleanArray(); + + /** Cached mapping of UIDs (for all users) to a list of packages in the UID. */ + private final SparseSetArray<String> mUidToPackageCache = new SparseSetArray<>(); + + /** + * List of jobs that started while the UID was in the TOP state. There will be no more than + * 16 ({@link JobSchedulerService#MAX_JOB_CONTEXTS_COUNT}) running at once, so an ArraySet is + * fine. + */ + private final ArraySet<JobStatus> mTopStartedJobs = new ArraySet<>(); + + private final ActivityManagerInternal mActivityManagerInternal; + private final AlarmManager mAlarmManager; + private final ChargingTracker mChargeTracker; + private final Handler mHandler; + private final QcConstants mQcConstants; + + private volatile boolean mInParole; + + /** How much time each app will have to run jobs within their standby bucket window. */ + private long mAllowedTimePerPeriodMs = QcConstants.DEFAULT_ALLOWED_TIME_PER_PERIOD_MS; + + /** + * The maximum amount of time an app can have its jobs running within a {@link #MAX_PERIOD_MS} + * window. + */ + private long mMaxExecutionTimeMs = QcConstants.DEFAULT_MAX_EXECUTION_TIME_MS; + + /** + * How much time the app should have before transitioning from out-of-quota to in-quota. + * This should not affect processing if the app is already in-quota. + */ + private long mQuotaBufferMs = QcConstants.DEFAULT_IN_QUOTA_BUFFER_MS; + + /** + * {@link #mAllowedTimePerPeriodMs} - {@link #mQuotaBufferMs}. This can be used to determine + * when an app will have enough quota to transition from out-of-quota to in-quota. + */ + private long mAllowedTimeIntoQuotaMs = mAllowedTimePerPeriodMs - mQuotaBufferMs; + + /** + * {@link #mMaxExecutionTimeMs} - {@link #mQuotaBufferMs}. This can be used to determine when an + * app will have enough quota to transition from out-of-quota to in-quota. + */ + private long mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs; + + /** The period of time used to rate limit recently run jobs. */ + private long mRateLimitingWindowMs = QcConstants.DEFAULT_RATE_LIMITING_WINDOW_MS; + + /** The maximum number of jobs that can run within the past {@link #mRateLimitingWindowMs}. */ + private int mMaxJobCountPerRateLimitingWindow = + QcConstants.DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW; + + /** + * The maximum number of {@link TimingSession}s that can run within the past {@link + * #mRateLimitingWindowMs}. + */ + private int mMaxSessionCountPerRateLimitingWindow = + QcConstants.DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW; + + private long mNextCleanupTimeElapsed = 0; + private final AlarmManager.OnAlarmListener mSessionCleanupAlarmListener = + new AlarmManager.OnAlarmListener() { + @Override + public void onAlarm() { + mHandler.obtainMessage(MSG_CLEAN_UP_SESSIONS).sendToTarget(); + } + }; + + private final IUidObserver mUidObserver = new IUidObserver.Stub() { + @Override + public void onUidStateChanged(int uid, int procState, long procStateSeq) { + mHandler.obtainMessage(MSG_UID_PROCESS_STATE_CHANGED, uid, procState).sendToTarget(); + } + + @Override + public void onUidGone(int uid, boolean disabled) { + } + + @Override + public void onUidActive(int uid) { + } + + @Override + public void onUidIdle(int uid, boolean disabled) { + } + + @Override + public void onUidCachedChanged(int uid, boolean cached) { + } + }; + + private final BroadcastReceiver mPackageAddedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent == null) { + return; + } + if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { + return; + } + final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1); + synchronized (mLock) { + mUidToPackageCache.remove(uid); + } + } + }; + + /** + * The rolling window size for each standby bucket. Within each window, an app will have 10 + * minutes to run its jobs. + */ + private final long[] mBucketPeriodsMs = new long[]{ + QcConstants.DEFAULT_WINDOW_SIZE_ACTIVE_MS, + QcConstants.DEFAULT_WINDOW_SIZE_WORKING_MS, + QcConstants.DEFAULT_WINDOW_SIZE_FREQUENT_MS, + QcConstants.DEFAULT_WINDOW_SIZE_RARE_MS + }; + + /** The maximum period any bucket can have. */ + private static final long MAX_PERIOD_MS = 24 * 60 * MINUTE_IN_MILLIS; + + /** + * The maximum number of jobs based on its standby bucket. For each max value count in the + * array, the app will not be allowed to run more than that many number of jobs within the + * latest time interval of its rolling window size. + * + * @see #mBucketPeriodsMs + */ + private final int[] mMaxBucketJobCounts = new int[]{ + QcConstants.DEFAULT_MAX_JOB_COUNT_ACTIVE, + QcConstants.DEFAULT_MAX_JOB_COUNT_WORKING, + QcConstants.DEFAULT_MAX_JOB_COUNT_FREQUENT, + QcConstants.DEFAULT_MAX_JOB_COUNT_RARE + }; + + /** + * The maximum number of {@link TimingSession}s based on its standby bucket. For each max value + * count in the array, the app will not be allowed to have more than that many number of + * {@link TimingSession}s within the latest time interval of its rolling window size. + * + * @see #mBucketPeriodsMs + */ + private final int[] mMaxBucketSessionCounts = new int[]{ + QcConstants.DEFAULT_MAX_SESSION_COUNT_ACTIVE, + QcConstants.DEFAULT_MAX_SESSION_COUNT_WORKING, + QcConstants.DEFAULT_MAX_SESSION_COUNT_FREQUENT, + QcConstants.DEFAULT_MAX_SESSION_COUNT_RARE + }; + + /** + * Treat two distinct {@link TimingSession}s as the same if they start and end within this + * amount of time of each other. + */ + private long mTimingSessionCoalescingDurationMs = + QcConstants.DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS; + + /** An app has reached its quota. The message should contain a {@link Package} object. */ + private static final int MSG_REACHED_QUOTA = 0; + /** Drop any old timing sessions. */ + private static final int MSG_CLEAN_UP_SESSIONS = 1; + /** Check if a package is now within its quota. */ + private static final int MSG_CHECK_PACKAGE = 2; + /** Process state for a UID has changed. */ + private static final int MSG_UID_PROCESS_STATE_CHANGED = 3; + + public QuotaController(JobSchedulerService service) { + super(service); + mHandler = new QcHandler(mContext.getMainLooper()); + mChargeTracker = new ChargingTracker(); + mChargeTracker.startTracking(); + mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class); + mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); + mQcConstants = new QcConstants(mHandler); + + final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); + mContext.registerReceiverAsUser(mPackageAddedReceiver, UserHandle.ALL, filter, null, null); + + // Set up the app standby bucketing tracker + UsageStatsManagerInternal usageStats = LocalServices.getService( + UsageStatsManagerInternal.class); + usageStats.addAppIdleStateChangeListener(new StandbyTracker()); + + try { + ActivityManager.getService().registerUidObserver(mUidObserver, + ActivityManager.UID_OBSERVER_PROCSTATE, + ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, null); + } catch (RemoteException e) { + // ignored; both services live in system_server + } + } + + @Override + public void onSystemServicesReady() { + mQcConstants.start(mContext.getContentResolver()); + } + + @Override + public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) { + final int userId = jobStatus.getSourceUserId(); + final String pkgName = jobStatus.getSourcePackageName(); + ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName); + if (jobs == null) { + jobs = new ArraySet<>(); + mTrackedJobs.add(userId, pkgName, jobs); + } + jobs.add(jobStatus); + jobStatus.setTrackingController(JobStatus.TRACKING_QUOTA); + final boolean isWithinQuota = isWithinQuotaLocked(jobStatus); + setConstraintSatisfied(jobStatus, isWithinQuota); + if (!isWithinQuota) { + maybeScheduleStartAlarmLocked(userId, pkgName, jobStatus.getEffectiveStandbyBucket()); + } + } + + @Override + public void prepareForExecutionLocked(JobStatus jobStatus) { + if (DEBUG) { + Slog.d(TAG, "Prepping for " + jobStatus.toShortString()); + } + + final int uid = jobStatus.getSourceUid(); + if (mActivityManagerInternal.getUidProcessState(uid) <= ActivityManager.PROCESS_STATE_TOP) { + if (DEBUG) { + Slog.d(TAG, jobStatus.toShortString() + " is top started job"); + } + mTopStartedJobs.add(jobStatus); + // Top jobs won't count towards quota so there's no need to involve the Timer. + return; + } + + final int userId = jobStatus.getSourceUserId(); + final String packageName = jobStatus.getSourcePackageName(); + Timer timer = mPkgTimers.get(userId, packageName); + if (timer == null) { + timer = new Timer(uid, userId, packageName); + mPkgTimers.add(userId, packageName, timer); + } + timer.startTrackingJobLocked(jobStatus); + } + + @Override + public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, + boolean forUpdate) { + if (jobStatus.clearTrackingController(JobStatus.TRACKING_QUOTA)) { + Timer timer = mPkgTimers.get(jobStatus.getSourceUserId(), + jobStatus.getSourcePackageName()); + if (timer != null) { + timer.stopTrackingJob(jobStatus); + } + ArraySet<JobStatus> jobs = mTrackedJobs.get(jobStatus.getSourceUserId(), + jobStatus.getSourcePackageName()); + if (jobs != null) { + jobs.remove(jobStatus); + } + mTopStartedJobs.remove(jobStatus); + } + } + + @Override + public void onAppRemovedLocked(String packageName, int uid) { + if (packageName == null) { + Slog.wtf(TAG, "Told app removed but given null package name."); + return; + } + final int userId = UserHandle.getUserId(uid); + mTrackedJobs.delete(userId, packageName); + Timer timer = mPkgTimers.get(userId, packageName); + if (timer != null) { + if (timer.isActive()) { + Slog.wtf(TAG, "onAppRemovedLocked called before Timer turned off."); + timer.dropEverythingLocked(); + } + mPkgTimers.delete(userId, packageName); + } + mTimingSessions.delete(userId, packageName); + QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName); + if (alarmListener != null) { + mAlarmManager.cancel(alarmListener); + mInQuotaAlarmListeners.delete(userId, packageName); + } + mExecutionStatsCache.delete(userId, packageName); + mForegroundUids.delete(uid); + mUidToPackageCache.remove(uid); + } + + @Override + public void onUserRemovedLocked(int userId) { + mTrackedJobs.delete(userId); + mPkgTimers.delete(userId); + mTimingSessions.delete(userId); + mInQuotaAlarmListeners.delete(userId); + mExecutionStatsCache.delete(userId); + mUidToPackageCache.clear(); + } + + private boolean isUidInForeground(int uid) { + if (UserHandle.isCore(uid)) { + return true; + } + synchronized (mLock) { + return mForegroundUids.get(uid); + } + } + + /** @return true if the job was started while the app was in the TOP state. */ + private boolean isTopStartedJobLocked(@NonNull final JobStatus jobStatus) { + return mTopStartedJobs.contains(jobStatus); + } + + /** Returns the maximum amount of time this job could run for. */ + public long getMaxJobExecutionTimeMsLocked(@NonNull final JobStatus jobStatus) { + // If quota is currently "free", then the job can run for the full amount of time. + if (mChargeTracker.isCharging() + || mInParole + || isTopStartedJobLocked(jobStatus) + || isUidInForeground(jobStatus.getSourceUid())) { + return JobServiceContext.EXECUTING_TIMESLICE_MILLIS; + } + return getRemainingExecutionTimeLocked(jobStatus); + } + + @VisibleForTesting + boolean isWithinQuotaLocked(@NonNull final JobStatus jobStatus) { + final int standbyBucket = jobStatus.getEffectiveStandbyBucket(); + // A job is within quota if one of the following is true: + // 1. it was started while the app was in the TOP state + // 2. the app is currently in the foreground + // 3. the app overall is within its quota + return isTopStartedJobLocked(jobStatus) + || isUidInForeground(jobStatus.getSourceUid()) + || isWithinQuotaLocked( + jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(), standbyBucket); + } + + @VisibleForTesting + boolean isWithinQuotaLocked(final int userId, @NonNull final String packageName, + final int standbyBucket) { + if (standbyBucket == NEVER_INDEX) return false; + + // Quota constraint is not enforced while charging or when parole is on. + if (mChargeTracker.isCharging() || mInParole) { + return true; + } + + ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket); + return getRemainingExecutionTimeLocked(stats) > 0 + && isUnderJobCountQuotaLocked(stats, standbyBucket) + && isUnderSessionCountQuotaLocked(stats, standbyBucket); + } + + private boolean isUnderJobCountQuotaLocked(@NonNull ExecutionStats stats, + final int standbyBucket) { + final long now = sElapsedRealtimeClock.millis(); + final boolean isUnderAllowedTimeQuota = + (stats.jobRateLimitExpirationTimeElapsed <= now + || stats.jobCountInRateLimitingWindow < mMaxJobCountPerRateLimitingWindow); + return isUnderAllowedTimeQuota + && (stats.bgJobCountInWindow < mMaxBucketJobCounts[standbyBucket]); + } + + private boolean isUnderSessionCountQuotaLocked(@NonNull ExecutionStats stats, + final int standbyBucket) { + final long now = sElapsedRealtimeClock.millis(); + final boolean isUnderAllowedTimeQuota = (stats.sessionRateLimitExpirationTimeElapsed <= now + || stats.sessionCountInRateLimitingWindow < mMaxSessionCountPerRateLimitingWindow); + return isUnderAllowedTimeQuota + && stats.sessionCountInWindow < mMaxBucketSessionCounts[standbyBucket]; + } + + @VisibleForTesting + long getRemainingExecutionTimeLocked(@NonNull final JobStatus jobStatus) { + return getRemainingExecutionTimeLocked(jobStatus.getSourceUserId(), + jobStatus.getSourcePackageName(), + jobStatus.getEffectiveStandbyBucket()); + } + + @VisibleForTesting + long getRemainingExecutionTimeLocked(final int userId, @NonNull final String packageName) { + final int standbyBucket = JobSchedulerService.standbyBucketForPackage(packageName, + userId, sElapsedRealtimeClock.millis()); + return getRemainingExecutionTimeLocked(userId, packageName, standbyBucket); + } + + /** + * Returns the amount of time, in milliseconds, that this job has remaining to run based on its + * current standby bucket. Time remaining could be negative if the app was moved from a less + * restricted to a more restricted bucket. + */ + private long getRemainingExecutionTimeLocked(final int userId, + @NonNull final String packageName, final int standbyBucket) { + if (standbyBucket == NEVER_INDEX) { + return 0; + } + return getRemainingExecutionTimeLocked( + getExecutionStatsLocked(userId, packageName, standbyBucket)); + } + + private long getRemainingExecutionTimeLocked(@NonNull ExecutionStats stats) { + return Math.min(mAllowedTimePerPeriodMs - stats.executionTimeInWindowMs, + mMaxExecutionTimeMs - stats.executionTimeInMaxPeriodMs); + } + + /** + * Returns the amount of time, in milliseconds, until the package would have reached its + * duration quota, assuming it has a job counting towards its quota the entire time. This takes + * into account any {@link TimingSession}s that may roll out of the window as the job is + * running. + */ + @VisibleForTesting + long getTimeUntilQuotaConsumedLocked(final int userId, @NonNull final String packageName) { + final long nowElapsed = sElapsedRealtimeClock.millis(); + final int standbyBucket = JobSchedulerService.standbyBucketForPackage( + packageName, userId, nowElapsed); + if (standbyBucket == NEVER_INDEX) { + return 0; + } + List<TimingSession> sessions = mTimingSessions.get(userId, packageName); + if (sessions == null || sessions.size() == 0) { + return mAllowedTimePerPeriodMs; + } + + final ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket); + final long startWindowElapsed = nowElapsed - stats.windowSizeMs; + final long startMaxElapsed = nowElapsed - MAX_PERIOD_MS; + final long allowedTimeRemainingMs = mAllowedTimePerPeriodMs - stats.executionTimeInWindowMs; + final long maxExecutionTimeRemainingMs = + mMaxExecutionTimeMs - stats.executionTimeInMaxPeriodMs; + + // Regular ACTIVE case. Since the bucket size equals the allowed time, the app jobs can + // essentially run until they reach the maximum limit. + if (stats.windowSizeMs == mAllowedTimePerPeriodMs) { + return calculateTimeUntilQuotaConsumedLocked( + sessions, startMaxElapsed, maxExecutionTimeRemainingMs); + } + + // Need to check both max time and period time in case one is less than the other. + // For example, max time remaining could be less than bucket time remaining, but sessions + // contributing to the max time remaining could phase out enough that we'd want to use the + // bucket value. + return Math.min( + calculateTimeUntilQuotaConsumedLocked( + sessions, startMaxElapsed, maxExecutionTimeRemainingMs), + calculateTimeUntilQuotaConsumedLocked( + sessions, startWindowElapsed, allowedTimeRemainingMs)); + } + + /** + * Calculates how much time it will take, in milliseconds, until the quota is fully consumed. + * + * @param windowStartElapsed The start of the window, in the elapsed realtime timebase. + * @param deadSpaceMs How much time can be allowed to count towards the quota + */ + private long calculateTimeUntilQuotaConsumedLocked(@NonNull List<TimingSession> sessions, + final long windowStartElapsed, long deadSpaceMs) { + long timeUntilQuotaConsumedMs = 0; + long start = windowStartElapsed; + for (int i = 0; i < sessions.size(); ++i) { + TimingSession session = sessions.get(i); + + if (session.endTimeElapsed < windowStartElapsed) { + // Outside of window. Ignore. + continue; + } else if (session.startTimeElapsed <= windowStartElapsed) { + // Overlapping session. Can extend time by portion of session in window. + timeUntilQuotaConsumedMs += session.endTimeElapsed - windowStartElapsed; + start = session.endTimeElapsed; + } else { + // Completely within the window. Can only consider if there's enough dead space + // to get to the start of the session. + long diff = session.startTimeElapsed - start; + if (diff > deadSpaceMs) { + break; + } + timeUntilQuotaConsumedMs += diff + + (session.endTimeElapsed - session.startTimeElapsed); + deadSpaceMs -= diff; + start = session.endTimeElapsed; + } + } + // Will be non-zero if the loop didn't look at any sessions. + timeUntilQuotaConsumedMs += deadSpaceMs; + if (timeUntilQuotaConsumedMs > mMaxExecutionTimeMs) { + Slog.wtf(TAG, "Calculated quota consumed time too high: " + timeUntilQuotaConsumedMs); + } + return timeUntilQuotaConsumedMs; + } + + /** Returns the execution stats of the app in the most recent window. */ + @VisibleForTesting + @NonNull + ExecutionStats getExecutionStatsLocked(final int userId, @NonNull final String packageName, + final int standbyBucket) { + return getExecutionStatsLocked(userId, packageName, standbyBucket, true); + } + + @NonNull + private ExecutionStats getExecutionStatsLocked(final int userId, + @NonNull final String packageName, final int standbyBucket, + final boolean refreshStatsIfOld) { + if (standbyBucket == NEVER_INDEX) { + Slog.wtf(TAG, "getExecutionStatsLocked called for a NEVER app."); + return new ExecutionStats(); + } + ExecutionStats[] appStats = mExecutionStatsCache.get(userId, packageName); + if (appStats == null) { + appStats = new ExecutionStats[mBucketPeriodsMs.length]; + mExecutionStatsCache.add(userId, packageName, appStats); + } + ExecutionStats stats = appStats[standbyBucket]; + if (stats == null) { + stats = new ExecutionStats(); + appStats[standbyBucket] = stats; + } + if (refreshStatsIfOld) { + final long bucketWindowSizeMs = mBucketPeriodsMs[standbyBucket]; + final int jobCountLimit = mMaxBucketJobCounts[standbyBucket]; + final int sessionCountLimit = mMaxBucketSessionCounts[standbyBucket]; + Timer timer = mPkgTimers.get(userId, packageName); + if ((timer != null && timer.isActive()) + || stats.expirationTimeElapsed <= sElapsedRealtimeClock.millis() + || stats.windowSizeMs != bucketWindowSizeMs + || stats.jobCountLimit != jobCountLimit + || stats.sessionCountLimit != sessionCountLimit) { + // The stats are no longer valid. + stats.windowSizeMs = bucketWindowSizeMs; + stats.jobCountLimit = jobCountLimit; + stats.sessionCountLimit = sessionCountLimit; + updateExecutionStatsLocked(userId, packageName, stats); + } + } + + return stats; + } + + @VisibleForTesting + void updateExecutionStatsLocked(final int userId, @NonNull final String packageName, + @NonNull ExecutionStats stats) { + stats.executionTimeInWindowMs = 0; + stats.bgJobCountInWindow = 0; + stats.executionTimeInMaxPeriodMs = 0; + stats.bgJobCountInMaxPeriod = 0; + stats.sessionCountInWindow = 0; + stats.inQuotaTimeElapsed = 0; + + Timer timer = mPkgTimers.get(userId, packageName); + final long nowElapsed = sElapsedRealtimeClock.millis(); + stats.expirationTimeElapsed = nowElapsed + MAX_PERIOD_MS; + if (timer != null && timer.isActive()) { + stats.executionTimeInWindowMs = + stats.executionTimeInMaxPeriodMs = timer.getCurrentDuration(nowElapsed); + stats.bgJobCountInWindow = stats.bgJobCountInMaxPeriod = timer.getBgJobCount(); + // If the timer is active, the value will be stale at the next method call, so + // invalidate now. + stats.expirationTimeElapsed = nowElapsed; + if (stats.executionTimeInWindowMs >= mAllowedTimeIntoQuotaMs) { + stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, + nowElapsed - mAllowedTimeIntoQuotaMs + stats.windowSizeMs); + } + if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) { + stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, + nowElapsed - mMaxExecutionTimeIntoQuotaMs + MAX_PERIOD_MS); + } + } + + List<TimingSession> sessions = mTimingSessions.get(userId, packageName); + if (sessions == null || sessions.size() == 0) { + return; + } + + final long startWindowElapsed = nowElapsed - stats.windowSizeMs; + final long startMaxElapsed = nowElapsed - MAX_PERIOD_MS; + int sessionCountInWindow = 0; + // The minimum time between the start time and the beginning of the sessions that were + // looked at --> how much time the stats will be valid for. + long emptyTimeMs = Long.MAX_VALUE; + // Sessions are non-overlapping and in order of occurrence, so iterating backwards will get + // the most recent ones. + final int loopStart = sessions.size() - 1; + for (int i = loopStart; i >= 0; --i) { + TimingSession session = sessions.get(i); + + // Window management. + if (startWindowElapsed < session.endTimeElapsed) { + final long start; + if (startWindowElapsed < session.startTimeElapsed) { + start = session.startTimeElapsed; + emptyTimeMs = + Math.min(emptyTimeMs, session.startTimeElapsed - startWindowElapsed); + } else { + // The session started before the window but ended within the window. Only + // include the portion that was within the window. + start = startWindowElapsed; + emptyTimeMs = 0; + } + + stats.executionTimeInWindowMs += session.endTimeElapsed - start; + stats.bgJobCountInWindow += session.bgJobCount; + if (stats.executionTimeInWindowMs >= mAllowedTimeIntoQuotaMs) { + stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, + start + stats.executionTimeInWindowMs - mAllowedTimeIntoQuotaMs + + stats.windowSizeMs); + } + if (stats.bgJobCountInWindow >= stats.jobCountLimit) { + stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, + session.endTimeElapsed + stats.windowSizeMs); + } + if (i == loopStart + || (sessions.get(i + 1).startTimeElapsed - session.endTimeElapsed) + > mTimingSessionCoalescingDurationMs) { + // Coalesce sessions if they are very close to each other in time + sessionCountInWindow++; + + if (sessionCountInWindow >= stats.sessionCountLimit) { + stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, + session.endTimeElapsed + stats.windowSizeMs); + } + } + } + + // Max period check. + if (startMaxElapsed < session.startTimeElapsed) { + stats.executionTimeInMaxPeriodMs += + session.endTimeElapsed - session.startTimeElapsed; + stats.bgJobCountInMaxPeriod += session.bgJobCount; + emptyTimeMs = Math.min(emptyTimeMs, session.startTimeElapsed - startMaxElapsed); + if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) { + stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, + session.startTimeElapsed + stats.executionTimeInMaxPeriodMs + - mMaxExecutionTimeIntoQuotaMs + MAX_PERIOD_MS); + } + } else if (startMaxElapsed < session.endTimeElapsed) { + // The session started before the window but ended within the window. Only include + // the portion that was within the window. + stats.executionTimeInMaxPeriodMs += session.endTimeElapsed - startMaxElapsed; + stats.bgJobCountInMaxPeriod += session.bgJobCount; + emptyTimeMs = 0; + if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) { + stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, + startMaxElapsed + stats.executionTimeInMaxPeriodMs + - mMaxExecutionTimeIntoQuotaMs + MAX_PERIOD_MS); + } + } else { + // This session ended before the window. No point in going any further. + break; + } + } + stats.expirationTimeElapsed = nowElapsed + emptyTimeMs; + stats.sessionCountInWindow = sessionCountInWindow; + } + + /** Invalidate ExecutionStats for all apps. */ + @VisibleForTesting + void invalidateAllExecutionStatsLocked() { + final long nowElapsed = sElapsedRealtimeClock.millis(); + mExecutionStatsCache.forEach((appStats) -> { + if (appStats != null) { + for (int i = 0; i < appStats.length; ++i) { + ExecutionStats stats = appStats[i]; + if (stats != null) { + stats.expirationTimeElapsed = nowElapsed; + } + } + } + }); + } + + @VisibleForTesting + void invalidateAllExecutionStatsLocked(final int userId, + @NonNull final String packageName) { + ExecutionStats[] appStats = mExecutionStatsCache.get(userId, packageName); + if (appStats != null) { + final long nowElapsed = sElapsedRealtimeClock.millis(); + for (int i = 0; i < appStats.length; ++i) { + ExecutionStats stats = appStats[i]; + if (stats != null) { + stats.expirationTimeElapsed = nowElapsed; + } + } + } + } + + @VisibleForTesting + void incrementJobCount(final int userId, @NonNull final String packageName, int count) { + final long now = sElapsedRealtimeClock.millis(); + ExecutionStats[] appStats = mExecutionStatsCache.get(userId, packageName); + if (appStats == null) { + appStats = new ExecutionStats[mBucketPeriodsMs.length]; + mExecutionStatsCache.add(userId, packageName, appStats); + } + for (int i = 0; i < appStats.length; ++i) { + ExecutionStats stats = appStats[i]; + if (stats == null) { + stats = new ExecutionStats(); + appStats[i] = stats; + } + if (stats.jobRateLimitExpirationTimeElapsed <= now) { + stats.jobRateLimitExpirationTimeElapsed = now + mRateLimitingWindowMs; + stats.jobCountInRateLimitingWindow = 0; + } + stats.jobCountInRateLimitingWindow += count; + } + } + + private void incrementTimingSessionCount(final int userId, @NonNull final String packageName) { + final long now = sElapsedRealtimeClock.millis(); + ExecutionStats[] appStats = mExecutionStatsCache.get(userId, packageName); + if (appStats == null) { + appStats = new ExecutionStats[mBucketPeriodsMs.length]; + mExecutionStatsCache.add(userId, packageName, appStats); + } + for (int i = 0; i < appStats.length; ++i) { + ExecutionStats stats = appStats[i]; + if (stats == null) { + stats = new ExecutionStats(); + appStats[i] = stats; + } + if (stats.sessionRateLimitExpirationTimeElapsed <= now) { + stats.sessionRateLimitExpirationTimeElapsed = now + mRateLimitingWindowMs; + stats.sessionCountInRateLimitingWindow = 0; + } + stats.sessionCountInRateLimitingWindow++; + } + } + + @VisibleForTesting + void saveTimingSession(final int userId, @NonNull final String packageName, + @NonNull final TimingSession session) { + synchronized (mLock) { + List<TimingSession> sessions = mTimingSessions.get(userId, packageName); + if (sessions == null) { + sessions = new ArrayList<>(); + mTimingSessions.add(userId, packageName, sessions); + } + sessions.add(session); + // Adding a new session means that the current stats are now incorrect. + invalidateAllExecutionStatsLocked(userId, packageName); + + maybeScheduleCleanupAlarmLocked(); + } + } + + private final class EarliestEndTimeFunctor implements Consumer<List<TimingSession>> { + public long earliestEndElapsed = Long.MAX_VALUE; + + @Override + public void accept(List<TimingSession> sessions) { + if (sessions != null && sessions.size() > 0) { + earliestEndElapsed = Math.min(earliestEndElapsed, sessions.get(0).endTimeElapsed); + } + } + + void reset() { + earliestEndElapsed = Long.MAX_VALUE; + } + } + + private final EarliestEndTimeFunctor mEarliestEndTimeFunctor = new EarliestEndTimeFunctor(); + + /** Schedule a cleanup alarm if necessary and there isn't already one scheduled. */ + @VisibleForTesting + void maybeScheduleCleanupAlarmLocked() { + if (mNextCleanupTimeElapsed > sElapsedRealtimeClock.millis()) { + // There's already an alarm scheduled. Just stick with that one. There's no way we'll + // end up scheduling an earlier alarm. + if (DEBUG) { + Slog.v(TAG, "Not scheduling cleanup since there's already one at " + + mNextCleanupTimeElapsed + " (in " + (mNextCleanupTimeElapsed + - sElapsedRealtimeClock.millis()) + "ms)"); + } + return; + } + mEarliestEndTimeFunctor.reset(); + mTimingSessions.forEach(mEarliestEndTimeFunctor); + final long earliestEndElapsed = mEarliestEndTimeFunctor.earliestEndElapsed; + if (earliestEndElapsed == Long.MAX_VALUE) { + // Couldn't find a good time to clean up. Maybe this was called after we deleted all + // timing sessions. + if (DEBUG) { + Slog.d(TAG, "Didn't find a time to schedule cleanup"); + } + return; + } + // Need to keep sessions for all apps up to the max period, regardless of their current + // standby bucket. + long nextCleanupElapsed = earliestEndElapsed + MAX_PERIOD_MS; + if (nextCleanupElapsed - mNextCleanupTimeElapsed <= 10 * MINUTE_IN_MILLIS) { + // No need to clean up too often. Delay the alarm if the next cleanup would be too soon + // after it. + nextCleanupElapsed += 10 * MINUTE_IN_MILLIS; + } + mNextCleanupTimeElapsed = nextCleanupElapsed; + mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, nextCleanupElapsed, ALARM_TAG_CLEANUP, + mSessionCleanupAlarmListener, mHandler); + if (DEBUG) { + Slog.d(TAG, "Scheduled next cleanup for " + mNextCleanupTimeElapsed); + } + } + + private void handleNewChargingStateLocked() { + final long nowElapsed = sElapsedRealtimeClock.millis(); + final boolean isCharging = mChargeTracker.isCharging(); + if (DEBUG) { + Slog.d(TAG, "handleNewChargingStateLocked: " + isCharging); + } + // Deal with Timers first. + mPkgTimers.forEach((t) -> t.onStateChangedLocked(nowElapsed, isCharging)); + // Now update jobs. + maybeUpdateAllConstraintsLocked(); + } + + private void maybeUpdateAllConstraintsLocked() { + boolean changed = false; + for (int u = 0; u < mTrackedJobs.numUsers(); ++u) { + final int userId = mTrackedJobs.keyAt(u); + for (int p = 0; p < mTrackedJobs.numPackagesForUser(userId); ++p) { + final String packageName = mTrackedJobs.keyAt(u, p); + changed |= maybeUpdateConstraintForPkgLocked(userId, packageName); + } + } + if (changed) { + mStateChangedListener.onControllerStateChanged(); + } + } + + /** + * Update the CONSTRAINT_WITHIN_QUOTA bit for all of the Jobs for a given package. + * + * @return true if at least one job had its bit changed + */ + private boolean maybeUpdateConstraintForPkgLocked(final int userId, + @NonNull final String packageName) { + ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, packageName); + if (jobs == null || jobs.size() == 0) { + return false; + } + + // Quota is the same for all jobs within a package. + final int realStandbyBucket = jobs.valueAt(0).getStandbyBucket(); + final boolean realInQuota = isWithinQuotaLocked(userId, packageName, realStandbyBucket); + boolean changed = false; + for (int i = jobs.size() - 1; i >= 0; --i) { + final JobStatus js = jobs.valueAt(i); + if (isTopStartedJobLocked(js)) { + // Job was started while the app was in the TOP state so we should allow it to + // finish. + changed |= js.setQuotaConstraintSatisfied(true); + } else if (realStandbyBucket != ACTIVE_INDEX + && realStandbyBucket == js.getEffectiveStandbyBucket()) { + // An app in the ACTIVE bucket may be out of quota while the job could be in quota + // for some reason. Therefore, avoid setting the real value here and check each job + // individually. + changed |= setConstraintSatisfied(js, realInQuota); + } else { + // This job is somehow exempted. Need to determine its own quota status. + changed |= setConstraintSatisfied(js, isWithinQuotaLocked(js)); + } + } + if (!realInQuota) { + // Don't want to use the effective standby bucket here since that bump the bucket to + // ACTIVE for one of the jobs, which doesn't help with other jobs that aren't + // exempted. + maybeScheduleStartAlarmLocked(userId, packageName, realStandbyBucket); + } else { + QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName); + if (alarmListener != null && alarmListener.isWaiting()) { + mAlarmManager.cancel(alarmListener); + // Set the trigger time to 0 so that the alarm doesn't think it's still waiting. + alarmListener.setTriggerTime(0); + } + } + return changed; + } + + private class UidConstraintUpdater implements Consumer<JobStatus> { + private final UserPackageMap<Integer> mToScheduleStartAlarms = new UserPackageMap<>(); + public boolean wasJobChanged; + + @Override + public void accept(JobStatus jobStatus) { + wasJobChanged |= setConstraintSatisfied(jobStatus, isWithinQuotaLocked(jobStatus)); + final int userId = jobStatus.getSourceUserId(); + final String packageName = jobStatus.getSourcePackageName(); + final int realStandbyBucket = jobStatus.getStandbyBucket(); + if (isWithinQuotaLocked(userId, packageName, realStandbyBucket)) { + QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName); + if (alarmListener != null && alarmListener.isWaiting()) { + mAlarmManager.cancel(alarmListener); + // Set the trigger time to 0 so that the alarm doesn't think it's still waiting. + alarmListener.setTriggerTime(0); + } + } else { + mToScheduleStartAlarms.add(userId, packageName, realStandbyBucket); + } + } + + void postProcess() { + for (int u = 0; u < mToScheduleStartAlarms.numUsers(); ++u) { + final int userId = mToScheduleStartAlarms.keyAt(u); + for (int p = 0; p < mToScheduleStartAlarms.numPackagesForUser(userId); ++p) { + final String packageName = mToScheduleStartAlarms.keyAt(u, p); + final int standbyBucket = mToScheduleStartAlarms.get(userId, packageName); + maybeScheduleStartAlarmLocked(userId, packageName, standbyBucket); + } + } + } + + void reset() { + wasJobChanged = false; + mToScheduleStartAlarms.clear(); + } + } + + private final UidConstraintUpdater mUpdateUidConstraints = new UidConstraintUpdater(); + + private boolean maybeUpdateConstraintForUidLocked(final int uid) { + mService.getJobStore().forEachJobForSourceUid(uid, mUpdateUidConstraints); + + mUpdateUidConstraints.postProcess(); + boolean changed = mUpdateUidConstraints.wasJobChanged; + mUpdateUidConstraints.reset(); + return changed; + } + + /** + * Maybe schedule a non-wakeup alarm for the next time this package will have quota to run + * again. This should only be called if the package is already out of quota. + */ + @VisibleForTesting + void maybeScheduleStartAlarmLocked(final int userId, @NonNull final String packageName, + final int standbyBucket) { + if (standbyBucket == NEVER_INDEX) { + return; + } + + final String pkgString = string(userId, packageName); + ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket); + final boolean isUnderJobCountQuota = isUnderJobCountQuotaLocked(stats, standbyBucket); + final boolean isUnderTimingSessionCountQuota = isUnderSessionCountQuotaLocked(stats, + standbyBucket); + + QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName); + if (stats.executionTimeInWindowMs < mAllowedTimePerPeriodMs + && stats.executionTimeInMaxPeriodMs < mMaxExecutionTimeMs + && isUnderJobCountQuota + && isUnderTimingSessionCountQuota) { + // Already in quota. Why was this method called? + if (DEBUG) { + Slog.e(TAG, "maybeScheduleStartAlarmLocked called for " + pkgString + + " even though it already has " + + getRemainingExecutionTimeLocked(userId, packageName, standbyBucket) + + "ms in its quota."); + } + if (alarmListener != null) { + // Cancel any pending alarm. + mAlarmManager.cancel(alarmListener); + // Set the trigger time to 0 so that the alarm doesn't think it's still waiting. + alarmListener.setTriggerTime(0); + } + mHandler.obtainMessage(MSG_CHECK_PACKAGE, userId, 0, packageName).sendToTarget(); + return; + } + + if (alarmListener == null) { + alarmListener = new QcAlarmListener(userId, packageName); + mInQuotaAlarmListeners.add(userId, packageName, alarmListener); + } + + // The time this app will have quota again. + long inQuotaTimeElapsed = stats.inQuotaTimeElapsed; + if (!isUnderJobCountQuota && stats.bgJobCountInWindow < stats.jobCountLimit) { + // App hit the rate limit. + inQuotaTimeElapsed = Math.max(inQuotaTimeElapsed, + stats.jobRateLimitExpirationTimeElapsed); + } + if (!isUnderTimingSessionCountQuota + && stats.sessionCountInWindow < stats.sessionCountLimit) { + // App hit the rate limit. + inQuotaTimeElapsed = Math.max(inQuotaTimeElapsed, + stats.sessionRateLimitExpirationTimeElapsed); + } + // Only schedule the alarm if: + // 1. There isn't one currently scheduled + // 2. The new alarm is significantly earlier than the previous alarm (which could be the + // case if the package moves into a higher standby bucket). If it's earlier but not + // significantly so, then we essentially delay the job a few extra minutes. + // 3. The alarm is after the current alarm by more than the quota buffer. + // TODO: this might be overengineering. Simplify if proven safe. + if (!alarmListener.isWaiting() + || inQuotaTimeElapsed < alarmListener.getTriggerTimeElapsed() - 3 * MINUTE_IN_MILLIS + || alarmListener.getTriggerTimeElapsed() < inQuotaTimeElapsed) { + if (DEBUG) { + Slog.d(TAG, "Scheduling start alarm for " + pkgString); + } + // If the next time this app will have quota is at least 3 minutes before the + // alarm is supposed to go off, reschedule the alarm. + mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, inQuotaTimeElapsed, + ALARM_TAG_QUOTA_CHECK, alarmListener, mHandler); + alarmListener.setTriggerTime(inQuotaTimeElapsed); + } else if (DEBUG) { + Slog.d(TAG, "No need to schedule start alarm for " + pkgString); + } + } + + private boolean setConstraintSatisfied(@NonNull JobStatus jobStatus, boolean isWithinQuota) { + if (!isWithinQuota && jobStatus.getWhenStandbyDeferred() == 0) { + // Mark that the job is being deferred due to buckets. + jobStatus.setWhenStandbyDeferred(sElapsedRealtimeClock.millis()); + } + return jobStatus.setQuotaConstraintSatisfied(isWithinQuota); + } + + private final class ChargingTracker extends BroadcastReceiver { + /** + * Track whether we're charging. This has a slightly different definition than that of + * BatteryController. + */ + private boolean mCharging; + + ChargingTracker() { + } + + public void startTracking() { + IntentFilter filter = new IntentFilter(); + + // Charging/not charging. + filter.addAction(BatteryManager.ACTION_CHARGING); + filter.addAction(BatteryManager.ACTION_DISCHARGING); + mContext.registerReceiver(this, filter); + + // Initialise tracker state. + BatteryManagerInternal batteryManagerInternal = + LocalServices.getService(BatteryManagerInternal.class); + mCharging = batteryManagerInternal.isPowered(BatteryManager.BATTERY_PLUGGED_ANY); + } + + public boolean isCharging() { + return mCharging; + } + + @Override + public void onReceive(Context context, Intent intent) { + synchronized (mLock) { + final String action = intent.getAction(); + if (BatteryManager.ACTION_CHARGING.equals(action)) { + if (DEBUG) { + Slog.d(TAG, "Received charging intent, fired @ " + + sElapsedRealtimeClock.millis()); + } + mCharging = true; + handleNewChargingStateLocked(); + } else if (BatteryManager.ACTION_DISCHARGING.equals(action)) { + if (DEBUG) { + Slog.d(TAG, "Disconnected from power."); + } + mCharging = false; + handleNewChargingStateLocked(); + } + } + } + } + + @VisibleForTesting + static final class TimingSession { + // Start timestamp in elapsed realtime timebase. + public final long startTimeElapsed; + // End timestamp in elapsed realtime timebase. + public final long endTimeElapsed; + // How many background jobs ran during this session. + public final int bgJobCount; + + private final int mHashCode; + + TimingSession(long startElapsed, long endElapsed, int bgJobCount) { + this.startTimeElapsed = startElapsed; + this.endTimeElapsed = endElapsed; + this.bgJobCount = bgJobCount; + + int hashCode = 0; + hashCode = 31 * hashCode + hashLong(startTimeElapsed); + hashCode = 31 * hashCode + hashLong(endTimeElapsed); + hashCode = 31 * hashCode + bgJobCount; + mHashCode = hashCode; + } + + @Override + public String toString() { + return "TimingSession{" + startTimeElapsed + "->" + endTimeElapsed + ", " + bgJobCount + + "}"; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof TimingSession) { + TimingSession other = (TimingSession) obj; + return startTimeElapsed == other.startTimeElapsed + && endTimeElapsed == other.endTimeElapsed + && bgJobCount == other.bgJobCount; + } else { + return false; + } + } + + @Override + public int hashCode() { + return mHashCode; + } + + public void dump(IndentingPrintWriter pw) { + pw.print(startTimeElapsed); + pw.print(" -> "); + pw.print(endTimeElapsed); + pw.print(" ("); + pw.print(endTimeElapsed - startTimeElapsed); + pw.print("), "); + pw.print(bgJobCount); + pw.print(" bg jobs."); + pw.println(); + } + + public void dump(@NonNull ProtoOutputStream proto, long fieldId) { + final long token = proto.start(fieldId); + + proto.write(StateControllerProto.QuotaController.TimingSession.START_TIME_ELAPSED, + startTimeElapsed); + proto.write(StateControllerProto.QuotaController.TimingSession.END_TIME_ELAPSED, + endTimeElapsed); + proto.write(StateControllerProto.QuotaController.TimingSession.BG_JOB_COUNT, + bgJobCount); + + proto.end(token); + } + } + + private final class Timer { + private final Package mPkg; + private final int mUid; + + // List of jobs currently running for this app that started when the app wasn't in the + // foreground. + private final ArraySet<JobStatus> mRunningBgJobs = new ArraySet<>(); + private long mStartTimeElapsed; + private int mBgJobCount; + + Timer(int uid, int userId, String packageName) { + mPkg = new Package(userId, packageName); + mUid = uid; + } + + void startTrackingJobLocked(@NonNull JobStatus jobStatus) { + if (isTopStartedJobLocked(jobStatus)) { + // We intentionally don't pay attention to fg state changes after a TOP job has + // started. + if (DEBUG) { + Slog.v(TAG, + "Timer ignoring " + jobStatus.toShortString() + " because isTop"); + } + return; + } + if (DEBUG) { + Slog.v(TAG, "Starting to track " + jobStatus.toShortString()); + } + // Always track jobs, even when charging. + mRunningBgJobs.add(jobStatus); + if (shouldTrackLocked()) { + mBgJobCount++; + incrementJobCount(mPkg.userId, mPkg.packageName, 1); + if (mRunningBgJobs.size() == 1) { + // Started tracking the first job. + mStartTimeElapsed = sElapsedRealtimeClock.millis(); + // Starting the timer means that all cached execution stats are now incorrect. + invalidateAllExecutionStatsLocked(mPkg.userId, mPkg.packageName); + scheduleCutoff(); + } + } + } + + void stopTrackingJob(@NonNull JobStatus jobStatus) { + if (DEBUG) { + Slog.v(TAG, "Stopping tracking of " + jobStatus.toShortString()); + } + synchronized (mLock) { + if (mRunningBgJobs.size() == 0) { + // maybeStopTrackingJobLocked can be called when an app cancels a job, so a + // timer may not be running when it's asked to stop tracking a job. + if (DEBUG) { + Slog.d(TAG, "Timer isn't tracking any jobs but still told to stop"); + } + return; + } + if (mRunningBgJobs.remove(jobStatus) + && !mChargeTracker.isCharging() && mRunningBgJobs.size() == 0) { + emitSessionLocked(sElapsedRealtimeClock.millis()); + cancelCutoff(); + } + } + } + + /** + * Stops tracking all jobs and cancels any pending alarms. This should only be called if + * the Timer is not going to be used anymore. + */ + void dropEverythingLocked() { + mRunningBgJobs.clear(); + cancelCutoff(); + } + + private void emitSessionLocked(long nowElapsed) { + if (mBgJobCount <= 0) { + // Nothing to emit. + return; + } + TimingSession ts = new TimingSession(mStartTimeElapsed, nowElapsed, mBgJobCount); + saveTimingSession(mPkg.userId, mPkg.packageName, ts); + mBgJobCount = 0; + // Don't reset the tracked jobs list as we need to keep tracking the current number + // of jobs. + // However, cancel the currently scheduled cutoff since it's not currently useful. + cancelCutoff(); + incrementTimingSessionCount(mPkg.userId, mPkg.packageName); + } + + /** + * Returns true if the Timer is actively tracking, as opposed to passively ref counting + * during charging. + */ + public boolean isActive() { + synchronized (mLock) { + return mBgJobCount > 0; + } + } + + boolean isRunning(JobStatus jobStatus) { + return mRunningBgJobs.contains(jobStatus); + } + + long getCurrentDuration(long nowElapsed) { + synchronized (mLock) { + return !isActive() ? 0 : nowElapsed - mStartTimeElapsed; + } + } + + int getBgJobCount() { + synchronized (mLock) { + return mBgJobCount; + } + } + + private boolean shouldTrackLocked() { + return !mChargeTracker.isCharging() && !mForegroundUids.get(mUid); + } + + void onStateChangedLocked(long nowElapsed, boolean isQuotaFree) { + if (isQuotaFree) { + emitSessionLocked(nowElapsed); + } else if (!isActive() && shouldTrackLocked()) { + // Start timing from unplug. + if (mRunningBgJobs.size() > 0) { + mStartTimeElapsed = nowElapsed; + // NOTE: this does have the unfortunate consequence that if the device is + // repeatedly plugged in and unplugged, or an app changes foreground state + // very frequently, the job count for a package may be artificially high. + mBgJobCount = mRunningBgJobs.size(); + incrementJobCount(mPkg.userId, mPkg.packageName, mBgJobCount); + // Starting the timer means that all cached execution stats are now + // incorrect. + invalidateAllExecutionStatsLocked(mPkg.userId, mPkg.packageName); + // Schedule cutoff since we're now actively tracking for quotas again. + scheduleCutoff(); + } + } + } + + void rescheduleCutoff() { + cancelCutoff(); + scheduleCutoff(); + } + + private void scheduleCutoff() { + // Each package can only be in one standby bucket, so we only need to have one + // message per timer. We only need to reschedule when restarting timer or when + // standby bucket changes. + synchronized (mLock) { + if (!isActive()) { + return; + } + Message msg = mHandler.obtainMessage(MSG_REACHED_QUOTA, mPkg); + final long timeRemainingMs = getTimeUntilQuotaConsumedLocked(mPkg.userId, + mPkg.packageName); + if (DEBUG) { + Slog.i(TAG, "Job for " + mPkg + " has " + timeRemainingMs + "ms left."); + } + // If the job was running the entire time, then the system would be up, so it's + // fine to use uptime millis for these messages. + mHandler.sendMessageDelayed(msg, timeRemainingMs); + } + } + + private void cancelCutoff() { + mHandler.removeMessages(MSG_REACHED_QUOTA, mPkg); + } + + public void dump(IndentingPrintWriter pw, Predicate<JobStatus> predicate) { + pw.print("Timer{"); + pw.print(mPkg); + pw.print("} "); + if (isActive()) { + pw.print("started at "); + pw.print(mStartTimeElapsed); + pw.print(" ("); + pw.print(sElapsedRealtimeClock.millis() - mStartTimeElapsed); + pw.print("ms ago)"); + } else { + pw.print("NOT active"); + } + pw.print(", "); + pw.print(mBgJobCount); + pw.print(" running bg jobs"); + pw.println(); + pw.increaseIndent(); + for (int i = 0; i < mRunningBgJobs.size(); i++) { + JobStatus js = mRunningBgJobs.valueAt(i); + if (predicate.test(js)) { + pw.println(js.toShortString()); + } + } + pw.decreaseIndent(); + } + + public void dump(ProtoOutputStream proto, long fieldId, Predicate<JobStatus> predicate) { + final long token = proto.start(fieldId); + + mPkg.writeToProto(proto, StateControllerProto.QuotaController.Timer.PKG); + proto.write(StateControllerProto.QuotaController.Timer.IS_ACTIVE, isActive()); + proto.write(StateControllerProto.QuotaController.Timer.START_TIME_ELAPSED, + mStartTimeElapsed); + proto.write(StateControllerProto.QuotaController.Timer.BG_JOB_COUNT, mBgJobCount); + for (int i = 0; i < mRunningBgJobs.size(); i++) { + JobStatus js = mRunningBgJobs.valueAt(i); + if (predicate.test(js)) { + js.writeToShortProto(proto, + StateControllerProto.QuotaController.Timer.RUNNING_JOBS); + } + } + + proto.end(token); + } + } + + /** + * Tracking of app assignments to standby buckets + */ + final class StandbyTracker extends AppIdleStateChangeListener { + + @Override + public void onAppIdleStateChanged(final String packageName, final @UserIdInt int userId, + boolean idle, int bucket, int reason) { + // Update job bookkeeping out of band. + BackgroundThread.getHandler().post(() -> { + final int bucketIndex = JobSchedulerService.standbyBucketToBucketIndex(bucket); + if (DEBUG) { + Slog.i(TAG, "Moving pkg " + string(userId, packageName) + " to bucketIndex " + + bucketIndex); + } + synchronized (mLock) { + ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, packageName); + if (jobs == null || jobs.size() == 0) { + return; + } + for (int i = jobs.size() - 1; i >= 0; i--) { + JobStatus js = jobs.valueAt(i); + js.setStandbyBucket(bucketIndex); + } + Timer timer = mPkgTimers.get(userId, packageName); + if (timer != null && timer.isActive()) { + timer.rescheduleCutoff(); + } + if (maybeUpdateConstraintForPkgLocked(userId, packageName)) { + mStateChangedListener.onControllerStateChanged(); + } + } + }); + } + + @Override + public void onParoleStateChanged(final boolean isParoleOn) { + mInParole = isParoleOn; + if (DEBUG) { + Slog.i(TAG, "Global parole state now " + (isParoleOn ? "ON" : "OFF")); + } + // Update job bookkeeping out of band. + BackgroundThread.getHandler().post(() -> { + synchronized (mLock) { + maybeUpdateAllConstraintsLocked(); + } + }); + } + } + + private final class DeleteTimingSessionsFunctor implements Consumer<List<TimingSession>> { + private final Predicate<TimingSession> mTooOld = new Predicate<TimingSession>() { + public boolean test(TimingSession ts) { + return ts.endTimeElapsed <= sElapsedRealtimeClock.millis() - MAX_PERIOD_MS; + } + }; + + @Override + public void accept(List<TimingSession> sessions) { + if (sessions != null) { + // Remove everything older than MAX_PERIOD_MS time ago. + sessions.removeIf(mTooOld); + } + } + } + + private final DeleteTimingSessionsFunctor mDeleteOldSessionsFunctor = + new DeleteTimingSessionsFunctor(); + + @VisibleForTesting + void deleteObsoleteSessionsLocked() { + mTimingSessions.forEach(mDeleteOldSessionsFunctor); + } + + private class QcHandler extends Handler { + QcHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + synchronized (mLock) { + switch (msg.what) { + case MSG_REACHED_QUOTA: { + Package pkg = (Package) msg.obj; + if (DEBUG) { + Slog.d(TAG, "Checking if " + pkg + " has reached its quota."); + } + + long timeRemainingMs = getRemainingExecutionTimeLocked(pkg.userId, + pkg.packageName); + if (timeRemainingMs <= 50) { + // Less than 50 milliseconds left. Start process of shutting down jobs. + if (DEBUG) Slog.d(TAG, pkg + " has reached its quota."); + if (maybeUpdateConstraintForPkgLocked(pkg.userId, pkg.packageName)) { + mStateChangedListener.onControllerStateChanged(); + } + } else { + // This could potentially happen if an old session phases out while a + // job is currently running. + // Reschedule message + Message rescheduleMsg = obtainMessage(MSG_REACHED_QUOTA, pkg); + timeRemainingMs = getTimeUntilQuotaConsumedLocked(pkg.userId, + pkg.packageName); + if (DEBUG) { + Slog.d(TAG, pkg + " has " + timeRemainingMs + "ms left."); + } + sendMessageDelayed(rescheduleMsg, timeRemainingMs); + } + break; + } + case MSG_CLEAN_UP_SESSIONS: + if (DEBUG) { + Slog.d(TAG, "Cleaning up timing sessions."); + } + deleteObsoleteSessionsLocked(); + maybeScheduleCleanupAlarmLocked(); + + break; + case MSG_CHECK_PACKAGE: { + String packageName = (String) msg.obj; + int userId = msg.arg1; + if (DEBUG) { + Slog.d(TAG, "Checking pkg " + string(userId, packageName)); + } + if (maybeUpdateConstraintForPkgLocked(userId, packageName)) { + mStateChangedListener.onControllerStateChanged(); + } + break; + } + case MSG_UID_PROCESS_STATE_CHANGED: { + final int uid = msg.arg1; + final int procState = msg.arg2; + final int userId = UserHandle.getUserId(uid); + final long nowElapsed = sElapsedRealtimeClock.millis(); + + synchronized (mLock) { + boolean isQuotaFree; + if (procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) { + mForegroundUids.put(uid, true); + isQuotaFree = true; + } else { + mForegroundUids.delete(uid); + isQuotaFree = false; + } + // Update Timers first. + if (mPkgTimers.indexOfKey(userId) >= 0) { + ArraySet<String> packages = mUidToPackageCache.get(uid); + if (packages == null) { + try { + String[] pkgs = AppGlobals.getPackageManager() + .getPackagesForUid(uid); + if (pkgs != null) { + for (String pkg : pkgs) { + mUidToPackageCache.add(uid, pkg); + } + packages = mUidToPackageCache.get(uid); + } + } catch (RemoteException e) { + Slog.wtf(TAG, "Failed to get package list", e); + } + } + if (packages != null) { + for (int i = packages.size() - 1; i >= 0; --i) { + Timer t = mPkgTimers.get(userId, packages.valueAt(i)); + if (t != null) { + t.onStateChangedLocked(nowElapsed, isQuotaFree); + } + } + } + } + if (maybeUpdateConstraintForUidLocked(uid)) { + mStateChangedListener.onControllerStateChanged(); + } + } + break; + } + } + } + } + } + + private class QcAlarmListener implements AlarmManager.OnAlarmListener { + private final int mUserId; + private final String mPackageName; + private volatile long mTriggerTimeElapsed; + + QcAlarmListener(int userId, String packageName) { + mUserId = userId; + mPackageName = packageName; + } + + boolean isWaiting() { + return mTriggerTimeElapsed > 0; + } + + void setTriggerTime(long timeElapsed) { + mTriggerTimeElapsed = timeElapsed; + } + + long getTriggerTimeElapsed() { + return mTriggerTimeElapsed; + } + + @Override + public void onAlarm() { + mHandler.obtainMessage(MSG_CHECK_PACKAGE, mUserId, 0, mPackageName).sendToTarget(); + mTriggerTimeElapsed = 0; + } + } + + @VisibleForTesting + class QcConstants extends ContentObserver { + private ContentResolver mResolver; + private final KeyValueListParser mParser = new KeyValueListParser(','); + + private static final String KEY_ALLOWED_TIME_PER_PERIOD_MS = "allowed_time_per_period_ms"; + private static final String KEY_IN_QUOTA_BUFFER_MS = "in_quota_buffer_ms"; + private static final String KEY_WINDOW_SIZE_ACTIVE_MS = "window_size_active_ms"; + private static final String KEY_WINDOW_SIZE_WORKING_MS = "window_size_working_ms"; + private static final String KEY_WINDOW_SIZE_FREQUENT_MS = "window_size_frequent_ms"; + private static final String KEY_WINDOW_SIZE_RARE_MS = "window_size_rare_ms"; + private static final String KEY_MAX_EXECUTION_TIME_MS = "max_execution_time_ms"; + private static final String KEY_MAX_JOB_COUNT_ACTIVE = "max_job_count_active"; + private static final String KEY_MAX_JOB_COUNT_WORKING = "max_job_count_working"; + private static final String KEY_MAX_JOB_COUNT_FREQUENT = "max_job_count_frequent"; + private static final String KEY_MAX_JOB_COUNT_RARE = "max_job_count_rare"; + private static final String KEY_RATE_LIMITING_WINDOW_MS = "rate_limiting_window_ms"; + private static final String KEY_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = + "max_job_count_per_rate_limiting_window"; + private static final String KEY_MAX_SESSION_COUNT_ACTIVE = "max_session_count_active"; + private static final String KEY_MAX_SESSION_COUNT_WORKING = "max_session_count_working"; + private static final String KEY_MAX_SESSION_COUNT_FREQUENT = "max_session_count_frequent"; + private static final String KEY_MAX_SESSION_COUNT_RARE = "max_session_count_rare"; + private static final String KEY_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = + "max_session_count_per_rate_limiting_window"; + private static final String KEY_TIMING_SESSION_COALESCING_DURATION_MS = + "timing_session_coalescing_duration_ms"; + + private static final long DEFAULT_ALLOWED_TIME_PER_PERIOD_MS = + 10 * 60 * 1000L; // 10 minutes + private static final long DEFAULT_IN_QUOTA_BUFFER_MS = + 30 * 1000L; // 30 seconds + private static final long DEFAULT_WINDOW_SIZE_ACTIVE_MS = + DEFAULT_ALLOWED_TIME_PER_PERIOD_MS; // ACTIVE apps can run jobs at any time + private static final long DEFAULT_WINDOW_SIZE_WORKING_MS = + 2 * 60 * 60 * 1000L; // 2 hours + private static final long DEFAULT_WINDOW_SIZE_FREQUENT_MS = + 8 * 60 * 60 * 1000L; // 8 hours + private static final long DEFAULT_WINDOW_SIZE_RARE_MS = + 24 * 60 * 60 * 1000L; // 24 hours + private static final long DEFAULT_MAX_EXECUTION_TIME_MS = + 4 * HOUR_IN_MILLIS; + private static final long DEFAULT_RATE_LIMITING_WINDOW_MS = + 10 * MINUTE_IN_MILLIS; + private static final int DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = 20; + private static final int DEFAULT_MAX_JOB_COUNT_ACTIVE = // 20/window = 120/hr = 1/session + DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW; + private static final int DEFAULT_MAX_JOB_COUNT_WORKING = // 120/window = 60/hr = 12/session + (int) (60.0 * DEFAULT_WINDOW_SIZE_WORKING_MS / HOUR_IN_MILLIS); + private static final int DEFAULT_MAX_JOB_COUNT_FREQUENT = // 200/window = 25/hr = 25/session + (int) (25.0 * DEFAULT_WINDOW_SIZE_FREQUENT_MS / HOUR_IN_MILLIS); + private static final int DEFAULT_MAX_JOB_COUNT_RARE = // 48/window = 2/hr = 16/session + (int) (2.0 * DEFAULT_WINDOW_SIZE_RARE_MS / HOUR_IN_MILLIS); + private static final int DEFAULT_MAX_SESSION_COUNT_ACTIVE = + 20; // 120/hr + private static final int DEFAULT_MAX_SESSION_COUNT_WORKING = + 10; // 5/hr + private static final int DEFAULT_MAX_SESSION_COUNT_FREQUENT = + 8; // 1/hr + private static final int DEFAULT_MAX_SESSION_COUNT_RARE = + 3; // .125/hr + private static final int DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = 20; + private static final long DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS = 5000; // 5 seconds + + /** How much time each app will have to run jobs within their standby bucket window. */ + public long ALLOWED_TIME_PER_PERIOD_MS = DEFAULT_ALLOWED_TIME_PER_PERIOD_MS; + + /** + * How much time the package should have before transitioning from out-of-quota to in-quota. + * This should not affect processing if the package is already in-quota. + */ + public long IN_QUOTA_BUFFER_MS = DEFAULT_IN_QUOTA_BUFFER_MS; + + /** + * The quota window size of the particular standby bucket. Apps in this standby bucket are + * expected to run only {@link #ALLOWED_TIME_PER_PERIOD_MS} within the past + * WINDOW_SIZE_MS. + */ + public long WINDOW_SIZE_ACTIVE_MS = DEFAULT_WINDOW_SIZE_ACTIVE_MS; + + /** + * The quota window size of the particular standby bucket. Apps in this standby bucket are + * expected to run only {@link #ALLOWED_TIME_PER_PERIOD_MS} within the past + * WINDOW_SIZE_MS. + */ + public long WINDOW_SIZE_WORKING_MS = DEFAULT_WINDOW_SIZE_WORKING_MS; + + /** + * The quota window size of the particular standby bucket. Apps in this standby bucket are + * expected to run only {@link #ALLOWED_TIME_PER_PERIOD_MS} within the past + * WINDOW_SIZE_MS. + */ + public long WINDOW_SIZE_FREQUENT_MS = DEFAULT_WINDOW_SIZE_FREQUENT_MS; + + /** + * The quota window size of the particular standby bucket. Apps in this standby bucket are + * expected to run only {@link #ALLOWED_TIME_PER_PERIOD_MS} within the past + * WINDOW_SIZE_MS. + */ + public long WINDOW_SIZE_RARE_MS = DEFAULT_WINDOW_SIZE_RARE_MS; + + /** + * The maximum amount of time an app can have its jobs running within a 24 hour window. + */ + public long MAX_EXECUTION_TIME_MS = DEFAULT_MAX_EXECUTION_TIME_MS; + + /** + * The maximum number of jobs an app can run within this particular standby bucket's + * window size. + */ + public int MAX_JOB_COUNT_ACTIVE = DEFAULT_MAX_JOB_COUNT_ACTIVE; + + /** + * The maximum number of jobs an app can run within this particular standby bucket's + * window size. + */ + public int MAX_JOB_COUNT_WORKING = DEFAULT_MAX_JOB_COUNT_WORKING; + + /** + * The maximum number of jobs an app can run within this particular standby bucket's + * window size. + */ + public int MAX_JOB_COUNT_FREQUENT = DEFAULT_MAX_JOB_COUNT_FREQUENT; + + /** + * The maximum number of jobs an app can run within this particular standby bucket's + * window size. + */ + public int MAX_JOB_COUNT_RARE = DEFAULT_MAX_JOB_COUNT_RARE; + + /** The period of time used to rate limit recently run jobs. */ + public long RATE_LIMITING_WINDOW_MS = DEFAULT_RATE_LIMITING_WINDOW_MS; + + /** + * The maximum number of jobs that can run within the past {@link #RATE_LIMITING_WINDOW_MS}. + */ + public int MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = + DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW; + + /** + * The maximum number of {@link TimingSession}s an app can run within this particular + * standby bucket's window size. + */ + public int MAX_SESSION_COUNT_ACTIVE = DEFAULT_MAX_SESSION_COUNT_ACTIVE; + + /** + * The maximum number of {@link TimingSession}s an app can run within this particular + * standby bucket's window size. + */ + public int MAX_SESSION_COUNT_WORKING = DEFAULT_MAX_SESSION_COUNT_WORKING; + + /** + * The maximum number of {@link TimingSession}s an app can run within this particular + * standby bucket's window size. + */ + public int MAX_SESSION_COUNT_FREQUENT = DEFAULT_MAX_SESSION_COUNT_FREQUENT; + + /** + * The maximum number of {@link TimingSession}s an app can run within this particular + * standby bucket's window size. + */ + public int MAX_SESSION_COUNT_RARE = DEFAULT_MAX_SESSION_COUNT_RARE; + + /** + * The maximum number of {@link TimingSession}s that can run within the past + * {@link #ALLOWED_TIME_PER_PERIOD_MS}. + */ + public int MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = + DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW; + + /** + * Treat two distinct {@link TimingSession}s as the same if they start and end within this + * amount of time of each other. + */ + public long TIMING_SESSION_COALESCING_DURATION_MS = + DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS; + + // Safeguards + + /** The minimum number of jobs that any bucket will be allowed to run within its window. */ + private static final int MIN_BUCKET_JOB_COUNT = 10; + + /** + * The minimum number of {@link TimingSession}s that any bucket will be allowed to run + * within its window. + */ + private static final int MIN_BUCKET_SESSION_COUNT = 1; + + /** The minimum value that {@link #MAX_EXECUTION_TIME_MS} can have. */ + private static final long MIN_MAX_EXECUTION_TIME_MS = 60 * MINUTE_IN_MILLIS; + + /** The minimum value that {@link #MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW} can have. */ + private static final int MIN_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = 10; + + /** The minimum value that {@link #MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW} can have. */ + private static final int MIN_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = 10; + + /** The minimum value that {@link #RATE_LIMITING_WINDOW_MS} can have. */ + private static final long MIN_RATE_LIMITING_WINDOW_MS = 30 * SECOND_IN_MILLIS; + + QcConstants(Handler handler) { + super(handler); + } + + private void start(ContentResolver resolver) { + mResolver = resolver; + mResolver.registerContentObserver(Settings.Global.getUriFor( + Settings.Global.JOB_SCHEDULER_QUOTA_CONTROLLER_CONSTANTS), false, this); + updateConstants(); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + final String constants = Settings.Global.getString( + mResolver, Settings.Global.JOB_SCHEDULER_QUOTA_CONTROLLER_CONSTANTS); + + try { + mParser.setString(constants); + } catch (Exception e) { + // Failed to parse the settings string, log this and move on with defaults. + Slog.e(TAG, "Bad jobscheduler quota controller settings", e); + } + + ALLOWED_TIME_PER_PERIOD_MS = mParser.getDurationMillis( + KEY_ALLOWED_TIME_PER_PERIOD_MS, DEFAULT_ALLOWED_TIME_PER_PERIOD_MS); + IN_QUOTA_BUFFER_MS = mParser.getDurationMillis( + KEY_IN_QUOTA_BUFFER_MS, DEFAULT_IN_QUOTA_BUFFER_MS); + WINDOW_SIZE_ACTIVE_MS = mParser.getDurationMillis( + KEY_WINDOW_SIZE_ACTIVE_MS, DEFAULT_WINDOW_SIZE_ACTIVE_MS); + WINDOW_SIZE_WORKING_MS = mParser.getDurationMillis( + KEY_WINDOW_SIZE_WORKING_MS, DEFAULT_WINDOW_SIZE_WORKING_MS); + WINDOW_SIZE_FREQUENT_MS = mParser.getDurationMillis( + KEY_WINDOW_SIZE_FREQUENT_MS, DEFAULT_WINDOW_SIZE_FREQUENT_MS); + WINDOW_SIZE_RARE_MS = mParser.getDurationMillis( + KEY_WINDOW_SIZE_RARE_MS, DEFAULT_WINDOW_SIZE_RARE_MS); + MAX_EXECUTION_TIME_MS = mParser.getDurationMillis( + KEY_MAX_EXECUTION_TIME_MS, DEFAULT_MAX_EXECUTION_TIME_MS); + MAX_JOB_COUNT_ACTIVE = mParser.getInt( + KEY_MAX_JOB_COUNT_ACTIVE, DEFAULT_MAX_JOB_COUNT_ACTIVE); + MAX_JOB_COUNT_WORKING = mParser.getInt( + KEY_MAX_JOB_COUNT_WORKING, DEFAULT_MAX_JOB_COUNT_WORKING); + MAX_JOB_COUNT_FREQUENT = mParser.getInt( + KEY_MAX_JOB_COUNT_FREQUENT, DEFAULT_MAX_JOB_COUNT_FREQUENT); + MAX_JOB_COUNT_RARE = mParser.getInt( + KEY_MAX_JOB_COUNT_RARE, DEFAULT_MAX_JOB_COUNT_RARE); + RATE_LIMITING_WINDOW_MS = mParser.getLong( + KEY_RATE_LIMITING_WINDOW_MS, DEFAULT_RATE_LIMITING_WINDOW_MS); + MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = mParser.getInt( + KEY_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW, + DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW); + MAX_SESSION_COUNT_ACTIVE = mParser.getInt( + KEY_MAX_SESSION_COUNT_ACTIVE, DEFAULT_MAX_SESSION_COUNT_ACTIVE); + MAX_SESSION_COUNT_WORKING = mParser.getInt( + KEY_MAX_SESSION_COUNT_WORKING, DEFAULT_MAX_SESSION_COUNT_WORKING); + MAX_SESSION_COUNT_FREQUENT = mParser.getInt( + KEY_MAX_SESSION_COUNT_FREQUENT, DEFAULT_MAX_SESSION_COUNT_FREQUENT); + MAX_SESSION_COUNT_RARE = mParser.getInt( + KEY_MAX_SESSION_COUNT_RARE, DEFAULT_MAX_SESSION_COUNT_RARE); + MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = mParser.getInt( + KEY_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW, + DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW); + TIMING_SESSION_COALESCING_DURATION_MS = mParser.getLong( + KEY_TIMING_SESSION_COALESCING_DURATION_MS, + DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS); + + updateConstants(); + } + + @VisibleForTesting + void updateConstants() { + synchronized (mLock) { + boolean changed = false; + + long newMaxExecutionTimeMs = Math.max(MIN_MAX_EXECUTION_TIME_MS, + Math.min(MAX_PERIOD_MS, MAX_EXECUTION_TIME_MS)); + if (mMaxExecutionTimeMs != newMaxExecutionTimeMs) { + mMaxExecutionTimeMs = newMaxExecutionTimeMs; + mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs; + changed = true; + } + long newAllowedTimeMs = Math.min(mMaxExecutionTimeMs, + Math.max(MINUTE_IN_MILLIS, ALLOWED_TIME_PER_PERIOD_MS)); + if (mAllowedTimePerPeriodMs != newAllowedTimeMs) { + mAllowedTimePerPeriodMs = newAllowedTimeMs; + mAllowedTimeIntoQuotaMs = mAllowedTimePerPeriodMs - mQuotaBufferMs; + changed = true; + } + // Make sure quota buffer is non-negative, not greater than allowed time per period, + // and no more than 5 minutes. + long newQuotaBufferMs = Math.max(0, Math.min(mAllowedTimePerPeriodMs, + Math.min(5 * MINUTE_IN_MILLIS, IN_QUOTA_BUFFER_MS))); + if (mQuotaBufferMs != newQuotaBufferMs) { + mQuotaBufferMs = newQuotaBufferMs; + mAllowedTimeIntoQuotaMs = mAllowedTimePerPeriodMs - mQuotaBufferMs; + mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs; + changed = true; + } + long newActivePeriodMs = Math.max(mAllowedTimePerPeriodMs, + Math.min(MAX_PERIOD_MS, WINDOW_SIZE_ACTIVE_MS)); + if (mBucketPeriodsMs[ACTIVE_INDEX] != newActivePeriodMs) { + mBucketPeriodsMs[ACTIVE_INDEX] = newActivePeriodMs; + changed = true; + } + long newWorkingPeriodMs = Math.max(mAllowedTimePerPeriodMs, + Math.min(MAX_PERIOD_MS, WINDOW_SIZE_WORKING_MS)); + if (mBucketPeriodsMs[WORKING_INDEX] != newWorkingPeriodMs) { + mBucketPeriodsMs[WORKING_INDEX] = newWorkingPeriodMs; + changed = true; + } + long newFrequentPeriodMs = Math.max(mAllowedTimePerPeriodMs, + Math.min(MAX_PERIOD_MS, WINDOW_SIZE_FREQUENT_MS)); + if (mBucketPeriodsMs[FREQUENT_INDEX] != newFrequentPeriodMs) { + mBucketPeriodsMs[FREQUENT_INDEX] = newFrequentPeriodMs; + changed = true; + } + long newRarePeriodMs = Math.max(mAllowedTimePerPeriodMs, + Math.min(MAX_PERIOD_MS, WINDOW_SIZE_RARE_MS)); + if (mBucketPeriodsMs[RARE_INDEX] != newRarePeriodMs) { + mBucketPeriodsMs[RARE_INDEX] = newRarePeriodMs; + changed = true; + } + long newRateLimitingWindowMs = Math.min(MAX_PERIOD_MS, + Math.max(MIN_RATE_LIMITING_WINDOW_MS, RATE_LIMITING_WINDOW_MS)); + if (mRateLimitingWindowMs != newRateLimitingWindowMs) { + mRateLimitingWindowMs = newRateLimitingWindowMs; + changed = true; + } + int newMaxJobCountPerRateLimitingWindow = Math.max( + MIN_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW, + MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW); + if (mMaxJobCountPerRateLimitingWindow != newMaxJobCountPerRateLimitingWindow) { + mMaxJobCountPerRateLimitingWindow = newMaxJobCountPerRateLimitingWindow; + changed = true; + } + int newActiveMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_ACTIVE); + if (mMaxBucketJobCounts[ACTIVE_INDEX] != newActiveMaxJobCount) { + mMaxBucketJobCounts[ACTIVE_INDEX] = newActiveMaxJobCount; + changed = true; + } + int newWorkingMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_WORKING); + if (mMaxBucketJobCounts[WORKING_INDEX] != newWorkingMaxJobCount) { + mMaxBucketJobCounts[WORKING_INDEX] = newWorkingMaxJobCount; + changed = true; + } + int newFrequentMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_FREQUENT); + if (mMaxBucketJobCounts[FREQUENT_INDEX] != newFrequentMaxJobCount) { + mMaxBucketJobCounts[FREQUENT_INDEX] = newFrequentMaxJobCount; + changed = true; + } + int newRareMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_RARE); + if (mMaxBucketJobCounts[RARE_INDEX] != newRareMaxJobCount) { + mMaxBucketJobCounts[RARE_INDEX] = newRareMaxJobCount; + changed = true; + } + int newMaxSessionCountPerRateLimitPeriod = Math.max( + MIN_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW, + MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW); + if (mMaxSessionCountPerRateLimitingWindow != newMaxSessionCountPerRateLimitPeriod) { + mMaxSessionCountPerRateLimitingWindow = newMaxSessionCountPerRateLimitPeriod; + changed = true; + } + int newActiveMaxSessionCount = + Math.max(MIN_BUCKET_SESSION_COUNT, MAX_SESSION_COUNT_ACTIVE); + if (mMaxBucketSessionCounts[ACTIVE_INDEX] != newActiveMaxSessionCount) { + mMaxBucketSessionCounts[ACTIVE_INDEX] = newActiveMaxSessionCount; + changed = true; + } + int newWorkingMaxSessionCount = + Math.max(MIN_BUCKET_SESSION_COUNT, MAX_SESSION_COUNT_WORKING); + if (mMaxBucketSessionCounts[WORKING_INDEX] != newWorkingMaxSessionCount) { + mMaxBucketSessionCounts[WORKING_INDEX] = newWorkingMaxSessionCount; + changed = true; + } + int newFrequentMaxSessionCount = + Math.max(MIN_BUCKET_SESSION_COUNT, MAX_SESSION_COUNT_FREQUENT); + if (mMaxBucketSessionCounts[FREQUENT_INDEX] != newFrequentMaxSessionCount) { + mMaxBucketSessionCounts[FREQUENT_INDEX] = newFrequentMaxSessionCount; + changed = true; + } + int newRareMaxSessionCount = + Math.max(MIN_BUCKET_SESSION_COUNT, MAX_SESSION_COUNT_RARE); + if (mMaxBucketSessionCounts[RARE_INDEX] != newRareMaxSessionCount) { + mMaxBucketSessionCounts[RARE_INDEX] = newRareMaxSessionCount; + changed = true; + } + long newSessionCoalescingDurationMs = Math.min(15 * MINUTE_IN_MILLIS, + Math.max(0, TIMING_SESSION_COALESCING_DURATION_MS)); + if (mTimingSessionCoalescingDurationMs != newSessionCoalescingDurationMs) { + mTimingSessionCoalescingDurationMs = newSessionCoalescingDurationMs; + changed = true; + } + + if (changed) { + // Update job bookkeeping out of band. + BackgroundThread.getHandler().post(() -> { + synchronized (mLock) { + invalidateAllExecutionStatsLocked(); + maybeUpdateAllConstraintsLocked(); + } + }); + } + } + } + + private void dump(IndentingPrintWriter pw) { + pw.println(); + pw.println("QuotaController:"); + pw.increaseIndent(); + pw.printPair(KEY_ALLOWED_TIME_PER_PERIOD_MS, ALLOWED_TIME_PER_PERIOD_MS).println(); + pw.printPair(KEY_IN_QUOTA_BUFFER_MS, IN_QUOTA_BUFFER_MS).println(); + pw.printPair(KEY_WINDOW_SIZE_ACTIVE_MS, WINDOW_SIZE_ACTIVE_MS).println(); + pw.printPair(KEY_WINDOW_SIZE_WORKING_MS, WINDOW_SIZE_WORKING_MS).println(); + pw.printPair(KEY_WINDOW_SIZE_FREQUENT_MS, WINDOW_SIZE_FREQUENT_MS).println(); + pw.printPair(KEY_WINDOW_SIZE_RARE_MS, WINDOW_SIZE_RARE_MS).println(); + pw.printPair(KEY_MAX_EXECUTION_TIME_MS, MAX_EXECUTION_TIME_MS).println(); + pw.printPair(KEY_MAX_JOB_COUNT_ACTIVE, MAX_JOB_COUNT_ACTIVE).println(); + pw.printPair(KEY_MAX_JOB_COUNT_WORKING, MAX_JOB_COUNT_WORKING).println(); + pw.printPair(KEY_MAX_JOB_COUNT_FREQUENT, MAX_JOB_COUNT_FREQUENT).println(); + pw.printPair(KEY_MAX_JOB_COUNT_RARE, MAX_JOB_COUNT_RARE).println(); + pw.printPair(KEY_RATE_LIMITING_WINDOW_MS, RATE_LIMITING_WINDOW_MS).println(); + pw.printPair(KEY_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW, + MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW).println(); + pw.printPair(KEY_MAX_SESSION_COUNT_ACTIVE, MAX_SESSION_COUNT_ACTIVE).println(); + pw.printPair(KEY_MAX_SESSION_COUNT_WORKING, MAX_SESSION_COUNT_WORKING).println(); + pw.printPair(KEY_MAX_SESSION_COUNT_FREQUENT, MAX_SESSION_COUNT_FREQUENT).println(); + pw.printPair(KEY_MAX_SESSION_COUNT_RARE, MAX_SESSION_COUNT_RARE).println(); + pw.printPair(KEY_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW, + MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW).println(); + pw.printPair(KEY_TIMING_SESSION_COALESCING_DURATION_MS, + TIMING_SESSION_COALESCING_DURATION_MS).println(); + pw.decreaseIndent(); + } + + private void dump(ProtoOutputStream proto) { + final long qcToken = proto.start(ConstantsProto.QUOTA_CONTROLLER); + proto.write(ConstantsProto.QuotaController.ALLOWED_TIME_PER_PERIOD_MS, + ALLOWED_TIME_PER_PERIOD_MS); + proto.write(ConstantsProto.QuotaController.IN_QUOTA_BUFFER_MS, IN_QUOTA_BUFFER_MS); + proto.write(ConstantsProto.QuotaController.ACTIVE_WINDOW_SIZE_MS, + WINDOW_SIZE_ACTIVE_MS); + proto.write(ConstantsProto.QuotaController.WORKING_WINDOW_SIZE_MS, + WINDOW_SIZE_WORKING_MS); + proto.write(ConstantsProto.QuotaController.FREQUENT_WINDOW_SIZE_MS, + WINDOW_SIZE_FREQUENT_MS); + proto.write(ConstantsProto.QuotaController.RARE_WINDOW_SIZE_MS, WINDOW_SIZE_RARE_MS); + proto.write(ConstantsProto.QuotaController.MAX_EXECUTION_TIME_MS, + MAX_EXECUTION_TIME_MS); + proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_ACTIVE, MAX_JOB_COUNT_ACTIVE); + proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_WORKING, + MAX_JOB_COUNT_WORKING); + proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_FREQUENT, + MAX_JOB_COUNT_FREQUENT); + proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_RARE, MAX_JOB_COUNT_RARE); + proto.write(ConstantsProto.QuotaController.RATE_LIMITING_WINDOW_MS, + RATE_LIMITING_WINDOW_MS); + proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW, + MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW); + proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_ACTIVE, + MAX_SESSION_COUNT_ACTIVE); + proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_WORKING, + MAX_SESSION_COUNT_WORKING); + proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_FREQUENT, + MAX_SESSION_COUNT_FREQUENT); + proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_RARE, + MAX_SESSION_COUNT_RARE); + proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW, + MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW); + proto.write(ConstantsProto.QuotaController.TIMING_SESSION_COALESCING_DURATION_MS, + TIMING_SESSION_COALESCING_DURATION_MS); + proto.end(qcToken); + } + } + + //////////////////////// TESTING HELPERS ///////////////////////////// + + @VisibleForTesting + long getAllowedTimePerPeriodMs() { + return mAllowedTimePerPeriodMs; + } + + @VisibleForTesting + @NonNull + int[] getBucketMaxJobCounts() { + return mMaxBucketJobCounts; + } + + @VisibleForTesting + @NonNull + int[] getBucketMaxSessionCounts() { + return mMaxBucketSessionCounts; + } + + @VisibleForTesting + @NonNull + long[] getBucketWindowSizes() { + return mBucketPeriodsMs; + } + + @VisibleForTesting + @NonNull + SparseBooleanArray getForegroundUids() { + return mForegroundUids; + } + + @VisibleForTesting + @NonNull + Handler getHandler() { + return mHandler; + } + + @VisibleForTesting + long getInQuotaBufferMs() { + return mQuotaBufferMs; + } + + @VisibleForTesting + long getMaxExecutionTimeMs() { + return mMaxExecutionTimeMs; + } + + @VisibleForTesting + int getMaxJobCountPerRateLimitingWindow() { + return mMaxJobCountPerRateLimitingWindow; + } + + @VisibleForTesting + int getMaxSessionCountPerRateLimitingWindow() { + return mMaxSessionCountPerRateLimitingWindow; + } + + @VisibleForTesting + long getRateLimitingWindowMs() { + return mRateLimitingWindowMs; + } + + @VisibleForTesting + long getTimingSessionCoalescingDurationMs() { + return mTimingSessionCoalescingDurationMs; + } + + @VisibleForTesting + @Nullable + List<TimingSession> getTimingSessions(int userId, String packageName) { + return mTimingSessions.get(userId, packageName); + } + + @VisibleForTesting + @NonNull + QcConstants getQcConstants() { + return mQcConstants; + } + + //////////////////////////// DATA DUMP ////////////////////////////// + + @Override + public void dumpControllerStateLocked(final IndentingPrintWriter pw, + final Predicate<JobStatus> predicate) { + pw.println("Is charging: " + mChargeTracker.isCharging()); + pw.println("In parole: " + mInParole); + pw.println("Current elapsed time: " + sElapsedRealtimeClock.millis()); + pw.println(); + + pw.print("Foreground UIDs: "); + pw.println(mForegroundUids.toString()); + pw.println(); + + pw.println("Cached UID->package map:"); + pw.increaseIndent(); + for (int i = 0; i < mUidToPackageCache.size(); ++i) { + final int uid = mUidToPackageCache.keyAt(i); + pw.print(uid); + pw.print(": "); + pw.println(mUidToPackageCache.get(uid)); + } + pw.decreaseIndent(); + pw.println(); + + mTrackedJobs.forEach((jobs) -> { + for (int j = 0; j < jobs.size(); j++) { + final JobStatus js = jobs.valueAt(j); + if (!predicate.test(js)) { + continue; + } + pw.print("#"); + js.printUniqueId(pw); + pw.print(" from "); + UserHandle.formatUid(pw, js.getSourceUid()); + if (mTopStartedJobs.contains(js)) { + pw.print(" (TOP)"); + } + pw.println(); + + pw.increaseIndent(); + pw.print(JobStatus.bucketName(js.getEffectiveStandbyBucket())); + pw.print(", "); + if (js.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)) { + pw.print("within quota"); + } else { + pw.print("not within quota"); + } + pw.print(", "); + pw.print(getRemainingExecutionTimeLocked(js)); + pw.print("ms remaining in quota"); + pw.decreaseIndent(); + pw.println(); + } + }); + + pw.println(); + for (int u = 0; u < mPkgTimers.numUsers(); ++u) { + final int userId = mPkgTimers.keyAt(u); + for (int p = 0; p < mPkgTimers.numPackagesForUser(userId); ++p) { + final String pkgName = mPkgTimers.keyAt(u, p); + mPkgTimers.valueAt(u, p).dump(pw, predicate); + pw.println(); + List<TimingSession> sessions = mTimingSessions.get(userId, pkgName); + if (sessions != null) { + pw.increaseIndent(); + pw.println("Saved sessions:"); + pw.increaseIndent(); + for (int j = sessions.size() - 1; j >= 0; j--) { + TimingSession session = sessions.get(j); + session.dump(pw); + } + pw.decreaseIndent(); + pw.decreaseIndent(); + pw.println(); + } + } + } + + pw.println("Cached execution stats:"); + pw.increaseIndent(); + for (int u = 0; u < mExecutionStatsCache.numUsers(); ++u) { + final int userId = mExecutionStatsCache.keyAt(u); + for (int p = 0; p < mExecutionStatsCache.numPackagesForUser(userId); ++p) { + final String pkgName = mExecutionStatsCache.keyAt(u, p); + ExecutionStats[] stats = mExecutionStatsCache.valueAt(u, p); + + pw.println(string(userId, pkgName)); + pw.increaseIndent(); + for (int i = 0; i < stats.length; ++i) { + ExecutionStats executionStats = stats[i]; + if (executionStats != null) { + pw.print(JobStatus.bucketName(i)); + pw.print(": "); + pw.println(executionStats); + } + } + pw.decreaseIndent(); + } + } + pw.decreaseIndent(); + + pw.println(); + pw.println("In quota alarms:"); + pw.increaseIndent(); + for (int u = 0; u < mInQuotaAlarmListeners.numUsers(); ++u) { + final int userId = mInQuotaAlarmListeners.keyAt(u); + for (int p = 0; p < mInQuotaAlarmListeners.numPackagesForUser(userId); ++p) { + final String pkgName = mInQuotaAlarmListeners.keyAt(u, p); + QcAlarmListener alarmListener = mInQuotaAlarmListeners.valueAt(u, p); + + pw.print(string(userId, pkgName)); + pw.print(": "); + if (alarmListener.isWaiting()) { + pw.println(alarmListener.getTriggerTimeElapsed()); + } else { + pw.println("NOT WAITING"); + } + } + } + pw.decreaseIndent(); + } + + @Override + public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, + Predicate<JobStatus> predicate) { + final long token = proto.start(fieldId); + final long mToken = proto.start(StateControllerProto.QUOTA); + + proto.write(StateControllerProto.QuotaController.IS_CHARGING, mChargeTracker.isCharging()); + proto.write(StateControllerProto.QuotaController.IS_IN_PAROLE, mInParole); + proto.write(StateControllerProto.QuotaController.ELAPSED_REALTIME, + sElapsedRealtimeClock.millis()); + + for (int i = 0; i < mForegroundUids.size(); ++i) { + proto.write(StateControllerProto.QuotaController.FOREGROUND_UIDS, + mForegroundUids.keyAt(i)); + } + + for (int i = 0; i < mUidToPackageCache.size(); ++i) { + final long upToken = proto.start( + StateControllerProto.QuotaController.UID_TO_PACKAGE_CACHE); + + final int uid = mUidToPackageCache.keyAt(i); + ArraySet<String> packages = mUidToPackageCache.get(uid); + + proto.write(StateControllerProto.QuotaController.UidPackageMapping.UID, uid); + for (int j = 0; j < packages.size(); ++j) { + proto.write(StateControllerProto.QuotaController.UidPackageMapping.PACKAGE_NAMES, + packages.valueAt(j)); + } + + proto.end(upToken); + } + + mTrackedJobs.forEach((jobs) -> { + for (int j = 0; j < jobs.size(); j++) { + final JobStatus js = jobs.valueAt(j); + if (!predicate.test(js)) { + continue; + } + final long jsToken = proto.start(StateControllerProto.QuotaController.TRACKED_JOBS); + js.writeToShortProto(proto, StateControllerProto.QuotaController.TrackedJob.INFO); + proto.write(StateControllerProto.QuotaController.TrackedJob.SOURCE_UID, + js.getSourceUid()); + proto.write( + StateControllerProto.QuotaController.TrackedJob.EFFECTIVE_STANDBY_BUCKET, + js.getEffectiveStandbyBucket()); + proto.write(StateControllerProto.QuotaController.TrackedJob.IS_TOP_STARTED_JOB, + mTopStartedJobs.contains(js)); + proto.write(StateControllerProto.QuotaController.TrackedJob.HAS_QUOTA, + js.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); + proto.write(StateControllerProto.QuotaController.TrackedJob.REMAINING_QUOTA_MS, + getRemainingExecutionTimeLocked(js)); + proto.end(jsToken); + } + }); + + for (int u = 0; u < mPkgTimers.numUsers(); ++u) { + final int userId = mPkgTimers.keyAt(u); + for (int p = 0; p < mPkgTimers.numPackagesForUser(userId); ++p) { + final String pkgName = mPkgTimers.keyAt(u, p); + final long psToken = proto.start( + StateControllerProto.QuotaController.PACKAGE_STATS); + mPkgTimers.valueAt(u, p).dump(proto, + StateControllerProto.QuotaController.PackageStats.TIMER, predicate); + + List<TimingSession> sessions = mTimingSessions.get(userId, pkgName); + if (sessions != null) { + for (int j = sessions.size() - 1; j >= 0; j--) { + TimingSession session = sessions.get(j); + session.dump(proto, + StateControllerProto.QuotaController.PackageStats.SAVED_SESSIONS); + } + } + + ExecutionStats[] stats = mExecutionStatsCache.get(userId, pkgName); + if (stats != null) { + for (int bucketIndex = 0; bucketIndex < stats.length; ++bucketIndex) { + ExecutionStats es = stats[bucketIndex]; + if (es == null) { + continue; + } + final long esToken = proto.start( + StateControllerProto.QuotaController.PackageStats.EXECUTION_STATS); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.STANDBY_BUCKET, + bucketIndex); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.EXPIRATION_TIME_ELAPSED, + es.expirationTimeElapsed); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.WINDOW_SIZE_MS, + es.windowSizeMs); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.JOB_COUNT_LIMIT, + es.jobCountLimit); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_LIMIT, + es.sessionCountLimit); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.EXECUTION_TIME_IN_WINDOW_MS, + es.executionTimeInWindowMs); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.BG_JOB_COUNT_IN_WINDOW, + es.bgJobCountInWindow); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.EXECUTION_TIME_IN_MAX_PERIOD_MS, + es.executionTimeInMaxPeriodMs); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.BG_JOB_COUNT_IN_MAX_PERIOD, + es.bgJobCountInMaxPeriod); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_IN_WINDOW, + es.sessionCountInWindow); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.IN_QUOTA_TIME_ELAPSED, + es.inQuotaTimeElapsed); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.JOB_COUNT_EXPIRATION_TIME_ELAPSED, + es.jobRateLimitExpirationTimeElapsed); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.JOB_COUNT_IN_RATE_LIMITING_WINDOW, + es.jobCountInRateLimitingWindow); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_EXPIRATION_TIME_ELAPSED, + es.sessionRateLimitExpirationTimeElapsed); + proto.write( + StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_IN_RATE_LIMITING_WINDOW, + es.sessionCountInRateLimitingWindow); + proto.end(esToken); + } + } + + QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, pkgName); + if (alarmListener != null) { + final long alToken = proto.start( + StateControllerProto.QuotaController.PackageStats.IN_QUOTA_ALARM_LISTENER); + proto.write(StateControllerProto.QuotaController.AlarmListener.IS_WAITING, + alarmListener.isWaiting()); + proto.write( + StateControllerProto.QuotaController.AlarmListener.TRIGGER_TIME_ELAPSED, + alarmListener.getTriggerTimeElapsed()); + proto.end(alToken); + } + + proto.end(psToken); + } + } + + proto.end(mToken); + proto.end(token); + } + + @Override + public void dumpConstants(IndentingPrintWriter pw) { + mQcConstants.dump(pw); + } + + @Override + public void dumpConstants(ProtoOutputStream proto) { + mQcConstants.dump(proto); + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java new file mode 100644 index 000000000000..51be38be990d --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2014 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 static com.android.server.job.JobSchedulerService.DEBUG; + +import android.content.Context; +import android.util.Slog; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.util.IndentingPrintWriter; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.JobSchedulerService.Constants; +import com.android.server.job.StateChangedListener; + +import java.util.function.Predicate; + +/** + * Incorporates shared controller logic between the various controllers of the JobManager. + * These are solely responsible for tracking a list of jobs, and notifying the JM when these + * are ready to run, or whether they must be stopped. + */ +public abstract class StateController { + private static final String TAG = "JobScheduler.SC"; + + protected final JobSchedulerService mService; + protected final StateChangedListener mStateChangedListener; + protected final Context mContext; + protected final Object mLock; + protected final Constants mConstants; + + StateController(JobSchedulerService service) { + mService = service; + mStateChangedListener = service; + mContext = service.getTestableContext(); + mLock = service.getLock(); + mConstants = service.getConstants(); + } + + /** + * Called when the system boot phase has reached + * {@link com.android.server.SystemService#PHASE_SYSTEM_SERVICES_READY}. + */ + public void onSystemServicesReady() { + } + + /** + * Implement the logic here to decide whether a job should be tracked by this controller. + * This logic is put here so the JobManager can be completely agnostic of Controller logic. + * Also called when updating a task, so implementing controllers have to be aware of + * preexisting tasks. + */ + public abstract void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob); + + /** + * Optionally implement logic here to prepare the job to be executed. + */ + public void prepareForExecutionLocked(JobStatus jobStatus) { + } + + /** + * Remove task - this will happen if the task is cancelled, completed, etc. + */ + public abstract void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, + boolean forUpdate); + + /** + * Called when a new job is being created to reschedule an old failed job. + */ + public void rescheduleForFailureLocked(JobStatus newJob, JobStatus failureToReschedule) { + } + + /** + * Called when the JobScheduler.Constants are updated. + */ + public void onConstantsUpdatedLocked() { + } + + /** Called when a package is uninstalled from the device (not for an update). */ + public void onAppRemovedLocked(String packageName, int uid) { + } + + /** Called when a user is removed from the device. */ + public void onUserRemovedLocked(int userId) { + } + + /** + * Called when JobSchedulerService has determined that the job is not ready to be run. The + * Controller can evaluate if it can or should do something to promote this job's readiness. + */ + public void evaluateStateLocked(JobStatus jobStatus) { + } + + /** + * Called when something with the UID has changed. The controller should re-evaluate any + * internal state tracking dependent on this UID. + */ + public void reevaluateStateLocked(int uid) { + } + + protected boolean wouldBeReadyWithConstraintLocked(JobStatus jobStatus, int constraint) { + // This is very cheap to check (just a few conditions on data in JobStatus). + final boolean jobWouldBeReady = jobStatus.wouldBeReadyWithConstraint(constraint); + if (DEBUG) { + Slog.v(TAG, "wouldBeReadyWithConstraintLocked: " + jobStatus.toShortString() + + " constraint=" + constraint + + " readyWithConstraint=" + jobWouldBeReady); + } + if (!jobWouldBeReady) { + // If the job wouldn't be ready, nothing to do here. + return false; + } + + // This is potentially more expensive since JSS may have to query component + // presence. + return mService.areComponentsInPlaceLocked(jobStatus); + } + + public abstract void dumpControllerStateLocked(IndentingPrintWriter pw, + Predicate<JobStatus> predicate); + public abstract void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, + Predicate<JobStatus> predicate); + + /** Dump any internal constants the Controller may have. */ + public void dumpConstants(IndentingPrintWriter pw) { + } + + /** Dump any internal constants the Controller may have. */ + public void dumpConstants(ProtoOutputStream proto) { + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/StorageController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/StorageController.java new file mode 100644 index 000000000000..51187dff4d59 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/StorageController.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2017 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 static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.UserHandle; +import android.util.ArraySet; +import android.util.Log; +import android.util.Slog; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.IndentingPrintWriter; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.StateControllerProto; +import com.android.server.storage.DeviceStorageMonitorService; + +import java.util.function.Predicate; + +/** + * Simple controller that tracks the status of the device's storage. + */ +public final class StorageController extends StateController { + private static final String TAG = "JobScheduler.Storage"; + private static final boolean DEBUG = JobSchedulerService.DEBUG + || Log.isLoggable(TAG, Log.DEBUG); + + private final ArraySet<JobStatus> mTrackedTasks = new ArraySet<JobStatus>(); + private final StorageTracker mStorageTracker; + + @VisibleForTesting + public StorageTracker getTracker() { + return mStorageTracker; + } + + public StorageController(JobSchedulerService service) { + super(service); + mStorageTracker = new StorageTracker(); + mStorageTracker.startTracking(); + } + + @Override + public void maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob) { + if (taskStatus.hasStorageNotLowConstraint()) { + mTrackedTasks.add(taskStatus); + taskStatus.setTrackingController(JobStatus.TRACKING_STORAGE); + taskStatus.setStorageNotLowConstraintSatisfied(mStorageTracker.isStorageNotLow()); + } + } + + @Override + public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, + boolean forUpdate) { + if (taskStatus.clearTrackingController(JobStatus.TRACKING_STORAGE)) { + mTrackedTasks.remove(taskStatus); + } + } + + private void maybeReportNewStorageState() { + final boolean storageNotLow = mStorageTracker.isStorageNotLow(); + boolean reportChange = false; + synchronized (mLock) { + for (int i = mTrackedTasks.size() - 1; i >= 0; i--) { + final JobStatus ts = mTrackedTasks.valueAt(i); + reportChange |= ts.setStorageNotLowConstraintSatisfied(storageNotLow); + } + } + if (storageNotLow) { + // Tell the scheduler that any ready jobs should be flushed. + mStateChangedListener.onRunJobNow(null); + } else if (reportChange) { + // Let the scheduler know that state has changed. This may or may not result in an + // execution. + mStateChangedListener.onControllerStateChanged(); + } + } + + public final class StorageTracker extends BroadcastReceiver { + /** + * Track whether storage is low. + */ + private boolean mStorageLow; + /** Sequence number of last broadcast. */ + private int mLastStorageSeq = -1; + + public StorageTracker() { + } + + public void startTracking() { + IntentFilter filter = new IntentFilter(); + + // Storage status. Just need to register, since STORAGE_LOW is a sticky + // broadcast we will receive that if it is currently active. + filter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW); + filter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); + mContext.registerReceiver(this, filter); + } + + public boolean isStorageNotLow() { + return !mStorageLow; + } + + public int getSeq() { + return mLastStorageSeq; + } + + @Override + public void onReceive(Context context, Intent intent) { + onReceiveInternal(intent); + } + + @VisibleForTesting + public void onReceiveInternal(Intent intent) { + final String action = intent.getAction(); + mLastStorageSeq = intent.getIntExtra(DeviceStorageMonitorService.EXTRA_SEQUENCE, + mLastStorageSeq); + if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(action)) { + if (DEBUG) { + Slog.d(TAG, "Available storage too low to do work. @ " + + sElapsedRealtimeClock.millis()); + } + mStorageLow = true; + maybeReportNewStorageState(); + } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(action)) { + if (DEBUG) { + Slog.d(TAG, "Available storage high enough to do work. @ " + + sElapsedRealtimeClock.millis()); + } + mStorageLow = false; + maybeReportNewStorageState(); + } + } + } + + @Override + public void dumpControllerStateLocked(IndentingPrintWriter pw, + Predicate<JobStatus> predicate) { + pw.println("Not low: " + mStorageTracker.isStorageNotLow()); + pw.println("Sequence: " + mStorageTracker.getSeq()); + pw.println(); + + for (int i = 0; i < mTrackedTasks.size(); i++) { + final JobStatus js = mTrackedTasks.valueAt(i); + if (!predicate.test(js)) { + continue; + } + pw.print("#"); + js.printUniqueId(pw); + pw.print(" from "); + UserHandle.formatUid(pw, js.getSourceUid()); + pw.println(); + } + } + + @Override + public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, + Predicate<JobStatus> predicate) { + final long token = proto.start(fieldId); + final long mToken = proto.start(StateControllerProto.STORAGE); + + proto.write(StateControllerProto.StorageController.IS_STORAGE_NOT_LOW, + mStorageTracker.isStorageNotLow()); + proto.write(StateControllerProto.StorageController.LAST_BROADCAST_SEQUENCE_NUMBER, + mStorageTracker.getSeq()); + + for (int i = 0; i < mTrackedTasks.size(); i++) { + final JobStatus js = mTrackedTasks.valueAt(i); + if (!predicate.test(js)) { + continue; + } + final long jsToken = proto.start(StateControllerProto.StorageController.TRACKED_JOBS); + js.writeToShortProto(proto, StateControllerProto.StorageController.TrackedJob.INFO); + proto.write(StateControllerProto.StorageController.TrackedJob.SOURCE_UID, + js.getSourceUid()); + proto.end(jsToken); + } + + proto.end(mToken); + proto.end(token); + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/TimeController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/TimeController.java new file mode 100644 index 000000000000..1bb9e967c025 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/TimeController.java @@ -0,0 +1,604 @@ +/* + * Copyright (C) 2014 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 static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.AlarmManager; +import android.app.AlarmManager.OnAlarmListener; +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.os.Process; +import android.os.UserHandle; +import android.os.WorkSource; +import android.provider.Settings; +import android.util.KeyValueListParser; +import android.util.Log; +import android.util.Slog; +import android.util.TimeUtils; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.IndentingPrintWriter; +import com.android.server.job.ConstantsProto; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.StateControllerProto; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.function.Predicate; + +/** + * This class sets an alarm for the next expiring job, and determines whether a job's minimum + * delay has been satisfied. + */ +public final class TimeController extends StateController { + private static final String TAG = "JobScheduler.Time"; + private static final boolean DEBUG = JobSchedulerService.DEBUG + || Log.isLoggable(TAG, Log.DEBUG); + + /** Deadline alarm tag for logging purposes */ + private final String DEADLINE_TAG = "*job.deadline*"; + /** Delay alarm tag for logging purposes */ + private final String DELAY_TAG = "*job.delay*"; + + private final Handler mHandler; + private final TcConstants mTcConstants; + + private long mNextJobExpiredElapsedMillis; + private long mNextDelayExpiredElapsedMillis; + + private final boolean mChainedAttributionEnabled; + + private AlarmManager mAlarmService = null; + /** List of tracked jobs, sorted asc. by deadline */ + private final List<JobStatus> mTrackedJobs = new LinkedList<>(); + + public TimeController(JobSchedulerService service) { + super(service); + + mNextJobExpiredElapsedMillis = Long.MAX_VALUE; + mNextDelayExpiredElapsedMillis = Long.MAX_VALUE; + mChainedAttributionEnabled = mService.isChainedAttributionEnabled(); + + mHandler = new Handler(mContext.getMainLooper()); + mTcConstants = new TcConstants(mHandler); + } + + @Override + public void onSystemServicesReady() { + mTcConstants.start(mContext.getContentResolver()); + } + + /** + * Check if the job has a timing constraint, and if so determine where to insert it in our + * list. + */ + @Override + public void maybeStartTrackingJobLocked(JobStatus job, JobStatus lastJob) { + if (job.hasTimingDelayConstraint() || job.hasDeadlineConstraint()) { + maybeStopTrackingJobLocked(job, null, false); + + // First: check the constraints now, because if they are already satisfied + // then there is no need to track it. This gives us a fast path for a common + // pattern of having a job with a 0 deadline constraint ("run immediately"). + // Unlike most controllers, once one of our constraints has been satisfied, it + // will never be unsatisfied (our time base can not go backwards). + final long nowElapsedMillis = sElapsedRealtimeClock.millis(); + if (job.hasDeadlineConstraint() && evaluateDeadlineConstraint(job, nowElapsedMillis)) { + return; + } else if (job.hasTimingDelayConstraint() && evaluateTimingDelayConstraint(job, + nowElapsedMillis)) { + if (!job.hasDeadlineConstraint()) { + // If it doesn't have a deadline, we'll never have to touch it again. + return; + } + } + + boolean isInsert = false; + ListIterator<JobStatus> it = mTrackedJobs.listIterator(mTrackedJobs.size()); + while (it.hasPrevious()) { + JobStatus ts = it.previous(); + if (ts.getLatestRunTimeElapsed() < job.getLatestRunTimeElapsed()) { + // Insert + isInsert = true; + break; + } + } + if (isInsert) { + it.next(); + } + it.add(job); + + job.setTrackingController(JobStatus.TRACKING_TIME); + WorkSource ws = deriveWorkSource(job.getSourceUid(), job.getSourcePackageName()); + + // Only update alarms if the job would be ready with the relevant timing constraint + // satisfied. + if (job.hasTimingDelayConstraint() + && wouldBeReadyWithConstraintLocked(job, JobStatus.CONSTRAINT_TIMING_DELAY)) { + maybeUpdateDelayAlarmLocked(job.getEarliestRunTime(), ws); + } + if (job.hasDeadlineConstraint() + && wouldBeReadyWithConstraintLocked(job, JobStatus.CONSTRAINT_DEADLINE)) { + maybeUpdateDeadlineAlarmLocked(job.getLatestRunTimeElapsed(), ws); + } + } + } + + /** + * When we stop tracking a job, we only need to update our alarms if the job we're no longer + * tracking was the one our alarms were based off of. + */ + @Override + public void maybeStopTrackingJobLocked(JobStatus job, JobStatus incomingJob, + boolean forUpdate) { + if (job.clearTrackingController(JobStatus.TRACKING_TIME)) { + if (mTrackedJobs.remove(job)) { + checkExpiredDelaysAndResetAlarm(); + checkExpiredDeadlinesAndResetAlarm(); + } + } + } + + @Override + public void evaluateStateLocked(JobStatus job) { + final long nowElapsedMillis = sElapsedRealtimeClock.millis(); + + // Check deadline constraint first because if it's satisfied, we avoid a little bit of + // unnecessary processing of the timing delay. + if (job.hasDeadlineConstraint() + && !job.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE) + && job.getLatestRunTimeElapsed() <= mNextJobExpiredElapsedMillis) { + if (evaluateDeadlineConstraint(job, nowElapsedMillis)) { + checkExpiredDeadlinesAndResetAlarm(); + checkExpiredDelaysAndResetAlarm(); + } else { + final boolean isAlarmForJob = + job.getLatestRunTimeElapsed() == mNextJobExpiredElapsedMillis; + final boolean wouldBeReady = wouldBeReadyWithConstraintLocked( + job, JobStatus.CONSTRAINT_DEADLINE); + if ((isAlarmForJob && !wouldBeReady) || (!isAlarmForJob && wouldBeReady)) { + checkExpiredDeadlinesAndResetAlarm(); + } + } + } + if (job.hasTimingDelayConstraint() + && !job.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY) + && job.getEarliestRunTime() <= mNextDelayExpiredElapsedMillis) { + if (evaluateTimingDelayConstraint(job, nowElapsedMillis)) { + checkExpiredDelaysAndResetAlarm(); + } else { + final boolean isAlarmForJob = + job.getEarliestRunTime() == mNextDelayExpiredElapsedMillis; + final boolean wouldBeReady = wouldBeReadyWithConstraintLocked( + job, JobStatus.CONSTRAINT_TIMING_DELAY); + if ((isAlarmForJob && !wouldBeReady) || (!isAlarmForJob && wouldBeReady)) { + checkExpiredDelaysAndResetAlarm(); + } + } + } + } + + @Override + public void reevaluateStateLocked(int uid) { + checkExpiredDeadlinesAndResetAlarm(); + checkExpiredDelaysAndResetAlarm(); + } + + /** + * Determines whether this controller can stop tracking the given job. + * The controller is no longer interested in a job once its time constraint is satisfied, and + * the job's deadline is fulfilled - unlike other controllers a time constraint can't toggle + * back and forth. + */ + private boolean canStopTrackingJobLocked(JobStatus job) { + return (!job.hasTimingDelayConstraint() + || job.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY)) + && (!job.hasDeadlineConstraint() + || job.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE)); + } + + private void ensureAlarmServiceLocked() { + if (mAlarmService == null) { + mAlarmService = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); + } + } + + /** + * Checks list of jobs for ones that have an expired deadline, sending them to the JobScheduler + * if so, removing them from this list, and updating the alarm for the next expiry time. + */ + @VisibleForTesting + void checkExpiredDeadlinesAndResetAlarm() { + synchronized (mLock) { + long nextExpiryTime = Long.MAX_VALUE; + int nextExpiryUid = 0; + String nextExpiryPackageName = null; + final long nowElapsedMillis = sElapsedRealtimeClock.millis(); + + ListIterator<JobStatus> it = mTrackedJobs.listIterator(); + while (it.hasNext()) { + JobStatus job = it.next(); + if (!job.hasDeadlineConstraint()) { + continue; + } + + if (evaluateDeadlineConstraint(job, nowElapsedMillis)) { + if (job.isReady()) { + // If the job still isn't ready, there's no point trying to rush the + // Scheduler. + mStateChangedListener.onRunJobNow(job); + } + it.remove(); + } else { // Sorted by expiry time, so take the next one and stop. + if (!wouldBeReadyWithConstraintLocked(job, JobStatus.CONSTRAINT_DEADLINE)) { + if (DEBUG) { + Slog.i(TAG, + "Skipping " + job + " because deadline won't make it ready."); + } + continue; + } + nextExpiryTime = job.getLatestRunTimeElapsed(); + nextExpiryUid = job.getSourceUid(); + nextExpiryPackageName = job.getSourcePackageName(); + break; + } + } + setDeadlineExpiredAlarmLocked(nextExpiryTime, + deriveWorkSource(nextExpiryUid, nextExpiryPackageName)); + } + } + + /** @return true if the job's deadline constraint is satisfied */ + private boolean evaluateDeadlineConstraint(JobStatus job, long nowElapsedMillis) { + final long jobDeadline = job.getLatestRunTimeElapsed(); + + if (jobDeadline <= nowElapsedMillis) { + if (job.hasTimingDelayConstraint()) { + job.setTimingDelayConstraintSatisfied(true); + } + job.setDeadlineConstraintSatisfied(true); + return true; + } + return false; + } + + /** + * Handles alarm that notifies us that a job's delay has expired. Iterates through the list of + * tracked jobs and marks them as ready as appropriate. + */ + @VisibleForTesting + void checkExpiredDelaysAndResetAlarm() { + synchronized (mLock) { + final long nowElapsedMillis = sElapsedRealtimeClock.millis(); + long nextDelayTime = Long.MAX_VALUE; + int nextDelayUid = 0; + String nextDelayPackageName = null; + boolean ready = false; + Iterator<JobStatus> it = mTrackedJobs.iterator(); + while (it.hasNext()) { + final JobStatus job = it.next(); + if (!job.hasTimingDelayConstraint()) { + continue; + } + if (evaluateTimingDelayConstraint(job, nowElapsedMillis)) { + if (canStopTrackingJobLocked(job)) { + it.remove(); + } + if (job.isReady()) { + ready = true; + } + } else { + if (!wouldBeReadyWithConstraintLocked(job, JobStatus.CONSTRAINT_TIMING_DELAY)) { + if (DEBUG) { + Slog.i(TAG, "Skipping " + job + " because delay won't make it ready."); + } + continue; + } + // If this job still doesn't have its delay constraint satisfied, + // then see if it is the next upcoming delay time for the alarm. + final long jobDelayTime = job.getEarliestRunTime(); + if (nextDelayTime > jobDelayTime) { + nextDelayTime = jobDelayTime; + nextDelayUid = job.getSourceUid(); + nextDelayPackageName = job.getSourcePackageName(); + } + } + } + if (ready) { + mStateChangedListener.onControllerStateChanged(); + } + setDelayExpiredAlarmLocked(nextDelayTime, + deriveWorkSource(nextDelayUid, nextDelayPackageName)); + } + } + + private WorkSource deriveWorkSource(int uid, @Nullable String packageName) { + if (mChainedAttributionEnabled) { + WorkSource ws = new WorkSource(); + ws.createWorkChain() + .addNode(uid, packageName) + .addNode(Process.SYSTEM_UID, "JobScheduler"); + return ws; + } else { + return packageName == null ? new WorkSource(uid) : new WorkSource(uid, packageName); + } + } + + /** @return true if the job's delay constraint is satisfied */ + private boolean evaluateTimingDelayConstraint(JobStatus job, long nowElapsedMillis) { + final long jobDelayTime = job.getEarliestRunTime(); + if (jobDelayTime <= nowElapsedMillis) { + job.setTimingDelayConstraintSatisfied(true); + return true; + } + return false; + } + + private void maybeUpdateDelayAlarmLocked(long delayExpiredElapsed, WorkSource ws) { + if (delayExpiredElapsed < mNextDelayExpiredElapsedMillis) { + setDelayExpiredAlarmLocked(delayExpiredElapsed, ws); + } + } + + private void maybeUpdateDeadlineAlarmLocked(long deadlineExpiredElapsed, WorkSource ws) { + if (deadlineExpiredElapsed < mNextJobExpiredElapsedMillis) { + setDeadlineExpiredAlarmLocked(deadlineExpiredElapsed, ws); + } + } + + /** + * Set an alarm with the {@link android.app.AlarmManager} for the next time at which a job's + * delay will expire. + * This alarm <b>will not</b> wake up the phone if + * {@link TcConstants#USE_NON_WAKEUP_ALARM_FOR_DELAY} is true. + */ + private void setDelayExpiredAlarmLocked(long alarmTimeElapsedMillis, WorkSource ws) { + alarmTimeElapsedMillis = maybeAdjustAlarmTime(alarmTimeElapsedMillis); + if (mNextDelayExpiredElapsedMillis == alarmTimeElapsedMillis) { + return; + } + mNextDelayExpiredElapsedMillis = alarmTimeElapsedMillis; + final int alarmType = + mTcConstants.USE_NON_WAKEUP_ALARM_FOR_DELAY + ? AlarmManager.ELAPSED_REALTIME : AlarmManager.ELAPSED_REALTIME_WAKEUP; + updateAlarmWithListenerLocked(DELAY_TAG, alarmType, + mNextDelayExpiredListener, mNextDelayExpiredElapsedMillis, ws); + } + + /** + * Set an alarm with the {@link android.app.AlarmManager} for the next time at which a job's + * deadline will expire. + * This alarm <b>will</b> wake up the phone. + */ + private void setDeadlineExpiredAlarmLocked(long alarmTimeElapsedMillis, WorkSource ws) { + alarmTimeElapsedMillis = maybeAdjustAlarmTime(alarmTimeElapsedMillis); + if (mNextJobExpiredElapsedMillis == alarmTimeElapsedMillis) { + return; + } + mNextJobExpiredElapsedMillis = alarmTimeElapsedMillis; + updateAlarmWithListenerLocked(DEADLINE_TAG, AlarmManager.ELAPSED_REALTIME_WAKEUP, + mDeadlineExpiredListener, mNextJobExpiredElapsedMillis, ws); + } + + private long maybeAdjustAlarmTime(long proposedAlarmTimeElapsedMillis) { + return Math.max(proposedAlarmTimeElapsedMillis, sElapsedRealtimeClock.millis()); + } + + private void updateAlarmWithListenerLocked(String tag, @AlarmManager.AlarmType int alarmType, + OnAlarmListener listener, long alarmTimeElapsed, WorkSource ws) { + ensureAlarmServiceLocked(); + if (alarmTimeElapsed == Long.MAX_VALUE) { + mAlarmService.cancel(listener); + } else { + if (DEBUG) { + Slog.d(TAG, "Setting " + tag + " for: " + alarmTimeElapsed); + } + mAlarmService.set(alarmType, alarmTimeElapsed, + AlarmManager.WINDOW_HEURISTIC, 0, tag, listener, null, ws); + } + } + + // Job/delay expiration alarm handling + + private final OnAlarmListener mDeadlineExpiredListener = new OnAlarmListener() { + @Override + public void onAlarm() { + if (DEBUG) { + Slog.d(TAG, "Deadline-expired alarm fired"); + } + checkExpiredDeadlinesAndResetAlarm(); + } + }; + + private final OnAlarmListener mNextDelayExpiredListener = new OnAlarmListener() { + @Override + public void onAlarm() { + if (DEBUG) { + Slog.d(TAG, "Delay-expired alarm fired"); + } + checkExpiredDelaysAndResetAlarm(); + } + }; + + @VisibleForTesting + class TcConstants extends ContentObserver { + private ContentResolver mResolver; + private final KeyValueListParser mParser = new KeyValueListParser(','); + + private static final String KEY_USE_NON_WAKEUP_ALARM_FOR_DELAY = + "use_non_wakeup_delay_alarm"; + + private static final boolean DEFAULT_USE_NON_WAKEUP_ALARM_FOR_DELAY = true; + + /** + * Whether or not TimeController should skip setting wakeup alarms for jobs that aren't + * ready now. + */ + public boolean USE_NON_WAKEUP_ALARM_FOR_DELAY = DEFAULT_USE_NON_WAKEUP_ALARM_FOR_DELAY; + + /** + * Creates a content observer. + * + * @param handler The handler to run {@link #onChange} on, or null if none. + */ + TcConstants(Handler handler) { + super(handler); + } + + private void start(ContentResolver resolver) { + mResolver = resolver; + mResolver.registerContentObserver(Settings.Global.getUriFor( + Settings.Global.JOB_SCHEDULER_TIME_CONTROLLER_CONSTANTS), false, this); + onChange(true, null); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + final String constants = Settings.Global.getString( + mResolver, Settings.Global.JOB_SCHEDULER_TIME_CONTROLLER_CONSTANTS); + + try { + mParser.setString(constants); + } catch (Exception e) { + // Failed to parse the settings string, log this and move on with defaults. + Slog.e(TAG, "Bad jobscheduler time controller settings", e); + } + + USE_NON_WAKEUP_ALARM_FOR_DELAY = mParser.getBoolean( + KEY_USE_NON_WAKEUP_ALARM_FOR_DELAY, DEFAULT_USE_NON_WAKEUP_ALARM_FOR_DELAY); + // Intentionally not calling checkExpiredDelaysAndResetAlarm() here. There's no need to + // iterate through the entire list again for this constant change. The next delay alarm + // that is set will make use of the new constant value. + } + + private void dump(IndentingPrintWriter pw) { + pw.println(); + pw.println("TimeController:"); + pw.increaseIndent(); + pw.printPair(KEY_USE_NON_WAKEUP_ALARM_FOR_DELAY, + USE_NON_WAKEUP_ALARM_FOR_DELAY).println(); + pw.decreaseIndent(); + } + + private void dump(ProtoOutputStream proto) { + final long tcToken = proto.start(ConstantsProto.TIME_CONTROLLER); + proto.write(ConstantsProto.TimeController.USE_NON_WAKEUP_ALARM_FOR_DELAY, + USE_NON_WAKEUP_ALARM_FOR_DELAY); + proto.end(tcToken); + } + } + + @VisibleForTesting + @NonNull + TcConstants getTcConstants() { + return mTcConstants; + } + + @Override + public void dumpControllerStateLocked(IndentingPrintWriter pw, + Predicate<JobStatus> predicate) { + final long nowElapsed = sElapsedRealtimeClock.millis(); + pw.println("Elapsed clock: " + nowElapsed); + + pw.print("Next delay alarm in "); + TimeUtils.formatDuration(mNextDelayExpiredElapsedMillis, nowElapsed, pw); + pw.println(); + pw.print("Next deadline alarm in "); + TimeUtils.formatDuration(mNextJobExpiredElapsedMillis, nowElapsed, pw); + pw.println(); + pw.println(); + + for (JobStatus ts : mTrackedJobs) { + if (!predicate.test(ts)) { + continue; + } + pw.print("#"); + ts.printUniqueId(pw); + pw.print(" from "); + UserHandle.formatUid(pw, ts.getSourceUid()); + pw.print(": Delay="); + if (ts.hasTimingDelayConstraint()) { + TimeUtils.formatDuration(ts.getEarliestRunTime(), nowElapsed, pw); + } else { + pw.print("N/A"); + } + pw.print(", Deadline="); + if (ts.hasDeadlineConstraint()) { + TimeUtils.formatDuration(ts.getLatestRunTimeElapsed(), nowElapsed, pw); + } else { + pw.print("N/A"); + } + pw.println(); + } + } + + @Override + public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, + Predicate<JobStatus> predicate) { + final long token = proto.start(fieldId); + final long mToken = proto.start(StateControllerProto.TIME); + + final long nowElapsed = sElapsedRealtimeClock.millis(); + proto.write(StateControllerProto.TimeController.NOW_ELAPSED_REALTIME, nowElapsed); + proto.write(StateControllerProto.TimeController.TIME_UNTIL_NEXT_DELAY_ALARM_MS, + mNextDelayExpiredElapsedMillis - nowElapsed); + proto.write(StateControllerProto.TimeController.TIME_UNTIL_NEXT_DEADLINE_ALARM_MS, + mNextJobExpiredElapsedMillis - nowElapsed); + + for (JobStatus ts : mTrackedJobs) { + if (!predicate.test(ts)) { + continue; + } + final long tsToken = proto.start(StateControllerProto.TimeController.TRACKED_JOBS); + ts.writeToShortProto(proto, StateControllerProto.TimeController.TrackedJob.INFO); + + proto.write(StateControllerProto.TimeController.TrackedJob.HAS_TIMING_DELAY_CONSTRAINT, + ts.hasTimingDelayConstraint()); + proto.write(StateControllerProto.TimeController.TrackedJob.DELAY_TIME_REMAINING_MS, + ts.getEarliestRunTime() - nowElapsed); + + proto.write(StateControllerProto.TimeController.TrackedJob.HAS_DEADLINE_CONSTRAINT, + ts.hasDeadlineConstraint()); + proto.write(StateControllerProto.TimeController.TrackedJob.TIME_REMAINING_UNTIL_DEADLINE_MS, + ts.getLatestRunTimeElapsed() - nowElapsed); + + proto.end(tsToken); + } + + proto.end(mToken); + proto.end(token); + } + + @Override + public void dumpConstants(IndentingPrintWriter pw) { + mTcConstants.dump(pw); + } + + @Override + public void dumpConstants(ProtoOutputStream proto) { + mTcConstants.dump(proto); + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/CarIdlenessTracker.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/CarIdlenessTracker.java new file mode 100644 index 000000000000..1e5b84d55a02 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/CarIdlenessTracker.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2018 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.idle; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.util.Log; +import android.util.Slog; +import android.util.proto.ProtoOutputStream; + +import com.android.server.am.ActivityManagerService; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.StateControllerProto; + +import java.io.PrintWriter; + +public final class CarIdlenessTracker extends BroadcastReceiver implements IdlenessTracker { + private static final String TAG = "JobScheduler.CarIdlenessTracker"; + private static final boolean DEBUG = JobSchedulerService.DEBUG + || Log.isLoggable(TAG, Log.DEBUG); + + public static final String ACTION_GARAGE_MODE_ON = + "com.android.server.jobscheduler.GARAGE_MODE_ON"; + public static final String ACTION_GARAGE_MODE_OFF = + "com.android.server.jobscheduler.GARAGE_MODE_OFF"; + + public static final String ACTION_FORCE_IDLE = "com.android.server.jobscheduler.FORCE_IDLE"; + public static final String ACTION_UNFORCE_IDLE = "com.android.server.jobscheduler.UNFORCE_IDLE"; + + // After construction, mutations of idle/screen-on state will only happen + // on the main looper thread, either in onReceive() or in an alarm callback. + private boolean mIdle; + private boolean mGarageModeOn; + private boolean mForced; + private IdlenessListener mIdleListener; + + public CarIdlenessTracker() { + // At boot we presume that the user has just "interacted" with the + // device in some meaningful way. + mIdle = false; + mGarageModeOn = false; + mForced = false; + } + + @Override + public boolean isIdle() { + return mIdle; + } + + @Override + public void startTracking(Context context, IdlenessListener listener) { + mIdleListener = listener; + + IntentFilter filter = new IntentFilter(); + + // Screen state + filter.addAction(Intent.ACTION_SCREEN_ON); + + // State of GarageMode + filter.addAction(ACTION_GARAGE_MODE_ON); + filter.addAction(ACTION_GARAGE_MODE_OFF); + + // Debugging/instrumentation + filter.addAction(ACTION_FORCE_IDLE); + filter.addAction(ACTION_UNFORCE_IDLE); + filter.addAction(ActivityManagerService.ACTION_TRIGGER_IDLE); + + context.registerReceiver(this, filter); + } + + @Override + public void dump(PrintWriter pw) { + pw.print(" mIdle: "); pw.println(mIdle); + pw.print(" mGarageModeOn: "); pw.println(mGarageModeOn); + } + + @Override + public void dump(ProtoOutputStream proto, long fieldId) { + final long token = proto.start(fieldId); + final long ciToken = proto.start( + StateControllerProto.IdleController.IdlenessTracker.CAR_IDLENESS_TRACKER); + + proto.write(StateControllerProto.IdleController.IdlenessTracker.CarIdlenessTracker.IS_IDLE, + mIdle); + proto.write( + StateControllerProto.IdleController.IdlenessTracker.CarIdlenessTracker.IS_GARAGE_MODE_ON, + mGarageModeOn); + + proto.end(ciToken); + proto.end(token); + } + + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + logIfDebug("Received action: " + action); + + // Check for forced actions + if (action.equals(ACTION_FORCE_IDLE)) { + logIfDebug("Forcing idle..."); + setForceIdleState(true); + } else if (action.equals(ACTION_UNFORCE_IDLE)) { + logIfDebug("Unforcing idle..."); + setForceIdleState(false); + } else if (action.equals(Intent.ACTION_SCREEN_ON)) { + logIfDebug("Screen is on..."); + handleScreenOn(); + } else if (action.equals(ACTION_GARAGE_MODE_ON)) { + logIfDebug("GarageMode is on..."); + mGarageModeOn = true; + updateIdlenessState(); + } else if (action.equals(ACTION_GARAGE_MODE_OFF)) { + logIfDebug("GarageMode is off..."); + mGarageModeOn = false; + updateIdlenessState(); + } else if (action.equals(ActivityManagerService.ACTION_TRIGGER_IDLE)) { + if (!mGarageModeOn) { + logIfDebug("Idle trigger fired..."); + triggerIdlenessOnce(); + } else { + logIfDebug("TRIGGER_IDLE received but not changing state; idle=" + + mIdle + " screen=" + mGarageModeOn); + } + } + } + + private void setForceIdleState(boolean forced) { + mForced = forced; + updateIdlenessState(); + } + + private void updateIdlenessState() { + final boolean newState = (mForced || mGarageModeOn); + if (mIdle != newState) { + // State of idleness changed. Notifying idleness controller + logIfDebug("Device idleness changed. New idle=" + newState); + mIdle = newState; + mIdleListener.reportNewIdleState(mIdle); + } else { + // Nothing changed, device idleness is in the same state as new state + logIfDebug("Device idleness is the same. Current idle=" + newState); + } + } + + private void triggerIdlenessOnce() { + // This is simply triggering idleness once until some constraint will switch it back off + if (mIdle) { + // Already in idle state. Nothing to do + logIfDebug("Device is already idle"); + } else { + // Going idle once + logIfDebug("Device is going idle once"); + mIdle = true; + mIdleListener.reportNewIdleState(mIdle); + } + } + + private void handleScreenOn() { + if (mForced || mGarageModeOn) { + // Even though screen is on, the device remains idle + logIfDebug("Screen is on, but device cannot exit idle"); + } else if (mIdle) { + // Exiting idle + logIfDebug("Device is exiting idle"); + mIdle = false; + } else { + // Already in non-idle state. Nothing to do + logIfDebug("Device is already non-idle"); + } + } + + private static void logIfDebug(String msg) { + if (DEBUG) { + Slog.v(TAG, msg); + } + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/DeviceIdlenessTracker.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/DeviceIdlenessTracker.java new file mode 100644 index 000000000000..f74eb3b9a45f --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/DeviceIdlenessTracker.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2018 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.idle; + +import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; + +import android.app.AlarmManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.util.Log; +import android.util.Slog; +import android.util.proto.ProtoOutputStream; + +import com.android.server.am.ActivityManagerService; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.StateControllerProto; + +import java.io.PrintWriter; + +public final class DeviceIdlenessTracker extends BroadcastReceiver implements IdlenessTracker { + private static final String TAG = "JobScheduler.DeviceIdlenessTracker"; + private static final boolean DEBUG = JobSchedulerService.DEBUG + || Log.isLoggable(TAG, Log.DEBUG); + + private AlarmManager mAlarm; + + // After construction, mutations of idle/screen-on state will only happen + // on the main looper thread, either in onReceive() or in an alarm callback. + private long mInactivityIdleThreshold; + private long mIdleWindowSlop; + private boolean mIdle; + private boolean mScreenOn; + private boolean mDockIdle; + private IdlenessListener mIdleListener; + + private AlarmManager.OnAlarmListener mIdleAlarmListener = () -> { + handleIdleTrigger(); + }; + + public DeviceIdlenessTracker() { + // At boot we presume that the user has just "interacted" with the + // device in some meaningful way. + mIdle = false; + mScreenOn = true; + mDockIdle = false; + } + + @Override + public boolean isIdle() { + return mIdle; + } + + @Override + public void startTracking(Context context, IdlenessListener listener) { + mIdleListener = listener; + mInactivityIdleThreshold = context.getResources().getInteger( + com.android.internal.R.integer.config_jobSchedulerInactivityIdleThreshold); + mIdleWindowSlop = context.getResources().getInteger( + com.android.internal.R.integer.config_jobSchedulerIdleWindowSlop); + mAlarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + + IntentFilter filter = new IntentFilter(); + + // Screen state + filter.addAction(Intent.ACTION_SCREEN_ON); + filter.addAction(Intent.ACTION_SCREEN_OFF); + + // Dreaming state + filter.addAction(Intent.ACTION_DREAMING_STARTED); + filter.addAction(Intent.ACTION_DREAMING_STOPPED); + + // Debugging/instrumentation + filter.addAction(ActivityManagerService.ACTION_TRIGGER_IDLE); + + // Wireless charging dock state + filter.addAction(Intent.ACTION_DOCK_IDLE); + filter.addAction(Intent.ACTION_DOCK_ACTIVE); + + context.registerReceiver(this, filter); + } + + @Override + public void dump(PrintWriter pw) { + pw.print(" mIdle: "); pw.println(mIdle); + pw.print(" mScreenOn: "); pw.println(mScreenOn); + pw.print(" mDockIdle: "); pw.println(mDockIdle); + } + + @Override + public void dump(ProtoOutputStream proto, long fieldId) { + final long token = proto.start(fieldId); + final long diToken = proto.start( + StateControllerProto.IdleController.IdlenessTracker.DEVICE_IDLENESS_TRACKER); + + proto.write(StateControllerProto.IdleController.IdlenessTracker.DeviceIdlenessTracker.IS_IDLE, + mIdle); + proto.write( + StateControllerProto.IdleController.IdlenessTracker.DeviceIdlenessTracker.IS_SCREEN_ON, + mScreenOn); + proto.write( + StateControllerProto.IdleController.IdlenessTracker.DeviceIdlenessTracker.IS_DOCK_IDLE, + mDockIdle); + + proto.end(diToken); + proto.end(token); + } + + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (action.equals(Intent.ACTION_SCREEN_ON) + || action.equals(Intent.ACTION_DREAMING_STOPPED) + || action.equals(Intent.ACTION_DOCK_ACTIVE)) { + if (action.equals(Intent.ACTION_DOCK_ACTIVE)) { + if (!mScreenOn) { + // Ignore this intent during screen off + return; + } else { + mDockIdle = false; + } + } else { + mScreenOn = true; + mDockIdle = false; + } + if (DEBUG) { + Slog.v(TAG,"exiting idle : " + action); + } + //cancel the alarm + mAlarm.cancel(mIdleAlarmListener); + if (mIdle) { + // possible transition to not-idle + mIdle = false; + mIdleListener.reportNewIdleState(mIdle); + } + } else if (action.equals(Intent.ACTION_SCREEN_OFF) + || action.equals(Intent.ACTION_DREAMING_STARTED) + || action.equals(Intent.ACTION_DOCK_IDLE)) { + // when the screen goes off or dreaming starts or wireless charging dock in idle, + // we schedule the alarm that will tell us when we have decided the device is + // truly idle. + if (action.equals(Intent.ACTION_DOCK_IDLE)) { + if (!mScreenOn) { + // Ignore this intent during screen off + return; + } else { + mDockIdle = true; + } + } else { + mScreenOn = false; + mDockIdle = false; + } + final long nowElapsed = sElapsedRealtimeClock.millis(); + final long when = nowElapsed + mInactivityIdleThreshold; + if (DEBUG) { + Slog.v(TAG, "Scheduling idle : " + action + " now:" + nowElapsed + " when=" + + when); + } + mAlarm.setWindow(AlarmManager.ELAPSED_REALTIME_WAKEUP, + when, mIdleWindowSlop, "JS idleness", mIdleAlarmListener, null); + } else if (action.equals(ActivityManagerService.ACTION_TRIGGER_IDLE)) { + handleIdleTrigger(); + } + } + + private void handleIdleTrigger() { + // idle time starts now. Do not set mIdle if screen is on. + if (!mIdle && (!mScreenOn || mDockIdle)) { + if (DEBUG) { + Slog.v(TAG, "Idle trigger fired @ " + sElapsedRealtimeClock.millis()); + } + mIdle = true; + mIdleListener.reportNewIdleState(mIdle); + } else { + if (DEBUG) { + Slog.v(TAG, "TRIGGER_IDLE received but not changing state; idle=" + + mIdle + " screen=" + mScreenOn); + } + } + } +}
\ No newline at end of file diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/IdlenessListener.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/IdlenessListener.java new file mode 100644 index 000000000000..7ffd7cd3e2e0 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/IdlenessListener.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2018 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.idle; + +/** + * Interface through which an IdlenessTracker informs the job scheduler of + * changes in the device's inactivity state. + */ +public interface IdlenessListener { + /** + * Tell the job scheduler that the device's idle state has changed. + * + * @param deviceIsIdle {@code true} to indicate that the device is now considered + * to be idle; {@code false} to indicate that the device is now being interacted with, + * so jobs with idle constraints should not be run. + */ + void reportNewIdleState(boolean deviceIsIdle); +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/IdlenessTracker.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/IdlenessTracker.java new file mode 100644 index 000000000000..cdab7e538ca5 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/IdlenessTracker.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2018 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.idle; + +import android.content.Context; +import android.util.proto.ProtoOutputStream; + +import java.io.PrintWriter; + +public interface IdlenessTracker { + /** + * One-time initialization: this method is called once, after construction of + * the IdlenessTracker instance. This is when the tracker should actually begin + * monitoring whatever signals it consumes in deciding when the device is in a + * non-interacting state. When the idle state changes thereafter, the given + * listener must be called to report the new state. + */ + void startTracking(Context context, IdlenessListener listener); + + /** + * Report whether the device is currently considered "idle" for purposes of + * running scheduled jobs with idleness constraints. + * + * @return {@code true} if the job scheduler should consider idleness + * constraints to be currently satisfied; {@code false} otherwise. + */ + boolean isIdle(); + + /** + * Dump useful information about tracked idleness-related state in plaintext. + */ + void dump(PrintWriter pw); + + /** + * Dump useful information about tracked idleness-related state to proto. + */ + void dump(ProtoOutputStream proto, long fieldId); +} diff --git a/apex/statsd/Android.bp b/apex/statsd/Android.bp new file mode 100644 index 000000000000..d76a40e9e26d --- /dev/null +++ b/apex/statsd/Android.bp @@ -0,0 +1,46 @@ +// Copyright (C) 2019 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. + +apex { + name: "com.android.os.statsd", + + manifest: "apex_manifest.json", + + // optional. if unspecified, a default one is auto-generated + //androidManifest: "AndroidManifest.xml", + + // libc.so and libcutils.so are included in the apex + // native_shared_libs: ["libc", "libcutils"], + // binaries: ["vold"], + // java_libs: ["core-all"], + // prebuilts: ["my_prebuilt"], + + compile_multilib: "both", + + key: "com.android.os.statsd.key", + certificate: ":com.android.os.statsd.certificate", +} + +apex_key { + name: "com.android.os.statsd.key", + public_key: "com.android.os.statsd.avbpubkey", + private_key: "com.android.os.statsd.pem", +} + +android_app_certificate { + name: "com.android.os.statsd.certificate", + // This will use com.android.os.statsd.x509.pem (the cert) and + // com.android.os.statsd.pk8 (the private key) + certificate: "com.android.os.statsd", +} diff --git a/apex/statsd/apex_manifest.json b/apex/statsd/apex_manifest.json new file mode 100644 index 000000000000..0c0ad860f3d1 --- /dev/null +++ b/apex/statsd/apex_manifest.json @@ -0,0 +1,5 @@ +{ + "name": "com.android.os.statsd", + "version": 1 +} + diff --git a/apex/statsd/com.android.os.statsd.avbpubkey b/apex/statsd/com.android.os.statsd.avbpubkey Binary files differnew file mode 100644 index 000000000000..d78af8b8bef2 --- /dev/null +++ b/apex/statsd/com.android.os.statsd.avbpubkey diff --git a/apex/statsd/com.android.os.statsd.pem b/apex/statsd/com.android.os.statsd.pem new file mode 100644 index 000000000000..558e17fd6864 --- /dev/null +++ b/apex/statsd/com.android.os.statsd.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKgIBAAKCAgEA893bbpkivKEiNgfknYBSlzC0csaKU/ddBm5Pb4ZFuab+LQSR +9DDc5JrsmxyrsrvuwL/zAtMbkyYWzEiUxJtx/w0bw8rC90GoPRSCmxyI0ZK8FuPy +IAQ7UeNfTWZ485mAUaTSasGIfQ3DY4F0P+aUSijeG3NUY02nALHDMqJX7lXR+mL1 +DUYDg05KB0jxQwlYqBeujTPPiAzEqm3PlBoHuan8/qgK2wdQMTVg/fieUD3lupmV +Wj2dRZgqfBPA16ZbV4Uo0j0bZSf+fQLiXlU2VJGb5i/FQfjLqMKGABDI0MgK7Sc2 +m4ySpV4g4XKDv/vw6Dw4kwWC7mATEVAkH+q6V7uiZeN6a7w30UMtPI8fPaUvAP3L +VBjCBIv/3m+CKkWcNxOZ3sQBQl5bS05dxcfiVsBvBLYbvQgC+Wy0Sc3b+1pXFT/E +uAsbZ4CyVsi1+PAdx3h5e2QAyNCXgZDOcvTUyxY6JLTE0LOVHmI4fJEujBex//Oz +PCRHvC8K+KiljyQWf/NYrLSD3QGYAjVMtQh7yu2yhzWzgBUxyhuv3rY4ATXsN3bJ +wW4w7/L/RSLSW5+lp/NoJOD9utbsKTyGMHOY6K8JLOmhv3ORoAEmLYlFTI+FqBi9 +AH1HQEKCyh8Z/bYHLUzGWl6FqAMtcnuintv40BbKyt0/D1ItdbSNKmOZ5rkCAwEA +AQKCAgAY7ll8mRNADYkd1Pi+UVwgMM6B3WJO6z8LZUOhtyxxqmzZ1VnGiShMBrqh +sPCsuSHTeswxQbvT81TpVZI/91RUKtbn0VbVSFUWyX4AtY4XPtUT0gHy2/vkh0Y6 +93ruDIdd0Wfhmh+GCV4sUhO8ZKpMWpk6XTQHYuzr2UCHcKlkqElrO6qpzLqXNe3D +iOWBYPc7WBB0RxO0aPnCIq/SCEc55/MBZdSWR80e+sILtNsagPl3djQaoanub3wI +a0yPv2YfMHHX7H9cfBY8WYsi8bs4MhqqEcAs2m6XtitU3mJpVcooLJYcmOZ1GYZr +BfYKLouWcnGmNi4IiLHqVzMaQDkEhAZsRaAXCkoVVrFBedLlmLPpiUIQlINF4vxe +3IcekTKWyMzkU6h+K8T15MU5mLSqeL2Gji1JIwKJno51FZ9uc++pUJVtfYQmNny8 +8RKvQ1hv/S5yLQKgN+VkNbaWlUoMP73dtUe3m/At71/2Dj7xB0KtcgT1lEMrM1GR +oynJAJLz/d0n5RUUREwkZZMcA4fQVC7Db6vpK69jPiQMShpZ3JKCEjfYLUuN0slt +FPhjiR175E0vTRuLoIj4kXNwLLswH0c9zqrKM2S92SCxAV3E4JJGKhUZalvT9s1g +LrPhMCl6CsOES98T87d3RyAIK0iVRCnRUG3bc+8rzyRd4fzkAQKCAQEA/UjmCSm3 +H46t/1w7YBZPew7SFQOAJe81iDzbonc3fNPD2R8lxtD3MwdvrQ5f9fhT4+uveWNr +dyBX7ppnissyM3LZRN+7BdeIVVeIPVen6Ou9W2i7q18ZoQx9IpRcZEw5tGJFZaGx +EmyPN4i1K0ccUkGbBvbXXQ/tcG3wElRpBAc5/TQ8vrpUgHll2/MbYhowx6P9uHv5 +thoyG98X+7Fbg8ikzw5GtyuedXfyX1CpJ7yUQVS2PEaOMXOkZdx2bbWRAYYCpsqB +dMmjs2PsFhZHu6CpLhlocHbfUiRztCUCaMZJPQXFSVmy8QDMvZEdVLvad9Poi8ny +lmHVRgxaNbAtIQKCAQEA9nscqRaaO7hIX9bOUxcDbI0486Ws4H0hAFApIN+6/LP4 +hkxey3xWArTYWrvSG1d5GkJAdn99ayWzo2PevmJlrhIJiO1QqYBAk+87cnhwSCmB +kb0sGkNWcc/xNRy7eqdhyCmVhaUnIbORee+cD6qiu/l2BAclTf2ZARFOGXjhQkvt +cDbc/9ZR467ceXbiTIU34Be4xnNAY1mo59jvwl9eqxgpefYTqPhcZ7OmlDli77Hd +XuRfuxLZCscv7A9M5Enc2zwOEP5VwRNwYzYtMm2Yh9CQZxNWC7JVh1Gw5MPFzsGl +sgEdb4WGneN6PPLQHK7NF0f7wYSNnF0i3XSME9MumQKCAQEA0qMbWydr+TyJC0LC +xigHtUkgAQXGPsXuePxTk4sdhBwAVcKHgg4qZi+a+gpoV4BLE9LfPU4nAwzM08to +rI5Lk2nBsnt1Z2hVItQGoy0QoK3b7fbti5ktETf3oRhMtcSGgLLxD5ImVjId8Isq +T3F15hpVOLdzZxtl1Qg4jKXSJ91yplYY5mzC9Yz/3qkQbsdlJcIFsLS5eG3UmkUw +Bsr6VmA4X1F6Eb6eqwYzdHz6D+fOS36NhxcODaYkY+myO46xptixv8/NVTiTgQ5q +OfwRb8Iur/3FUzIoioFyD7Bvjn7ITY1NArEsFS0bF9Nk1yDakKiUThyGN/Xojbac +FuYKwQKCAQEAxOWJ+qU8phJLdowBHC0ZJiEWasRhep9auoZOpJ01IWOfV6EwZLs5 +dkYDQ1Agwoi5DDn6hu7HQM3IV/CS4mF2OnzcMw7ozc7PR53nTkVZ5LuLbuHAlmZO +avKjDDucpJmLqjtV34IT5X8t6kt3zqgQAbuBBCy1Jz07ebfaPMzsnWpMDcU1/AW4 +OvrX0wweMOSGwzQP/i/ZMsRQAo2w0gQfeuv9Thk+kU99ebXwjx3co//hCEnFE4s1 +6L8/0AJU+VTr4hJyZi7WUDt4HzkLF+qm22/Hux+eMA/Q9R1UAxtFLCpTdAQiAJGY +/Q3X+1I434DgAwYU3f1Gpq9cB65vq/KamQKCAQEAjIub5wde/ttHlLALvnOXrbqe +nUIfWHExMzhul/rkr8fFEJwij2nZUuN2EWUGzBWQQoNXw5QKHLZyPsyFUOa/P2BS +osnffAa+sumL4k36E71xFdTVV5ExyTXZVB49sPmUpivP9gEucFFqDHKjGsF45dBF ++DZdykLUIv+/jQUzXGkZ5Wv/r52YUNho4EZdwnlJ2so7cxnsYnjW+c1nlp17tkq5 +DfwktkeD9iFzlaZ66vLoO44luaBm+lC3xM2sHinOTwbk0gvhJAIoLfkOYhpmGc8A +4W/E1OHfVz6xqVDsMBFhRbQpHNkf8XZNqkIoqHVMTaMOJJlM+lb0+A9B8Bm/XA== +-----END RSA PRIVATE KEY----- diff --git a/apex/statsd/com.android.os.statsd.pk8 b/apex/statsd/com.android.os.statsd.pk8 Binary files differnew file mode 100644 index 000000000000..49910f80a05c --- /dev/null +++ b/apex/statsd/com.android.os.statsd.pk8 diff --git a/apex/statsd/com.android.os.statsd.x509.pem b/apex/statsd/com.android.os.statsd.x509.pem new file mode 100644 index 000000000000..e7b16b2048cb --- /dev/null +++ b/apex/statsd/com.android.os.statsd.x509.pem @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFDTCCAvWgAwIBAgIUCnta1LAl5fMMLLQx//4zWz9A2A8wDQYJKoZIhvcNAQEL +BQAwFTETMBEGA1UECgwKR29vZ2xlIExMQzAgFw0xOTA4MTIyMjM5MzBaGA80NzU3 +MDcwODIyMzkzMFowFTETMBEGA1UECgwKR29vZ2xlIExMQzCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAOranWZ19jkXCF9WIlXv01tUUvLKMHWKV7X9Earw +cL7/aax0pFbNJutgyBUiOszbR+0T7quZxz6jACu+6y7iaMJnvMluZsfTi+p2UvQt +y6Ql7ZUOQ7bVluCFIW5hZ+8d9RrLmZdvX1r4YfF6HufDBkAbj+os+y6407OezJAV +8EATpemc9gsCC4RJZpwzTs1RUXMD4UoNrLZAE8+7iaJZeBxmz0MAPj92pYc9M7/d +xInzYvOR08/uEpHt8jlMdVgSQS/FaRlIOIqcGBk3cjkjDlpVATQ4Hyjy+IPQPjTD +bJUmDJiYeBCyY/pYZQvTQjl8s+fvykTsF9Lfb+E+PhZ0+N8pRi7sUSpisZHSiqaN +W3oxYWc0YQSuzygHHog8HH/azHX5L805g/+Rwfb/cUF9eJgjq0vrkFnsz4UKgKNV +hHL90mfqpbc2UvJ8VY8BvIjbsHQ77LrBKlqI9VMPorttpTOuwHHJPKsyN972F0Ul +lRB6CwFE8csVGWXoNaDZWBv7xTDdbdirmlKDNueg9pw6ksYV2Is9Dv8PxmsZvb+4 +oftC/hb4X1Pudn01PPs9Tx44CwHuVLENUwlDEVzG5zNetsv9kAuCYt3VRVF+NYqj +NAfLbxCKLe25wGzJrZUEJ1YrYIjpUbfwnttEad/9Pu13DAS7HZwn5vwqEKB/1LlT +NSUXAgMBAAGjUzBRMB0GA1UdDgQWBBSKElkhJSbzgh8+iysye8SrkmJ62DAfBgNV +HSMEGDAWgBSKElkhJSbzgh8+iysye8SrkmJ62DAPBgNVHRMBAf8EBTADAQH/MA0G +CSqGSIb3DQEBCwUAA4ICAQANFGnc2wJBrFbh2nzhl06g4TjPKGRCw365vZ1A3T9O +jXP0lToHDxB33TpKk6d7zszR1uPphQQxgzhSVZB/jx8q4kWSSoKoF9Dlx7h8rAt+ +2TM5DaBvxrwu5mqOALwQuF81wap1Pl2L2fFHvygCm8b+Ci4iS5vcr0axNnp1rK1b +vUtRWY4mfxTjJYcgeCVUGskqTb+cCxQZ6Icno6VTKajT1FybRmD3KZJaUuLbNEN+ +IE4nGTMG2WZ5Hl2vR8JJp1sYYn8T3ElMAb0MSNFkqsfI+tToEwGsuJDgYEdtEnzf +lTycQvn5NhrIZRRN3pqSyWpAU7p9mmyTK0PHMz2D/Rtfb7lE692vXzxCmZND51mc +YXCCoanV6eZZ7Sbqzh60+5QV38hgFBst5l8CcFaWWSFK9nBWdzS5lhs9lmQ4aiYd +IE0qsNZgMob+TTP1VW39hu4EDjNmOrKfimM9J2tcPZ5QP01DgETPvAsB7vn2Xz9J +HGt5ntiSV4W2izDP8viQ1M5NvfdBaUhcnNsE6/sxfU0USRs2hrEp1oiqrv4p6V0P +qOt7C2/YtJzkrxfsHZAxBUSRHa7LwtzgeiJDUivHn94VnAzSAH8MLx6CzDPQ8HWN +NiZFxTKfMVyjEmbQ2PalHWB8pWtpdEh7X4rzaqhnLBTis3pGssASgo3ArLIYleAU ++g== +-----END CERTIFICATE----- |