diff options
Diffstat (limited to 'apex')
83 files changed, 30971 insertions, 0 deletions
diff --git a/apex/blobstore/framework/Android.bp b/apex/blobstore/framework/Android.bp new file mode 100644 index 000000000000..24693511117c --- /dev/null +++ b/apex/blobstore/framework/Android.bp @@ -0,0 +1,40 @@ +// 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. + +filegroup { + name: "framework-blobstore-sources", + srcs: [ + "java/**/*.java", + "java/**/*.aidl" + ], + path: "java", +} + +java_library { + name: "blobstore-framework", + installable: false, + compile_dex: true, + sdk_version: "core_platform", + srcs: [ + ":framework-blobstore-sources", + ], + aidl: { + export_include_dirs: [ + "java", + ], + }, + libs: [ + "framework-minus-apex", + ], +} diff --git a/apex/blobstore/framework/java/android/app/blob/BlobStoreManager.java b/apex/blobstore/framework/java/android/app/blob/BlobStoreManager.java new file mode 100644 index 000000000000..1ed188e69881 --- /dev/null +++ b/apex/blobstore/framework/java/android/app/blob/BlobStoreManager.java @@ -0,0 +1,41 @@ +/* + * Copyright 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.blob; + +import android.annotation.SystemService; +import android.content.Context; + +/** + * This class provides access to the blob store maintained by the system. + * + * Apps can publish data blobs which might be useful for other apps on the device to be + * maintained by the system and apps that would like to access these data blobs can do so + * by addressing them via their cryptographically secure hashes. + * + * TODO: make this public once the APIs are added. + * @hide + */ +@SystemService(Context.BLOB_STORE_SERVICE) +public class BlobStoreManager { + private final Context mContext; + private final IBlobStoreManager mService; + + /** @hide */ + public BlobStoreManager(Context context, IBlobStoreManager service) { + mContext = context; + mService = service; + } +} diff --git a/apex/blobstore/framework/java/android/app/blob/BlobStoreManagerFrameworkInitializer.java b/apex/blobstore/framework/java/android/app/blob/BlobStoreManagerFrameworkInitializer.java new file mode 100644 index 000000000000..56c419ab0591 --- /dev/null +++ b/apex/blobstore/framework/java/android/app/blob/BlobStoreManagerFrameworkInitializer.java @@ -0,0 +1,34 @@ +/* + * Copyright 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.blob; + +import android.app.SystemServiceRegistry; +import android.content.Context; + +/** + * This is where the BlobStoreManagerService wrapper is registered. + * + * @hide + */ +public class BlobStoreManagerFrameworkInitializer { + /** Register the BlobStoreManager wrapper class */ + public static void initialize() { + SystemServiceRegistry.registerContextAwareService( + Context.BLOB_STORE_SERVICE, BlobStoreManager.class, + (context, service) -> + new BlobStoreManager(context, IBlobStoreManager.Stub.asInterface(service))); + } +} diff --git a/apex/blobstore/framework/java/android/app/blob/IBlobStoreManager.aidl b/apex/blobstore/framework/java/android/app/blob/IBlobStoreManager.aidl new file mode 100644 index 000000000000..00c1ed4daa27 --- /dev/null +++ b/apex/blobstore/framework/java/android/app/blob/IBlobStoreManager.aidl @@ -0,0 +1,20 @@ +/** + * Copyright 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.blob; + +/** {@hide} */ +interface IBlobStoreManager { +}
\ No newline at end of file diff --git a/apex/blobstore/service/Android.bp b/apex/blobstore/service/Android.bp new file mode 100644 index 000000000000..019f98937df3 --- /dev/null +++ b/apex/blobstore/service/Android.bp @@ -0,0 +1,27 @@ +// 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. + +java_library { + name: "blobstore-service", + installable: true, + + srcs: [ + "java/**/*.java", + ], + + libs: [ + "framework", + "services.core", + ], +}
\ No newline at end of file diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java new file mode 100644 index 000000000000..d7cab5998881 --- /dev/null +++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java @@ -0,0 +1,39 @@ +/* + * Copyright 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.blob; + +import android.app.blob.IBlobStoreManager; +import android.content.Context; + +import com.android.server.SystemService; + +/** + * Service responsible for maintaining and facilitating access to data blobs published by apps. + */ +public class BlobStoreManagerService extends SystemService { + + public BlobStoreManagerService(Context context) { + super(context); + } + + @Override + public void onStart() { + publishBinderService(Context.BLOB_STORE_SERVICE, new Stub()); + } + + private class Stub extends IBlobStoreManager.Stub { + } +} 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..ea20e3e29d99 --- /dev/null +++ b/apex/jobscheduler/README_js-mainline.md @@ -0,0 +1,20 @@ +# Making Job Scheduler into a Mainline Module + +## 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/ethernet-service.jar:/system/framework/com.android.location.provider.jar:/system/framework/jobscheduler-service.jar` + + `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. + +The current structure is *not* the final design. diff --git a/apex/jobscheduler/framework/Android.bp b/apex/jobscheduler/framework/Android.bp new file mode 100644 index 000000000000..3902aa212e32 --- /dev/null +++ b/apex/jobscheduler/framework/Android.bp @@ -0,0 +1,29 @@ +filegroup { + name: "framework-jobscheduler-sources", + 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: false, + compile_dex: true, + sdk_version: "core_platform", + srcs: [ + ":framework-jobscheduler-sources", + ], + aidl: { + export_include_dirs: [ + "java", + ], + }, + libs: [ + "framework-minus-apex", + ], +} 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..42cf17b1264e --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java @@ -0,0 +1,395 @@ +/* + * 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 + * @deprecated use {@link #getReasonCodeDescription(int)} + */ + @Deprecated + 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; + } + } + + /** @hide */ + // @SystemApi TODO make it a system api for mainline + @NonNull + public static int[] getJobStopReasonCodes() { + return JOB_STOP_REASON_CODES; + } + + /** @hide */ + // @SystemApi TODO make it a system api for mainline + @NonNull + public static String getReasonCodeDescription(int reasonCode) { + return getReasonName(reasonCode); + } + + @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..f3ec5e5752a0 --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/JobSchedulerFrameworkInitializer.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 android.app.job; + +import android.app.JobSchedulerImpl; +import android.app.SystemServiceRegistry; +import android.content.Context; +import android.os.DeviceIdleManager; +import android.os.IDeviceIdleController; + +/** + * Class holding initialization code for the job scheduler module. + * + * @hide + */ +public class JobSchedulerFrameworkInitializer { + private JobSchedulerFrameworkInitializer() { + } + + /** + * Called by {@link SystemServiceRegistry}'s static initializer and registers + * {@link JobScheduler} and other services to {@link Context}, so + * {@link Context#getSystemService} can return them. + * + * <p>If this is called from other places, it throws a {@link IllegalStateException). + * + * TODO Make it a system API + */ + public static void registerServiceWrappers() { + SystemServiceRegistry.registerStaticService( + Context.JOB_SCHEDULER_SERVICE, JobScheduler.class, + (b) -> new JobSchedulerImpl(IJobScheduler.Stub.asInterface(b))); + SystemServiceRegistry.registerContextAwareService( + Context.DEVICE_IDLE_CONTROLLER, DeviceIdleManager.class, + (context, b) -> new DeviceIdleManager( + context, IDeviceIdleController.Stub.asInterface(b))); + } +} 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..0568beb34e08 --- /dev/null +++ b/apex/jobscheduler/framework/java/android/os/DeviceIdleManager.java @@ -0,0 +1,100 @@ +/* + * 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.RequiresPermission; +import android.annotation.SystemService; +import android.annotation.TestApi; +import android.content.Context; + +import java.util.List; + +/** + * 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]; + } + } + + /** + * Add the specified packages to the power save whitelist. + * + * @return the number of packages that were successfully added to the whitelist + */ + @RequiresPermission(android.Manifest.permission.DEVICE_POWER) + public int addPowerSaveWhitelistApps(@NonNull List<String> packageNames) { + try { + return mService.addPowerSaveWhitelistApps(packageNames); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + return 0; + } + } + + /** + * Return whether a given package is in the power-save whitelist or not. + * @hide + */ + public boolean isApplicationWhitelisted(@NonNull String packageName) { + try { + return mService.isPowerSaveWhitelistApp(packageName); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + return false; + } + } +} 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..21ce5ccd3ccc --- /dev/null +++ b/apex/jobscheduler/framework/java/android/os/IDeviceIdleController.aidl @@ -0,0 +1,51 @@ +/** + * 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); + int addPowerSaveWhitelistApps(in List<String> packageNames); + 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(); + @UnsupportedAppUsage + String[] getFullPowerWhitelistExceptIdle(); + String[] getFullPowerWhitelist(); + int[] getAppIdWhitelistExceptIdle(); + int[] getAppIdWhitelist(); + int[] getAppIdUserWhitelist(); + @UnsupportedAppUsage + 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..6475f5706a6d --- /dev/null +++ b/apex/jobscheduler/framework/java/com/android/server/DeviceIdleInternal.java @@ -0,0 +1,72 @@ +/* + * 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(); + + /** + * Listener to be notified when DeviceIdleController determines that the device has moved or is + * stationary. + */ + interface StationaryListener { + void onDeviceStationaryChanged(boolean isStationary); + } + + /** + * Registers a listener that will be notified when the system has detected that the device is + * stationary or in motion. + */ + void registerStationaryListener(StationaryListener listener); + + /** + * Unregisters a registered stationary listener from being notified when the system has detected + * that the device is stationary or in motion. + */ + void unregisterStationaryListener(StationaryListener listener); +} 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/framework/java/com/android/server/usage/AppStandbyInternal.java b/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java new file mode 100644 index 000000000000..041825c235d0 --- /dev/null +++ b/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java @@ -0,0 +1,130 @@ +package com.android.server.usage; + +import android.annotation.UserIdInt; +import android.app.usage.AppStandbyInfo; +import android.app.usage.UsageEvents; +import android.app.usage.UsageStatsManager.StandbyBuckets; +import android.content.Context; +import android.os.Looper; + +import com.android.internal.util.IndentingPrintWriter; + +import java.io.PrintWriter; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.List; +import java.util.Set; + +public interface AppStandbyInternal { + /** + * TODO AppStandbyController should probably be a binder service, and then we shouldn't need + * this method. + */ + static AppStandbyInternal newAppStandbyController(ClassLoader loader, Context context, + Looper looper) { + try { + final Class<?> clazz = Class.forName("com.android.server.usage.AppStandbyController", + true, loader); + final Constructor<?> ctor = clazz.getConstructor(Context.class, Looper.class); + return (AppStandbyInternal) ctor.newInstance(context, looper); + } catch (NoSuchMethodException | InstantiationException + | IllegalAccessException | InvocationTargetException | ClassNotFoundException e) { + throw new RuntimeException("Unable to instantiate AppStandbyController!", e); + } + } + + /** + * Listener interface for notifications that an app's idle state changed. + */ + abstract static class AppIdleStateChangeListener { + + /** Callback to inform listeners that the idle state has changed to a new bucket. */ + public abstract void onAppIdleStateChanged(String packageName, @UserIdInt int userId, + boolean idle, int bucket, int reason); + + /** + * Optional callback to inform the listener that the app has transitioned into + * an active state due to user interaction. + */ + public void onUserInteractionStarted(String packageName, @UserIdInt int userId) { + // No-op by default + } + } + + void onBootPhase(int phase); + + void postCheckIdleStates(int userId); + + /** + * We send a different message to check idle states once, otherwise we would end up + * scheduling a series of repeating checkIdleStates each time we fired off one. + */ + void postOneTimeCheckIdleStates(); + + void reportEvent(UsageEvents.Event event, long elapsedRealtime, int userId); + + void setLastJobRunTime(String packageName, int userId, long elapsedRealtime); + + long getTimeSinceLastJobRun(String packageName, int userId); + + void onUserRemoved(int userId); + + void addListener(AppIdleStateChangeListener listener); + + void removeListener(AppIdleStateChangeListener listener); + + int getAppId(String packageName); + + /** + * @see #isAppIdleFiltered(String, int, int, long) + */ + boolean isAppIdleFiltered(String packageName, int userId, long elapsedRealtime, + boolean shouldObfuscateInstantApps); + + /** + * Checks if an app has been idle for a while and filters out apps that are excluded. + * It returns false if the current system state allows all apps to be considered active. + * Called by interface impls. + */ + boolean isAppIdleFiltered(String packageName, int appId, int userId, + long elapsedRealtime); + + int[] getIdleUidsForUser(int userId); + + void setAppIdleAsync(String packageName, boolean idle, int userId); + + @StandbyBuckets + int getAppStandbyBucket(String packageName, int userId, + long elapsedRealtime, boolean shouldObfuscateInstantApps); + + List<AppStandbyInfo> getAppStandbyBuckets(int userId); + + void setAppStandbyBucket(String packageName, int userId, @StandbyBuckets int newBucket, + int reason, long elapsedRealtime, boolean resetTimeout); + + void addActiveDeviceAdmin(String adminPkg, int userId); + + void setActiveAdminApps(Set<String> adminPkgs, int userId); + + void onAdminDataAvailable(); + + void clearCarrierPrivilegedApps(); + + void flushToDisk(int userId); + + void flushDurationsToDisk(); + + void initializeDefaultsForSystemApps(int userId); + + void postReportContentProviderUsage(String name, String packageName, int userId); + + void postReportSyncScheduled(String packageName, int userId, boolean exempted); + + void postReportExemptedSyncStart(String packageName, int userId); + + void dumpUser(IndentingPrintWriter idpw, int userId, String pkg); + + void dumpState(String[] args, PrintWriter pw); + + boolean isAppIdleEnabled(); +} 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..dfe7a90ba246 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java @@ -0,0 +1,4586 @@ +/* + * 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.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.Collections; +import java.util.List; +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 DeviceIdleInternal mLocalService; + 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 mQuickDozeActivatedWhileIdling; + 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; + + /** Time in the elapsed realtime timebase when this listener last received a motion event. */ + private long mLastMotionEventElapsed; + + // 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 final ArraySet<DeviceIdleInternal.StationaryListener> mStationaryListeners = + new ArraySet<>(); + + 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 mMotionTimeoutAlarmListener = () -> { + synchronized (DeviceIdleController.this) { + if (!isStationaryLocked()) { + // If the device keeps registering motion, then the alarm should be + // rescheduled, so this shouldn't go off until the device is stationary. + // This case may happen in a race condition (alarm goes off right before + // motion is detected, but handleMotionDetectedLocked is called before + // we enter this block). + Slog.w(TAG, "motion timeout went off and device isn't stationary"); + return; + } + } + postStationaryStatusUpdated(); + }; + + 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(); + } + } + }; + + /** Post stationary status only to this listener. */ + private void postStationaryStatus(DeviceIdleInternal.StationaryListener listener) { + mHandler.obtainMessage(MSG_REPORT_STATIONARY_STATUS, listener).sendToTarget(); + } + + /** Post stationary status to all registered listeners. */ + private void postStationaryStatusUpdated() { + mHandler.sendEmptyMessage(MSG_REPORT_STATIONARY_STATUS); + } + + private boolean isStationaryLocked() { + final long now = mInjector.getElapsedRealtime(); + return mMotionListener.active + // Listening for motion for long enough and last motion was long enough ago. + && now - Math.max(mMotionListener.activatedTimeElapsed, mLastMotionEventElapsed) + >= mConstants.MOTION_INACTIVE_TIMEOUT; + } + + @VisibleForTesting + void registerStationaryListener(DeviceIdleInternal.StationaryListener listener) { + synchronized (this) { + if (!mStationaryListeners.add(listener)) { + // Listener already registered. + return; + } + postStationaryStatus(listener); + if (mMotionListener.active) { + if (!isStationaryLocked() && mStationaryListeners.size() == 1) { + // First listener to be registered and the device isn't stationary, so we + // need to register the alarm to report the device is stationary. + scheduleMotionTimeoutAlarmLocked(); + } + } else { + startMonitoringMotionLocked(); + scheduleMotionTimeoutAlarmLocked(); + } + } + } + + private void unregisterStationaryListener(DeviceIdleInternal.StationaryListener listener) { + synchronized (this) { + if (mStationaryListeners.remove(listener) && mStationaryListeners.size() == 0 + // Motion detection is started when transitioning from INACTIVE to IDLE_PENDING + // and so doesn't need to be on for ACTIVE or INACTIVE states. + // Motion detection isn't needed when idling due to Quick Doze. + && (mState == STATE_ACTIVE || mState == STATE_INACTIVE + || mQuickDozeActivated)) { + maybeStopMonitoringMotionLocked(); + } + } + } + + @VisibleForTesting + final class MotionListener extends TriggerEventListener + implements SensorEventListener { + + boolean active = false; + + /** + * Time in the elapsed realtime timebase when this listener was activated. Only valid if + * {@link #active} is true. + */ + long activatedTimeElapsed; + + public boolean isActive() { + return active; + } + + @Override + public void onTrigger(TriggerEvent event) { + synchronized (DeviceIdleController.this) { + motionLocked(); + } + } + + @Override + public void onSensorChanged(SensorEvent event) { + synchronized (DeviceIdleController.this) { + 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; + activatedTimeElapsed = mInjector.getElapsedRealtime(); + } 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; + @VisibleForTesting + static final int MSG_REPORT_STATIONARY_STATUS = 7; + 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; + @VisibleForTesting + static final int MSG_UPDATE_PRE_IDLE_TIMEOUT_FACTOR = 11; + @VisibleForTesting + 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; + case MSG_REPORT_STATIONARY_STATUS: { + final DeviceIdleInternal.StationaryListener newListener = + (DeviceIdleInternal.StationaryListener) msg.obj; + final DeviceIdleInternal.StationaryListener[] listeners; + final boolean isStationary; + synchronized (DeviceIdleController.this) { + isStationary = isStationaryLocked(); + if (newListener == null) { + // Only notify all listeners if we aren't directing to one listener. + listeners = mStationaryListeners.toArray( + new DeviceIdleInternal.StationaryListener[ + mStationaryListeners.size()]); + } else { + listeners = null; + } + } + if (listeners != null) { + for (DeviceIdleInternal.StationaryListener listener : listeners) { + listener.onDeviceStationaryChanged(isStationary); + } + } + if (newListener != null) { + newListener.onDeviceStationaryChanged(isStationary); + } + } + 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 + ")"); + } + addPowerSaveWhitelistApps(Collections.singletonList(name)); + } + + @Override + public int addPowerSaveWhitelistApps(List<String> packageNames) { + if (DEBUG) { + Slog.i(TAG, + "addPowerSaveWhitelistApps(name = " + packageNames + ")"); + } + getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, + null); + long ident = Binder.clearCallingIdentity(); + try { + return addPowerSaveWhitelistAppsInternal(packageNames); + } 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(); + } + + @Override + public void registerStationaryListener(StationaryListener listener) { + DeviceIdleController.this.registerStationaryListener(listener); + } + + @Override + public void unregisterStationaryListener(StationaryListener listener) { + DeviceIdleController.this.unregisterStationaryListener(listener); + } + } + + static class Injector { + private final Context mContext; + private ConnectivityManager mConnectivityManager; + 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); + } + + ConnectivityManager getConnectivityManager() { + if (mConnectivityManager == null) { + mConnectivityManager = mContext.getSystemService(ConnectivityManager.class); + } + return mConnectivityManager; + } + + 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); + mLocalService = new LocalService(); + publishLocalService(DeviceIdleInternal.class, mLocalService); + } + + @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 = LocationRequest.create() + .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++; + } + } + } + + private int addPowerSaveWhitelistAppsInternal(List<String> pkgNames) { + int numAdded = 0; + int numErrors = 0; + synchronized (this) { + for (int i = pkgNames.size() - 1; i >= 0; --i) { + final String name = pkgNames.get(i); + if (name == null) { + numErrors++; + continue; + } + try { + ApplicationInfo ai = getContext().getPackageManager().getApplicationInfo(name, + PackageManager.MATCH_ANY_USER); + if (mPowerSaveWhitelistUserApps.put(name, UserHandle.getAppId(ai.uid)) + == null) { + numAdded++; + } + } catch (PackageManager.NameNotFoundException e) { + Slog.e(TAG, "Tried to add unknown package to power save whitelist: " + name); + numErrors++; + } + } + if (numAdded > 0) { + reportPowerSaveWhitelistChangedLocked(); + updateWhitelistAppIdsLocked(); + writeConfigFileLocked(); + } + } + return pkgNames.size() - numErrors; + } + + 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) { + ConnectivityManager cm; + synchronized (this) { + cm = mInjector.getConnectivityManager(); + } + 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; + mQuickDozeActivatedWhileIdling = + mQuickDozeActivated && (mState == STATE_IDLE || mState == STATE_IDLE_MAINTENANCE); + 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; + mQuickDozeActivatedWhileIdling = false; + cancelAlarmLocked(); + cancelSensingTimeoutAlarmLocked(); + cancelLocatingLocked(); + maybeStopMonitoringMotionLocked(); + 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); + } + + private 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); + } + } + } + + private 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; + maybeDoImmediateMaintenance(); + } + } + + @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()"); + mLastMotionEventElapsed = mInjector.getElapsedRealtime(); + handleMotionDetectedLocked(mConstants.MOTION_INACTIVE_TIMEOUT, "motion"); + } + + void handleMotionDetectedLocked(long timeout, String type) { + if (mStationaryListeners.size() > 0) { + postStationaryStatusUpdated(); + scheduleMotionTimeoutAlarmLocked(); + } + if (mQuickDozeActivated && !mQuickDozeActivatedWhileIdling) { + // Don't exit idle due to motion if quick doze is enabled. + // However, if the device started idling due to the normal progression (going through + // all the states) and then had quick doze activated, come out briefly on motion so the + // user can get slightly fresher content. + return; + } + maybeStopMonitoringMotionLocked(); + // 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(); + } + } + + /** + * Stops motion monitoring. Will not stop monitoring if there are registered stationary + * listeners. + */ + private void maybeStopMonitoringMotionLocked() { + if (DEBUG) Slog.d(TAG, "maybeStopMonitoringMotionLocked()"); + if (mMotionSensor != null && mMotionListener.active && mStationaryListeners.size() == 0) { + mMotionListener.unregisterLocked(); + cancelMotionTimeoutAlarmLocked(); + } + } + + 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; + } + } + + private void cancelMotionTimeoutAlarmLocked() { + mAlarmManager.cancel(mMotionTimeoutAlarmListener); + } + + 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); + } + + private void scheduleMotionTimeoutAlarmLocked() { + if (DEBUG) Slog.d(TAG, "scheduleMotionAlarmLocked"); + long nextMotionTimeoutAlarmTime = + mInjector.getElapsedRealtime() + mConstants.MOTION_INACTIVE_TIMEOUT; + mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextMotionTimeoutAlarmTime, + "DeviceIdleController.motion", mMotionTimeoutAlarmListener, 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); + 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 (addPowerSaveWhitelistAppsInternal(Collections.singletonList(pkg)) + == 1) { + 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 || mStationaryListeners.size() > 0) { + pw.print(" mMotionActive="); pw.println(mMotionListener.active); + pw.print(" mNotMoving="); pw.println(mNotMoving); + pw.print(" mMotionListener.activatedTimeElapsed="); + pw.println(mMotionListener.activatedTimeElapsed); + pw.print(" mLastMotionEventElapsed="); pw.println(mLastMotionEventElapsed); + pw.print(" "); pw.print(mStationaryListeners.size()); + pw.println(" stationary listeners registered"); + } + 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..0a4e020e07cd --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -0,0 +1,3353 @@ +/* + * 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.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.Looper; +import android.os.Message; +import android.os.ParcelFileDescriptor; +import android.os.Process; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; +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.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 com.android.server.job.restrictions.JobRestriction; +import com.android.server.job.restrictions.ThermalStatusRestriction; +import com.android.server.usage.AppStandbyInternal; +import com.android.server.usage.AppStandbyInternal.AppIdleStateChangeListener; + +import libcore.util.EmptyArray; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZoneOffset; +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(); + + private abstract static class MySimpleClock extends Clock { + private final ZoneId mZoneId; + + MySimpleClock(ZoneId zoneId) { + this.mZoneId = zoneId; + } + + @Override + public ZoneId getZone() { + return mZoneId; + } + + @Override + public Clock withZone(ZoneId zone) { + return new MySimpleClock(zone) { + @Override + public long millis() { + return MySimpleClock.this.millis(); + } + }; + } + + @Override + public abstract long millis(); + + @Override + public Instant instant() { + return Instant.ofEpochMilli(millis()); + } + } + + @VisibleForTesting + public static Clock sUptimeMillisClock = new MySimpleClock(ZoneOffset.UTC) { + @Override + public long millis() { + return SystemClock.uptimeMillis(); + } + }; + + @VisibleForTesting + public static Clock sElapsedRealtimeClock = new MySimpleClock(ZoneOffset.UTC) { + @Override + public long millis() { + return SystemClock.elapsedRealtime(); + } + }; + + /** 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; + /** + * List of restrictions. + * Note: do not add to or remove from this list at runtime except in the constructor, because we + * do not synchronize access to this list. + */ + private final List<JobRestriction> mJobRestrictions; + + /** + * 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; + + /** + * 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); + } + } + } + } + + 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, + int capability) { + 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 (toCancel != null) { + // Implicitly replaces the existing job record with the new instance + cancelJobImplLocked(toCancel, jobStatus, "job rescheduled by app"); + } else { + startTrackingJobLocked(jobStatus, null); + } + + if (work != null) { + // If work has been supplied, enqueue it into the new job. + jobStatus.enqueueWorkLocked(work); + } + + 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); + + AppStandbyInternal appStandby = LocalServices.getService(AppStandbyInternal.class); + appStandby.addListener(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); + + // Create restrictions + mJobRestrictions = new ArrayList<>(); + mJobRestrictions.add(new ThermalStatusRestriction(this)); + + // 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(); + + for (int i = mJobRestrictions.size() - 1; i >= 0; i--) { + mJobRestrictions.get(i).onSystemServicesReady(); + } + } 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. + } + } + } + + /** + * Check if a job is restricted by any of the declared {@link JobRestriction}s. + * Note, that the jobs with {@link JobInfo#PRIORITY_FOREGROUND_APP} priority or higher may not + * be restricted, thus we won't even perform the check, but simply return null early. + * + * @param job to be checked + * @return the first {@link JobRestriction} restricting the given job that has been found; null + * - if passes all the restrictions or has priority {@link JobInfo#PRIORITY_FOREGROUND_APP} + * or higher. + */ + private JobRestriction checkIfRestricted(JobStatus job) { + if (evaluateJobPriorityLocked(job) >= JobInfo.PRIORITY_FOREGROUND_APP) { + // Jobs with PRIORITY_FOREGROUND_APP or higher should not be restricted + return null; + } + for (int i = mJobRestrictions.size() - 1; i >= 0; i--) { + final JobRestriction restriction = mJobRestrictions.get(i); + if (restriction.isJobRestricted(job)) { + return restriction; + } + } + return null; + } + + 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 { + final JobRestriction restriction = checkIfRestricted(running); + if (restriction != null) { + final int reason = restriction.getReason(); + serviceContext.cancelExecutingJobLocked(reason, + "restricted due to " + JobParameters.getReasonName(reason)); + } + } + } + } + + /** + * 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 (checkIfRestricted(job) != null) { + 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 new RuntimeException(e); + } + + 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 (checkIfRestricted(job) != null) { + 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 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 + protected int handleShellCommand(@NonNull ParcelFileDescriptor in, + @NonNull ParcelFileDescriptor out, @NonNull ParcelFileDescriptor err, + @NonNull String[] args) { + return (new JobSchedulerShellCommand(JobSchedulerService.this)).exec( + this, in.getFileDescriptor(), out.getFileDescriptor(), err.getFileDescriptor(), + args); + } + + /** + * <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(); + + for (int i = mJobRestrictions.size() - 1; i >= 0; i--) { + pw.print(" "); + mJobRestrictions.get(i).dumpConstants(pw); + 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(" Restricted due to:"); + final boolean isRestricted = checkIfRestricted(job) != null; + if (isRestricted) { + for (int i = mJobRestrictions.size() - 1; i >= 0; i--) { + final JobRestriction restriction = mJobRestrictions.get(i); + if (restriction.isJobRestricted(job)) { + final int reason = restriction.getReason(); + pw.write(" " + JobParameters.getReasonName(reason) + "[" + reason + "]"); + } + } + } else { + pw.print(" none"); + } + pw.println("."); + + pw.print(" Ready: "); + pw.print(isReadyToBeExecutedLocked(job)); + pw.print(" (job="); + pw.print(job.isReady()); + pw.print(" user="); + pw.print(areUsersStartedLocked(job)); + pw.print(" !restricted="); + pw.print(!isRestricted); + 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); + + for (int i = mJobRestrictions.size() - 1; i >= 0; i--) { + mJobRestrictions.get(i).dumpConstants(proto); + } + + 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_RESTRICTED, + checkIfRestricted(job) != null); + 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)); + + for (JobRestriction restriction : mJobRestrictions) { + final long restrictionsToken = proto.start( + JobSchedulerServiceDumpProto.RegisteredJob.RESTRICTIONS); + proto.write(JobSchedulerServiceDumpProto.JobRestriction.REASON, + restriction.getReason()); + proto.write(JobSchedulerServiceDumpProto.JobRestriction.IS_RESTRICTING, + restriction.isJobRestricted(job)); + proto.end(restrictionsToken); + } + + 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..a5c6c0132fc8 --- /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.BasicShellCommandHandler; +import android.os.Binder; +import android.os.UserHandle; + +import java.io.PrintWriter; + +public final class JobSchedulerShellCommand extends BasicShellCommandHandler { + 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..26db4a30ebda --- /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, + UserHandle.of(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..2f5f555817ec --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java @@ -0,0 +1,1272 @@ +/* + * 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); + jobs.removeIf(predicate); + if (jobs.size() == 0) { + mJobs.removeAt(jobSetIndex); + } + } + for (int jobSetIndex = mJobsPerSourceUid.size() - 1; jobSetIndex >= 0; jobSetIndex--) { + final ArraySet<JobStatus> jobs = mJobsPerSourceUid.valueAt(jobSetIndex); + jobs.removeIf(predicate); + 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..7d3630338fc1 --- /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.ArrayMap; +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 ArrayMap<Network, NetworkCapabilities> networkToCapabilities = new ArrayMap<>(); + + 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, + ArrayMap<Network, NetworkCapabilities> networkToCapabilities) { + if (jobs == null || jobs.size() == 0) { + return false; + } + + final Network network = mConnManager.getActiveNetworkForUid(jobs.valueAt(0).getSourceUid()); + NetworkCapabilities capabilities = networkToCapabilities.get(network); + if (capabilities == null) { + capabilities = mConnManager.getNetworkCapabilities(network); + networkToCapabilities.put(network, 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..c76346ffd996 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java @@ -0,0 +1,1867 @@ +/* + * 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.DateFormat; +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); + b.append(job.getClipData()); + 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(formatTime(mLastSuccessfulRunTime)); + } + if (mLastFailedRunTime != 0) { + pw.print(prefix); pw.print("Last failed run: "); + pw.println(formatTime(mLastFailedRunTime)); + } + } + + private static CharSequence formatTime(long time) { + return DateFormat.format("yyyy-MM-dd HH:mm:ss", time); + } + + 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..3fdc5711c0d3 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java @@ -0,0 +1,2688 @@ +/* + * 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.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.ArraySet; +import android.util.KeyValueListParser; +import android.util.Log; +import android.util.Slog; +import android.util.SparseArrayMap; +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 com.android.server.usage.AppStandbyInternal; +import com.android.server.usage.AppStandbyInternal.AppIdleStateChangeListener; + +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*"; + + /** + * 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 SparseArrayMap<ArraySet<JobStatus>> mTrackedJobs = new SparseArrayMap<>(); + + /** Timer for each package-userId combo. */ + private final SparseArrayMap<Timer> mPkgTimers = new SparseArrayMap<>(); + + /** List of all timing sessions for a package-userId combo, in chronological order. */ + private final SparseArrayMap<List<TimingSession>> mTimingSessions = new SparseArrayMap<>(); + + /** + * List of alarm listeners for each package that listen for when each package comes back within + * quota. + */ + private final SparseArrayMap<QcAlarmListener> mInQuotaAlarmListeners = new SparseArrayMap<>(); + + /** Cached calculation results for each app, with the standby buckets as the array indices. */ + private final SparseArrayMap<ExecutionStats[]> mExecutionStatsCache = new SparseArrayMap<>(); + + /** 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; + + /** 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, int capability) { + 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 + AppStandbyInternal appStandby = LocalServices.getService(AppStandbyInternal.class); + appStandby.addListener(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() + || 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. + if (mChargeTracker.isCharging()) { + 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.numMaps(); ++u) { + final int userId = mTrackedJobs.keyAt(u); + for (int p = 0; p < mTrackedJobs.numElementsForKey(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 SparseArrayMap<Integer> mToScheduleStartAlarms = new SparseArrayMap<>(); + 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.numMaps(); ++u) { + final int userId = mToScheduleStartAlarms.keyAt(u); + for (int p = 0; p < mToScheduleStartAlarms.numElementsForKey(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(); + } + } + }); + } + } + + 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 = + 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 = + 75; // 75/window = 450/hr = 1/session + 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 = + 75; // 450/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); + onChange(true, null); + } + + @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("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.numMaps(); ++u) { + final int userId = mPkgTimers.keyAt(u); + for (int p = 0; p < mPkgTimers.numElementsForKey(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.numMaps(); ++u) { + final int userId = mExecutionStatsCache.keyAt(u); + for (int p = 0; p < mExecutionStatsCache.numElementsForKey(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.numMaps(); ++u) { + final int userId = mInQuotaAlarmListeners.keyAt(u); + for (int p = 0; p < mInQuotaAlarmListeners.numElementsForKey(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.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.numMaps(); ++u) { + final int userId = mPkgTimers.keyAt(u); + for (int p = 0; p < mPkgTimers.numElementsForKey(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/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java b/apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java new file mode 100644 index 000000000000..e180c55e1bf2 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java @@ -0,0 +1,72 @@ +/* + * 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.job.restrictions; + +import android.app.job.JobInfo; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.util.IndentingPrintWriter; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.controllers.JobStatus; + +/** + * Used by {@link JobSchedulerService} to impose additional restrictions regarding whether jobs + * should be scheduled or not based on the state of the system/device. + * Every restriction is associated with exactly one reason (from {@link + * android.app.job.JobParameters#JOB_STOP_REASON_CODES}), which could be retrieved using {@link + * #getReason()}. + * Note, that this is not taken into account for the jobs that have priority + * {@link JobInfo#PRIORITY_FOREGROUND_APP} or higher. + */ +public abstract class JobRestriction { + + final JobSchedulerService mService; + private final int mReason; + + JobRestriction(JobSchedulerService service, int reason) { + mService = service; + mReason = reason; + } + + /** + * Called when the system boot phase has reached + * {@link com.android.server.SystemService#PHASE_SYSTEM_SERVICES_READY}. + */ + public void onSystemServicesReady() { + } + + /** + * Called by {@link JobSchedulerService} to check if it may proceed with scheduling the job (in + * case all constraints are satisfied and all other {@link JobRestriction}s are fine with it) + * + * @param job to be checked + * @return false if the {@link JobSchedulerService} should not schedule this job at the moment, + * true - otherwise + */ + public abstract boolean isJobRestricted(JobStatus job); + + /** Dump any internal constants the Restriction may have. */ + public abstract void dumpConstants(IndentingPrintWriter pw); + + /** Dump any internal constants the Restriction may have. */ + public abstract void dumpConstants(ProtoOutputStream proto); + + /** @return reason code for the Restriction. */ + public final int getReason() { + return mReason; + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java b/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java new file mode 100644 index 000000000000..aa7696df6dbd --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java @@ -0,0 +1,74 @@ +/* + * 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.job.restrictions; + +import android.app.job.JobParameters; +import android.os.PowerManager; +import android.os.PowerManager.OnThermalStatusChangedListener; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.util.IndentingPrintWriter; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.JobSchedulerServiceDumpProto; +import com.android.server.job.controllers.JobStatus; + +public class ThermalStatusRestriction extends JobRestriction { + private static final String TAG = "ThermalStatusRestriction"; + + private volatile boolean mIsThermalRestricted = false; + + private PowerManager mPowerManager; + + public ThermalStatusRestriction(JobSchedulerService service) { + super(service, JobParameters.REASON_DEVICE_THERMAL); + } + + @Override + public void onSystemServicesReady() { + mPowerManager = mService.getContext().getSystemService(PowerManager.class); + // Use MainExecutor + mPowerManager.addThermalStatusListener(new OnThermalStatusChangedListener() { + @Override + public void onThermalStatusChanged(int status) { + // This is called on the main thread. Do not do any slow operations in it. + // mService.onControllerStateChanged() will just post a message, which is okay. + final boolean shouldBeActive = status >= PowerManager.THERMAL_STATUS_SEVERE; + if (mIsThermalRestricted == shouldBeActive) { + return; + } + mIsThermalRestricted = shouldBeActive; + mService.onControllerStateChanged(); + } + }); + } + + @Override + public boolean isJobRestricted(JobStatus job) { + return mIsThermalRestricted && job.hasConnectivityConstraint(); + } + + @Override + public void dumpConstants(IndentingPrintWriter pw) { + pw.print("In thermal throttling?: "); + pw.print(mIsThermalRestricted); + } + + @Override + public void dumpConstants(ProtoOutputStream proto) { + proto.write(JobSchedulerServiceDumpProto.IN_THERMAL, mIsThermalRestricted); + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java b/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java new file mode 100644 index 000000000000..82292cfeea09 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java @@ -0,0 +1,699 @@ +/** + * 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.usage; + +import static android.app.usage.UsageStatsManager.REASON_MAIN_DEFAULT; +import static android.app.usage.UsageStatsManager.REASON_MAIN_FORCED; +import static android.app.usage.UsageStatsManager.REASON_MAIN_MASK; +import static android.app.usage.UsageStatsManager.REASON_MAIN_PREDICTED; +import static android.app.usage.UsageStatsManager.REASON_MAIN_USAGE; +import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_USER_INTERACTION; +import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_ACTIVE; +import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_NEVER; +import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RARE; +import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_WORKING_SET; + +import android.app.usage.AppStandbyInfo; +import android.app.usage.UsageStatsManager; +import android.os.SystemClock; +import android.util.ArrayMap; +import android.util.AtomicFile; +import android.util.Slog; +import android.util.SparseArray; +import android.util.TimeUtils; +import android.util.Xml; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.FastXmlSerializer; +import com.android.internal.util.IndentingPrintWriter; + +import libcore.io.IoUtils; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; + +/** + * Keeps track of recent active state changes in apps. + * Access should be guarded by a lock by the caller. + */ +public class AppIdleHistory { + + private static final String TAG = "AppIdleHistory"; + + private static final boolean DEBUG = AppStandbyController.DEBUG; + + // History for all users and all packages + private SparseArray<ArrayMap<String,AppUsageHistory>> mIdleHistory = new SparseArray<>(); + private static final long ONE_MINUTE = 60 * 1000; + + private static final int STANDBY_BUCKET_UNKNOWN = -1; + + @VisibleForTesting + static final String APP_IDLE_FILENAME = "app_idle_stats.xml"; + private static final String TAG_PACKAGES = "packages"; + private static final String TAG_PACKAGE = "package"; + private static final String ATTR_NAME = "name"; + // Screen on timebase time when app was last used + private static final String ATTR_SCREEN_IDLE = "screenIdleTime"; + // Elapsed timebase time when app was last used + private static final String ATTR_ELAPSED_IDLE = "elapsedIdleTime"; + // Elapsed timebase time when the app bucket was last predicted externally + private static final String ATTR_LAST_PREDICTED_TIME = "lastPredictedTime"; + // The standby bucket for the app + private static final String ATTR_CURRENT_BUCKET = "appLimitBucket"; + // The reason the app was put in the above bucket + private static final String ATTR_BUCKETING_REASON = "bucketReason"; + // The last time a job was run for this app + private static final String ATTR_LAST_RUN_JOB_TIME = "lastJobRunTime"; + // The time when the forced active state can be overridden. + private static final String ATTR_BUCKET_ACTIVE_TIMEOUT_TIME = "activeTimeoutTime"; + // The time when the forced working_set state can be overridden. + private static final String ATTR_BUCKET_WORKING_SET_TIMEOUT_TIME = "workingSetTimeoutTime"; + + // device on time = mElapsedDuration + (timeNow - mElapsedSnapshot) + private long mElapsedSnapshot; // Elapsed time snapshot when last write of mDeviceOnDuration + private long mElapsedDuration; // Total device on duration since device was "born" + + // screen on time = mScreenOnDuration + (timeNow - mScreenOnSnapshot) + private long mScreenOnSnapshot; // Elapsed time snapshot when last write of mScreenOnDuration + private long mScreenOnDuration; // Total screen on duration since device was "born" + + private final File mStorageDir; + + private boolean mScreenOn; + + static class AppUsageHistory { + // Last used time using elapsed timebase + long lastUsedElapsedTime; + // Last used time using screen_on timebase + long lastUsedScreenTime; + // Last predicted time using elapsed timebase + long lastPredictedTime; + // Last predicted bucket + @UsageStatsManager.StandbyBuckets + int lastPredictedBucket = STANDBY_BUCKET_UNKNOWN; + // Standby bucket + @UsageStatsManager.StandbyBuckets + int currentBucket; + // Reason for setting the standby bucket. The value here is a combination of + // one of UsageStatsManager.REASON_MAIN_* and one (or none) of + // UsageStatsManager.REASON_SUB_*. Also see REASON_MAIN_MASK and REASON_SUB_MASK. + int bucketingReason; + // In-memory only, last bucket for which the listeners were informed + int lastInformedBucket; + // The last time a job was run for this app, using elapsed timebase + long lastJobRunTime; + // When should the bucket active state timeout, in elapsed timebase, if greater than + // lastUsedElapsedTime. + // This is used to keep the app in a high bucket regardless of other timeouts and + // predictions. + long bucketActiveTimeoutTime; + // If there's a forced working_set state, this is when it times out. This can be sitting + // under any active state timeout, so that it becomes applicable after the active state + // timeout expires. + long bucketWorkingSetTimeoutTime; + } + + AppIdleHistory(File storageDir, long elapsedRealtime) { + mElapsedSnapshot = elapsedRealtime; + mScreenOnSnapshot = elapsedRealtime; + mStorageDir = storageDir; + readScreenOnTime(); + } + + public void updateDisplay(boolean screenOn, long elapsedRealtime) { + if (screenOn == mScreenOn) return; + + mScreenOn = screenOn; + if (mScreenOn) { + mScreenOnSnapshot = elapsedRealtime; + } else { + mScreenOnDuration += elapsedRealtime - mScreenOnSnapshot; + mElapsedDuration += elapsedRealtime - mElapsedSnapshot; + mElapsedSnapshot = elapsedRealtime; + } + if (DEBUG) Slog.d(TAG, "mScreenOnSnapshot=" + mScreenOnSnapshot + + ", mScreenOnDuration=" + mScreenOnDuration + + ", mScreenOn=" + mScreenOn); + } + + public long getScreenOnTime(long elapsedRealtime) { + long screenOnTime = mScreenOnDuration; + if (mScreenOn) { + screenOnTime += elapsedRealtime - mScreenOnSnapshot; + } + return screenOnTime; + } + + @VisibleForTesting + File getScreenOnTimeFile() { + return new File(mStorageDir, "screen_on_time"); + } + + private void readScreenOnTime() { + File screenOnTimeFile = getScreenOnTimeFile(); + if (screenOnTimeFile.exists()) { + try { + BufferedReader reader = new BufferedReader(new FileReader(screenOnTimeFile)); + mScreenOnDuration = Long.parseLong(reader.readLine()); + mElapsedDuration = Long.parseLong(reader.readLine()); + reader.close(); + } catch (IOException | NumberFormatException e) { + } + } else { + writeScreenOnTime(); + } + } + + private void writeScreenOnTime() { + AtomicFile screenOnTimeFile = new AtomicFile(getScreenOnTimeFile()); + FileOutputStream fos = null; + try { + fos = screenOnTimeFile.startWrite(); + fos.write((Long.toString(mScreenOnDuration) + "\n" + + Long.toString(mElapsedDuration) + "\n").getBytes()); + screenOnTimeFile.finishWrite(fos); + } catch (IOException ioe) { + screenOnTimeFile.failWrite(fos); + } + } + + /** + * To be called periodically to keep track of elapsed time when app idle times are written + */ + public void writeAppIdleDurations() { + final long elapsedRealtime = SystemClock.elapsedRealtime(); + // Only bump up and snapshot the elapsed time. Don't change screen on duration. + mElapsedDuration += elapsedRealtime - mElapsedSnapshot; + mElapsedSnapshot = elapsedRealtime; + writeScreenOnTime(); + } + + /** + * Mark the app as used and update the bucket if necessary. If there is a timeout specified + * that's in the future, then the usage event is temporary and keeps the app in the specified + * bucket at least until the timeout is reached. This can be used to keep the app in an + * elevated bucket for a while until some important task gets to run. + * @param appUsageHistory the usage record for the app being updated + * @param packageName name of the app being updated, for logging purposes + * @param newBucket the bucket to set the app to + * @param usageReason the sub-reason for usage, one of REASON_SUB_USAGE_* + * @param elapsedRealtime mark as used time if non-zero + * @param timeout set the timeout of the specified bucket, if non-zero. Can only be used + * with bucket values of ACTIVE and WORKING_SET. + * @return + */ + public AppUsageHistory reportUsage(AppUsageHistory appUsageHistory, String packageName, + int newBucket, int usageReason, long elapsedRealtime, long timeout) { + // Set the timeout if applicable + if (timeout > elapsedRealtime) { + // Convert to elapsed timebase + final long timeoutTime = mElapsedDuration + (timeout - mElapsedSnapshot); + if (newBucket == STANDBY_BUCKET_ACTIVE) { + appUsageHistory.bucketActiveTimeoutTime = Math.max(timeoutTime, + appUsageHistory.bucketActiveTimeoutTime); + } else if (newBucket == STANDBY_BUCKET_WORKING_SET) { + appUsageHistory.bucketWorkingSetTimeoutTime = Math.max(timeoutTime, + appUsageHistory.bucketWorkingSetTimeoutTime); + } else { + throw new IllegalArgumentException("Cannot set a timeout on bucket=" + + newBucket); + } + } + + if (elapsedRealtime != 0) { + appUsageHistory.lastUsedElapsedTime = mElapsedDuration + + (elapsedRealtime - mElapsedSnapshot); + appUsageHistory.lastUsedScreenTime = getScreenOnTime(elapsedRealtime); + } + + if (appUsageHistory.currentBucket > newBucket) { + appUsageHistory.currentBucket = newBucket; + if (DEBUG) { + Slog.d(TAG, "Moved " + packageName + " to bucket=" + appUsageHistory + .currentBucket + + ", reason=0x0" + Integer.toHexString(appUsageHistory.bucketingReason)); + } + } + appUsageHistory.bucketingReason = REASON_MAIN_USAGE | usageReason; + + return appUsageHistory; + } + + /** + * Mark the app as used and update the bucket if necessary. If there is a timeout specified + * that's in the future, then the usage event is temporary and keeps the app in the specified + * bucket at least until the timeout is reached. This can be used to keep the app in an + * elevated bucket for a while until some important task gets to run. + * @param packageName + * @param userId + * @param newBucket the bucket to set the app to + * @param usageReason sub reason for usage + * @param nowElapsed mark as used time if non-zero + * @param timeout set the timeout of the specified bucket, if non-zero. Can only be used + * with bucket values of ACTIVE and WORKING_SET. + * @return + */ + public AppUsageHistory reportUsage(String packageName, int userId, int newBucket, + int usageReason, long nowElapsed, long timeout) { + ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); + AppUsageHistory history = getPackageHistory(userHistory, packageName, nowElapsed, true); + return reportUsage(history, packageName, newBucket, usageReason, nowElapsed, timeout); + } + + private ArrayMap<String, AppUsageHistory> getUserHistory(int userId) { + ArrayMap<String, AppUsageHistory> userHistory = mIdleHistory.get(userId); + if (userHistory == null) { + userHistory = new ArrayMap<>(); + mIdleHistory.put(userId, userHistory); + readAppIdleTimes(userId, userHistory); + } + return userHistory; + } + + private AppUsageHistory getPackageHistory(ArrayMap<String, AppUsageHistory> userHistory, + String packageName, long elapsedRealtime, boolean create) { + AppUsageHistory appUsageHistory = userHistory.get(packageName); + if (appUsageHistory == null && create) { + appUsageHistory = new AppUsageHistory(); + appUsageHistory.lastUsedElapsedTime = getElapsedTime(elapsedRealtime); + appUsageHistory.lastUsedScreenTime = getScreenOnTime(elapsedRealtime); + appUsageHistory.lastPredictedTime = getElapsedTime(0); + appUsageHistory.currentBucket = STANDBY_BUCKET_NEVER; + appUsageHistory.bucketingReason = REASON_MAIN_DEFAULT; + appUsageHistory.lastInformedBucket = -1; + appUsageHistory.lastJobRunTime = Long.MIN_VALUE; // long long time ago + userHistory.put(packageName, appUsageHistory); + } + return appUsageHistory; + } + + public void onUserRemoved(int userId) { + mIdleHistory.remove(userId); + } + + public boolean isIdle(String packageName, int userId, long elapsedRealtime) { + ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); + AppUsageHistory appUsageHistory = + getPackageHistory(userHistory, packageName, elapsedRealtime, true); + return appUsageHistory.currentBucket >= STANDBY_BUCKET_RARE; + } + + public AppUsageHistory getAppUsageHistory(String packageName, int userId, + long elapsedRealtime) { + ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); + AppUsageHistory appUsageHistory = + getPackageHistory(userHistory, packageName, elapsedRealtime, true); + return appUsageHistory; + } + + public void setAppStandbyBucket(String packageName, int userId, long elapsedRealtime, + int bucket, int reason) { + setAppStandbyBucket(packageName, userId, elapsedRealtime, bucket, reason, false); + } + + public void setAppStandbyBucket(String packageName, int userId, long elapsedRealtime, + int bucket, int reason, boolean resetTimeout) { + ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); + AppUsageHistory appUsageHistory = + getPackageHistory(userHistory, packageName, elapsedRealtime, true); + appUsageHistory.currentBucket = bucket; + appUsageHistory.bucketingReason = reason; + + final long elapsed = getElapsedTime(elapsedRealtime); + + if ((reason & REASON_MAIN_MASK) == REASON_MAIN_PREDICTED) { + appUsageHistory.lastPredictedTime = elapsed; + appUsageHistory.lastPredictedBucket = bucket; + } + if (resetTimeout) { + appUsageHistory.bucketActiveTimeoutTime = elapsed; + appUsageHistory.bucketWorkingSetTimeoutTime = elapsed; + } + if (DEBUG) { + Slog.d(TAG, "Moved " + packageName + " to bucket=" + appUsageHistory.currentBucket + + ", reason=0x0" + Integer.toHexString(appUsageHistory.bucketingReason)); + } + } + + /** + * Update the prediction for the app but don't change the actual bucket + * @param app The app for which the prediction was made + * @param elapsedTimeAdjusted The elapsed time in the elapsed duration timebase + * @param bucket The predicted bucket + */ + public void updateLastPrediction(AppUsageHistory app, long elapsedTimeAdjusted, int bucket) { + app.lastPredictedTime = elapsedTimeAdjusted; + app.lastPredictedBucket = bucket; + } + + /** + * Marks the last time a job was run, with the given elapsedRealtime. The time stored is + * based on the elapsed timebase. + * @param packageName + * @param userId + * @param elapsedRealtime + */ + public void setLastJobRunTime(String packageName, int userId, long elapsedRealtime) { + ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); + AppUsageHistory appUsageHistory = + getPackageHistory(userHistory, packageName, elapsedRealtime, true); + appUsageHistory.lastJobRunTime = getElapsedTime(elapsedRealtime); + } + + /** + * Returns the time since the last job was run for this app. This can be larger than the + * current elapsedRealtime, in case it happened before boot or a really large value if no jobs + * were ever run. + * @param packageName + * @param userId + * @param elapsedRealtime + * @return + */ + public long getTimeSinceLastJobRun(String packageName, int userId, long elapsedRealtime) { + ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); + AppUsageHistory appUsageHistory = + getPackageHistory(userHistory, packageName, elapsedRealtime, false); + // Don't adjust the default, else it'll wrap around to a positive value + if (appUsageHistory == null || appUsageHistory.lastJobRunTime == Long.MIN_VALUE) { + return Long.MAX_VALUE; + } + return getElapsedTime(elapsedRealtime) - appUsageHistory.lastJobRunTime; + } + + public int getAppStandbyBucket(String packageName, int userId, long elapsedRealtime) { + ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); + AppUsageHistory appUsageHistory = + getPackageHistory(userHistory, packageName, elapsedRealtime, false); + return appUsageHistory == null ? STANDBY_BUCKET_NEVER : appUsageHistory.currentBucket; + } + + public ArrayList<AppStandbyInfo> getAppStandbyBuckets(int userId, boolean appIdleEnabled) { + ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); + int size = userHistory.size(); + ArrayList<AppStandbyInfo> buckets = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + buckets.add(new AppStandbyInfo(userHistory.keyAt(i), + appIdleEnabled ? userHistory.valueAt(i).currentBucket : STANDBY_BUCKET_ACTIVE)); + } + return buckets; + } + + public int getAppStandbyReason(String packageName, int userId, long elapsedRealtime) { + ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); + AppUsageHistory appUsageHistory = + getPackageHistory(userHistory, packageName, elapsedRealtime, false); + return appUsageHistory != null ? appUsageHistory.bucketingReason : 0; + } + + public long getElapsedTime(long elapsedRealtime) { + return (elapsedRealtime - mElapsedSnapshot + mElapsedDuration); + } + + /* Returns the new standby bucket the app is assigned to */ + public int setIdle(String packageName, int userId, boolean idle, long elapsedRealtime) { + ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); + AppUsageHistory appUsageHistory = getPackageHistory(userHistory, packageName, + elapsedRealtime, true); + if (idle) { + appUsageHistory.currentBucket = STANDBY_BUCKET_RARE; + appUsageHistory.bucketingReason = REASON_MAIN_FORCED; + } else { + appUsageHistory.currentBucket = STANDBY_BUCKET_ACTIVE; + // This is to pretend that the app was just used, don't freeze the state anymore. + appUsageHistory.bucketingReason = REASON_MAIN_USAGE | REASON_SUB_USAGE_USER_INTERACTION; + } + return appUsageHistory.currentBucket; + } + + public void clearUsage(String packageName, int userId) { + ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); + userHistory.remove(packageName); + } + + boolean shouldInformListeners(String packageName, int userId, + long elapsedRealtime, int bucket) { + ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); + AppUsageHistory appUsageHistory = getPackageHistory(userHistory, packageName, + elapsedRealtime, true); + if (appUsageHistory.lastInformedBucket != bucket) { + appUsageHistory.lastInformedBucket = bucket; + return true; + } + return false; + } + + /** + * Returns the index in the arrays of screenTimeThresholds and elapsedTimeThresholds + * that corresponds to how long since the app was used. + * @param packageName + * @param userId + * @param elapsedRealtime current time + * @param screenTimeThresholds Array of screen times, in ascending order, first one is 0 + * @param elapsedTimeThresholds Array of elapsed time, in ascending order, first one is 0 + * @return The index whose values the app's used time exceeds (in both arrays) + */ + int getThresholdIndex(String packageName, int userId, long elapsedRealtime, + long[] screenTimeThresholds, long[] elapsedTimeThresholds) { + ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); + AppUsageHistory appUsageHistory = getPackageHistory(userHistory, packageName, + elapsedRealtime, false); + // If we don't have any state for the app, assume never used + if (appUsageHistory == null) return screenTimeThresholds.length - 1; + + long screenOnDelta = getScreenOnTime(elapsedRealtime) - appUsageHistory.lastUsedScreenTime; + long elapsedDelta = getElapsedTime(elapsedRealtime) - appUsageHistory.lastUsedElapsedTime; + + if (DEBUG) Slog.d(TAG, packageName + + " lastUsedScreen=" + appUsageHistory.lastUsedScreenTime + + " lastUsedElapsed=" + appUsageHistory.lastUsedElapsedTime); + if (DEBUG) Slog.d(TAG, packageName + " screenOn=" + screenOnDelta + + ", elapsed=" + elapsedDelta); + for (int i = screenTimeThresholds.length - 1; i >= 0; i--) { + if (screenOnDelta >= screenTimeThresholds[i] + && elapsedDelta >= elapsedTimeThresholds[i]) { + return i; + } + } + return 0; + } + + @VisibleForTesting + File getUserFile(int userId) { + return new File(new File(new File(mStorageDir, "users"), + Integer.toString(userId)), APP_IDLE_FILENAME); + } + + + /** + * Check if App Idle File exists on disk + * @param userId + * @return true if file exists + */ + public boolean userFileExists(int userId) { + return getUserFile(userId).exists(); + } + + private void readAppIdleTimes(int userId, ArrayMap<String, AppUsageHistory> userHistory) { + FileInputStream fis = null; + try { + AtomicFile appIdleFile = new AtomicFile(getUserFile(userId)); + fis = appIdleFile.openRead(); + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(fis, StandardCharsets.UTF_8.name()); + + int type; + while ((type = parser.next()) != XmlPullParser.START_TAG + && type != XmlPullParser.END_DOCUMENT) { + // Skip + } + + if (type != XmlPullParser.START_TAG) { + Slog.e(TAG, "Unable to read app idle file for user " + userId); + return; + } + if (!parser.getName().equals(TAG_PACKAGES)) { + return; + } + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { + if (type == XmlPullParser.START_TAG) { + final String name = parser.getName(); + if (name.equals(TAG_PACKAGE)) { + final String packageName = parser.getAttributeValue(null, ATTR_NAME); + AppUsageHistory appUsageHistory = new AppUsageHistory(); + appUsageHistory.lastUsedElapsedTime = + Long.parseLong(parser.getAttributeValue(null, ATTR_ELAPSED_IDLE)); + appUsageHistory.lastUsedScreenTime = + Long.parseLong(parser.getAttributeValue(null, ATTR_SCREEN_IDLE)); + appUsageHistory.lastPredictedTime = getLongValue(parser, + ATTR_LAST_PREDICTED_TIME, 0L); + String currentBucketString = parser.getAttributeValue(null, + ATTR_CURRENT_BUCKET); + appUsageHistory.currentBucket = currentBucketString == null + ? STANDBY_BUCKET_ACTIVE + : Integer.parseInt(currentBucketString); + String bucketingReason = + parser.getAttributeValue(null, ATTR_BUCKETING_REASON); + appUsageHistory.lastJobRunTime = getLongValue(parser, + ATTR_LAST_RUN_JOB_TIME, Long.MIN_VALUE); + appUsageHistory.bucketActiveTimeoutTime = getLongValue(parser, + ATTR_BUCKET_ACTIVE_TIMEOUT_TIME, 0L); + appUsageHistory.bucketWorkingSetTimeoutTime = getLongValue(parser, + ATTR_BUCKET_WORKING_SET_TIMEOUT_TIME, 0L); + appUsageHistory.bucketingReason = REASON_MAIN_DEFAULT; + if (bucketingReason != null) { + try { + appUsageHistory.bucketingReason = + Integer.parseInt(bucketingReason, 16); + } catch (NumberFormatException nfe) { + } + } + appUsageHistory.lastInformedBucket = -1; + userHistory.put(packageName, appUsageHistory); + } + } + } + } catch (IOException | XmlPullParserException e) { + Slog.e(TAG, "Unable to read app idle file for user " + userId, e); + } finally { + IoUtils.closeQuietly(fis); + } + } + + private long getLongValue(XmlPullParser parser, String attrName, long defValue) { + String value = parser.getAttributeValue(null, attrName); + if (value == null) return defValue; + return Long.parseLong(value); + } + + public void writeAppIdleTimes(int userId) { + FileOutputStream fos = null; + AtomicFile appIdleFile = new AtomicFile(getUserFile(userId)); + try { + fos = appIdleFile.startWrite(); + final BufferedOutputStream bos = new BufferedOutputStream(fos); + + FastXmlSerializer xml = new FastXmlSerializer(); + xml.setOutput(bos, StandardCharsets.UTF_8.name()); + xml.startDocument(null, true); + xml.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); + + xml.startTag(null, TAG_PACKAGES); + + ArrayMap<String,AppUsageHistory> userHistory = getUserHistory(userId); + final int N = userHistory.size(); + for (int i = 0; i < N; i++) { + String packageName = userHistory.keyAt(i); + // Skip any unexpected null package names + if (packageName == null) { + Slog.w(TAG, "Skipping App Idle write for unexpected null package"); + continue; + } + AppUsageHistory history = userHistory.valueAt(i); + xml.startTag(null, TAG_PACKAGE); + xml.attribute(null, ATTR_NAME, packageName); + xml.attribute(null, ATTR_ELAPSED_IDLE, + Long.toString(history.lastUsedElapsedTime)); + xml.attribute(null, ATTR_SCREEN_IDLE, + Long.toString(history.lastUsedScreenTime)); + xml.attribute(null, ATTR_LAST_PREDICTED_TIME, + Long.toString(history.lastPredictedTime)); + xml.attribute(null, ATTR_CURRENT_BUCKET, + Integer.toString(history.currentBucket)); + xml.attribute(null, ATTR_BUCKETING_REASON, + Integer.toHexString(history.bucketingReason)); + if (history.bucketActiveTimeoutTime > 0) { + xml.attribute(null, ATTR_BUCKET_ACTIVE_TIMEOUT_TIME, Long.toString(history + .bucketActiveTimeoutTime)); + } + if (history.bucketWorkingSetTimeoutTime > 0) { + xml.attribute(null, ATTR_BUCKET_WORKING_SET_TIMEOUT_TIME, Long.toString(history + .bucketWorkingSetTimeoutTime)); + } + if (history.lastJobRunTime != Long.MIN_VALUE) { + xml.attribute(null, ATTR_LAST_RUN_JOB_TIME, Long.toString(history + .lastJobRunTime)); + } + xml.endTag(null, TAG_PACKAGE); + } + + xml.endTag(null, TAG_PACKAGES); + xml.endDocument(); + appIdleFile.finishWrite(fos); + } catch (Exception e) { + appIdleFile.failWrite(fos); + Slog.e(TAG, "Error writing app idle file for user " + userId, e); + } + } + + public void dump(IndentingPrintWriter idpw, int userId, String pkg) { + idpw.println("App Standby States:"); + idpw.increaseIndent(); + ArrayMap<String, AppUsageHistory> userHistory = mIdleHistory.get(userId); + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long totalElapsedTime = getElapsedTime(elapsedRealtime); + final long screenOnTime = getScreenOnTime(elapsedRealtime); + if (userHistory == null) return; + final int P = userHistory.size(); + for (int p = 0; p < P; p++) { + final String packageName = userHistory.keyAt(p); + final AppUsageHistory appUsageHistory = userHistory.valueAt(p); + if (pkg != null && !pkg.equals(packageName)) { + continue; + } + idpw.print("package=" + packageName); + idpw.print(" u=" + userId); + idpw.print(" bucket=" + appUsageHistory.currentBucket + + " reason=" + + UsageStatsManager.reasonToString(appUsageHistory.bucketingReason)); + idpw.print(" used="); + TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastUsedElapsedTime, idpw); + idpw.print(" usedScr="); + TimeUtils.formatDuration(screenOnTime - appUsageHistory.lastUsedScreenTime, idpw); + idpw.print(" lastPred="); + TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastPredictedTime, idpw); + idpw.print(" activeLeft="); + TimeUtils.formatDuration(appUsageHistory.bucketActiveTimeoutTime - totalElapsedTime, + idpw); + idpw.print(" wsLeft="); + TimeUtils.formatDuration(appUsageHistory.bucketWorkingSetTimeoutTime - totalElapsedTime, + idpw); + idpw.print(" lastJob="); + TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastJobRunTime, idpw); + idpw.print(" idle=" + (isIdle(packageName, userId, elapsedRealtime) ? "y" : "n")); + idpw.println(); + } + idpw.println(); + idpw.print("totalElapsedTime="); + TimeUtils.formatDuration(getElapsedTime(elapsedRealtime), idpw); + idpw.println(); + idpw.print("totalScreenOnTime="); + TimeUtils.formatDuration(getScreenOnTime(elapsedRealtime), idpw); + idpw.println(); + idpw.decreaseIndent(); + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java new file mode 100644 index 000000000000..bcd8be7b63e0 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java @@ -0,0 +1,1754 @@ +/** + * 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.usage; + +import static android.app.usage.UsageStatsManager.REASON_MAIN_DEFAULT; +import static android.app.usage.UsageStatsManager.REASON_MAIN_FORCED; +import static android.app.usage.UsageStatsManager.REASON_MAIN_MASK; +import static android.app.usage.UsageStatsManager.REASON_MAIN_PREDICTED; +import static android.app.usage.UsageStatsManager.REASON_MAIN_TIMEOUT; +import static android.app.usage.UsageStatsManager.REASON_MAIN_USAGE; +import static android.app.usage.UsageStatsManager.REASON_SUB_PREDICTED_RESTORED; +import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_ACTIVE_TIMEOUT; +import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_EXEMPTED_SYNC_SCHEDULED_DOZE; +import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_EXEMPTED_SYNC_SCHEDULED_NON_DOZE; +import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_EXEMPTED_SYNC_START; +import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_FOREGROUND_SERVICE_START; +import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_MOVE_TO_BACKGROUND; +import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_MOVE_TO_FOREGROUND; +import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_NOTIFICATION_SEEN; +import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_SLICE_PINNED; +import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_SLICE_PINNED_PRIV; +import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_SYNC_ADAPTER; +import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_SYSTEM_INTERACTION; +import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_SYSTEM_UPDATE; +import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_UNEXEMPTED_SYNC_SCHEDULED; +import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_USER_INTERACTION; +import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_ACTIVE; +import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_EXEMPTED; +import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_FREQUENT; +import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_NEVER; +import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RARE; +import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_WORKING_SET; + +import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY; + +import android.annotation.UserIdInt; +import android.app.ActivityManager; +import android.app.AppGlobals; +import android.app.usage.AppStandbyInfo; +import android.app.usage.UsageEvents; +import android.app.usage.UsageStatsManager.StandbyBuckets; +import android.appwidget.AppWidgetManager; +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.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManagerInternal; +import android.content.pm.ParceledListSlice; +import android.database.ContentObserver; +import android.hardware.display.DisplayManager; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkRequest; +import android.net.NetworkScoreManager; +import android.os.BatteryStats; +import android.os.Environment; +import android.os.Handler; +import android.os.IDeviceIdleController; +import android.os.Looper; +import android.os.Message; +import android.os.PowerManager; +import android.os.Process; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.os.UserHandle; +import android.provider.Settings.Global; +import android.telephony.TelephonyManager; +import android.util.ArraySet; +import android.util.KeyValueListParser; +import android.util.Slog; +import android.util.SparseArray; +import android.util.SparseIntArray; +import android.util.TimeUtils; +import android.view.Display; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.app.IBatteryStats; +import com.android.internal.os.SomeArgs; +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.ConcurrentUtils; +import com.android.internal.util.IndentingPrintWriter; +import com.android.server.LocalServices; +import com.android.server.usage.AppIdleHistory.AppUsageHistory; +import com.android.server.usage.AppStandbyInternal.AppIdleStateChangeListener; + +import java.io.File; +import java.io.PrintWriter; +import java.time.Duration; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CountDownLatch; + +/** + * Manages the standby state of an app, listening to various events. + * + * Unit test: + atest com.android.server.usage.AppStandbyControllerTests + */ +public class AppStandbyController implements AppStandbyInternal { + + private static final String TAG = "AppStandbyController"; + static final boolean DEBUG = false; + + static final boolean COMPRESS_TIME = false; + private static final long ONE_MINUTE = 60 * 1000; + private static final long ONE_HOUR = ONE_MINUTE * 60; + private static final long ONE_DAY = ONE_HOUR * 24; + + static final long[] SCREEN_TIME_THRESHOLDS = { + 0, + 0, + COMPRESS_TIME ? 120 * 1000 : 1 * ONE_HOUR, + COMPRESS_TIME ? 240 * 1000 : 2 * ONE_HOUR + }; + + static final long[] ELAPSED_TIME_THRESHOLDS = { + 0, + COMPRESS_TIME ? 1 * ONE_MINUTE : 12 * ONE_HOUR, + COMPRESS_TIME ? 4 * ONE_MINUTE : 24 * ONE_HOUR, + COMPRESS_TIME ? 16 * ONE_MINUTE : 48 * ONE_HOUR + }; + + static final int[] THRESHOLD_BUCKETS = { + STANDBY_BUCKET_ACTIVE, + STANDBY_BUCKET_WORKING_SET, + STANDBY_BUCKET_FREQUENT, + STANDBY_BUCKET_RARE + }; + + /** Default expiration time for bucket prediction. After this, use thresholds to downgrade. */ + private static final long DEFAULT_PREDICTION_TIMEOUT = 12 * ONE_HOUR; + + /** + * Indicates the maximum wait time for admin data to be available; + */ + private static final long WAIT_FOR_ADMIN_DATA_TIMEOUT_MS = 10_000; + + // To name the lock for stack traces + static class Lock {} + + /** Lock to protect the app's standby state. Required for calls into AppIdleHistory */ + private final Object mAppIdleLock = new Lock(); + + /** Keeps the history and state for each app. */ + @GuardedBy("mAppIdleLock") + private AppIdleHistory mAppIdleHistory; + + @GuardedBy("mPackageAccessListeners") + private ArrayList<AppIdleStateChangeListener> + mPackageAccessListeners = new ArrayList<>(); + + /** Whether we've queried the list of carrier privileged apps. */ + @GuardedBy("mAppIdleLock") + private boolean mHaveCarrierPrivilegedApps; + + /** List of carrier-privileged apps that should be excluded from standby */ + @GuardedBy("mAppIdleLock") + private List<String> mCarrierPrivilegedApps; + + @GuardedBy("mActiveAdminApps") + private final SparseArray<Set<String>> mActiveAdminApps = new SparseArray<>(); + + private final CountDownLatch mAdminDataAvailableLatch = new CountDownLatch(1); + + // Messages for the handler + static final int MSG_INFORM_LISTENERS = 3; + static final int MSG_FORCE_IDLE_STATE = 4; + static final int MSG_CHECK_IDLE_STATES = 5; + static final int MSG_REPORT_CONTENT_PROVIDER_USAGE = 8; + static final int MSG_ONE_TIME_CHECK_IDLE_STATES = 10; + /** Check the state of one app: arg1 = userId, arg2 = uid, obj = (String) packageName */ + static final int MSG_CHECK_PACKAGE_IDLE_STATE = 11; + static final int MSG_REPORT_SYNC_SCHEDULED = 12; + static final int MSG_REPORT_EXEMPTED_SYNC_START = 13; + + long mCheckIdleIntervalMillis; + long[] mAppStandbyScreenThresholds = SCREEN_TIME_THRESHOLDS; + long[] mAppStandbyElapsedThresholds = ELAPSED_TIME_THRESHOLDS; + /** Minimum time a strong usage event should keep the bucket elevated. */ + long mStrongUsageTimeoutMillis; + /** Minimum time a notification seen event should keep the bucket elevated. */ + long mNotificationSeenTimeoutMillis; + /** Minimum time a system update event should keep the buckets elevated. */ + long mSystemUpdateUsageTimeoutMillis; + /** Maximum time to wait for a prediction before using simple timeouts to downgrade buckets. */ + long mPredictionTimeoutMillis; + /** Maximum time a sync adapter associated with a CP should keep the buckets elevated. */ + long mSyncAdapterTimeoutMillis; + /** + * Maximum time an exempted sync should keep the buckets elevated, when sync is scheduled in + * non-doze + */ + long mExemptedSyncScheduledNonDozeTimeoutMillis; + /** + * Maximum time an exempted sync should keep the buckets elevated, when sync is scheduled in + * doze + */ + long mExemptedSyncScheduledDozeTimeoutMillis; + /** + * Maximum time an exempted sync should keep the buckets elevated, when sync is started. + */ + long mExemptedSyncStartTimeoutMillis; + /** + * Maximum time an unexempted sync should keep the buckets elevated, when sync is scheduled + */ + long mUnexemptedSyncScheduledTimeoutMillis; + /** Maximum time a system interaction should keep the buckets elevated. */ + long mSystemInteractionTimeoutMillis; + /** + * Maximum time a foreground service start should keep the buckets elevated if the service + * start is the first usage of the app + */ + long mInitialForegroundServiceStartTimeoutMillis; + + private volatile boolean mAppIdleEnabled; + private boolean mSystemServicesReady = false; + // There was a system update, defaults need to be initialized after services are ready + private boolean mPendingInitializeDefaults; + + private volatile boolean mPendingOneTimeCheckIdleStates; + + private final AppStandbyHandler mHandler; + private final Context mContext; + + // TODO: Provide a mechanism to set an external bucketing service + + private AppWidgetManager mAppWidgetManager; + private ConnectivityManager mConnectivityManager; + private PackageManager mPackageManager; + Injector mInjector; + + static final ArrayList<StandbyUpdateRecord> sStandbyUpdatePool = new ArrayList<>(4); + + public static class StandbyUpdateRecord { + // Identity of the app whose standby state has changed + String packageName; + int userId; + + // What the standby bucket the app is now in + int bucket; + + // Whether the bucket change is because the user has started interacting with the app + boolean isUserInteraction; + + // Reason for bucket change + int reason; + + StandbyUpdateRecord(String pkgName, int userId, int bucket, int reason, + boolean isInteraction) { + this.packageName = pkgName; + this.userId = userId; + this.bucket = bucket; + this.reason = reason; + this.isUserInteraction = isInteraction; + } + + public static StandbyUpdateRecord obtain(String pkgName, int userId, + int bucket, int reason, boolean isInteraction) { + synchronized (sStandbyUpdatePool) { + final int size = sStandbyUpdatePool.size(); + if (size < 1) { + return new StandbyUpdateRecord(pkgName, userId, bucket, reason, isInteraction); + } + StandbyUpdateRecord r = sStandbyUpdatePool.remove(size - 1); + r.packageName = pkgName; + r.userId = userId; + r.bucket = bucket; + r.reason = reason; + r.isUserInteraction = isInteraction; + return r; + } + } + + public void recycle() { + synchronized (sStandbyUpdatePool) { + sStandbyUpdatePool.add(this); + } + } + } + + public AppStandbyController(Context context, Looper looper) { + this(new Injector(context, looper)); + } + + AppStandbyController(Injector injector) { + mInjector = injector; + mContext = mInjector.getContext(); + mHandler = new AppStandbyHandler(mInjector.getLooper()); + mPackageManager = mContext.getPackageManager(); + + synchronized (mAppIdleLock) { + mAppIdleHistory = new AppIdleHistory(mInjector.getDataSystemDirectory(), + mInjector.elapsedRealtime()); + } + + IntentFilter packageFilter = new IntentFilter(); + packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED); + packageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED); + packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); + packageFilter.addDataScheme("package"); + + mContext.registerReceiverAsUser(new PackageReceiver(), UserHandle.ALL, packageFilter, + null, mHandler); + } + + @VisibleForTesting + void setAppIdleEnabled(boolean enabled) { + mAppIdleEnabled = enabled; + } + + @Override + public boolean isAppIdleEnabled() { + return mAppIdleEnabled; + } + + @Override + public void onBootPhase(int phase) { + mInjector.onBootPhase(phase); + if (phase == PHASE_SYSTEM_SERVICES_READY) { + Slog.d(TAG, "Setting app idle enabled state"); + // Observe changes to the threshold + SettingsObserver settingsObserver = new SettingsObserver(mHandler); + settingsObserver.registerObserver(); + settingsObserver.updateSettings(); + + mAppWidgetManager = mContext.getSystemService(AppWidgetManager.class); + mConnectivityManager = mContext.getSystemService(ConnectivityManager.class); + + mInjector.registerDisplayListener(mDisplayListener, mHandler); + synchronized (mAppIdleLock) { + mAppIdleHistory.updateDisplay(isDisplayOn(), mInjector.elapsedRealtime()); + } + + mSystemServicesReady = true; + + boolean userFileExists; + synchronized (mAppIdleLock) { + userFileExists = mAppIdleHistory.userFileExists(UserHandle.USER_SYSTEM); + } + + if (mPendingInitializeDefaults || !userFileExists) { + initializeDefaultsForSystemApps(UserHandle.USER_SYSTEM); + } + + if (mPendingOneTimeCheckIdleStates) { + postOneTimeCheckIdleStates(); + } + } + } + + private void reportContentProviderUsage(String authority, String providerPkgName, int userId) { + if (!mAppIdleEnabled) return; + + // Get sync adapters for the authority + String[] packages = ContentResolver.getSyncAdapterPackagesForAuthorityAsUser( + authority, userId); + final long elapsedRealtime = mInjector.elapsedRealtime(); + for (String packageName: packages) { + // Only force the sync adapters to active if the provider is not in the same package and + // the sync adapter is a system package. + try { + PackageInfo pi = mPackageManager.getPackageInfoAsUser( + packageName, PackageManager.MATCH_SYSTEM_ONLY, userId); + if (pi == null || pi.applicationInfo == null) { + continue; + } + if (!packageName.equals(providerPkgName)) { + synchronized (mAppIdleLock) { + AppUsageHistory appUsage = mAppIdleHistory.reportUsage(packageName, userId, + STANDBY_BUCKET_ACTIVE, REASON_SUB_USAGE_SYNC_ADAPTER, + 0, + elapsedRealtime + mSyncAdapterTimeoutMillis); + maybeInformListeners(packageName, userId, elapsedRealtime, + appUsage.currentBucket, appUsage.bucketingReason, false); + } + } + } catch (PackageManager.NameNotFoundException e) { + // Shouldn't happen + } + } + } + + private void reportExemptedSyncScheduled(String packageName, int userId) { + if (!mAppIdleEnabled) return; + + final int bucketToPromote; + final int usageReason; + final long durationMillis; + + if (!mInjector.isDeviceIdleMode()) { + // Not dozing. + bucketToPromote = STANDBY_BUCKET_ACTIVE; + usageReason = REASON_SUB_USAGE_EXEMPTED_SYNC_SCHEDULED_NON_DOZE; + durationMillis = mExemptedSyncScheduledNonDozeTimeoutMillis; + } else { + // Dozing. + bucketToPromote = STANDBY_BUCKET_WORKING_SET; + usageReason = REASON_SUB_USAGE_EXEMPTED_SYNC_SCHEDULED_DOZE; + durationMillis = mExemptedSyncScheduledDozeTimeoutMillis; + } + + final long elapsedRealtime = mInjector.elapsedRealtime(); + + synchronized (mAppIdleLock) { + AppUsageHistory appUsage = mAppIdleHistory.reportUsage(packageName, userId, + bucketToPromote, usageReason, + 0, + elapsedRealtime + durationMillis); + maybeInformListeners(packageName, userId, elapsedRealtime, + appUsage.currentBucket, appUsage.bucketingReason, false); + } + } + + private void reportUnexemptedSyncScheduled(String packageName, int userId) { + if (!mAppIdleEnabled) return; + + final long elapsedRealtime = mInjector.elapsedRealtime(); + synchronized (mAppIdleLock) { + final int currentBucket = + mAppIdleHistory.getAppStandbyBucket(packageName, userId, elapsedRealtime); + if (currentBucket == STANDBY_BUCKET_NEVER) { + // Bring the app out of the never bucket + AppUsageHistory appUsage = mAppIdleHistory.reportUsage(packageName, userId, + STANDBY_BUCKET_WORKING_SET, REASON_SUB_USAGE_UNEXEMPTED_SYNC_SCHEDULED, + 0, + elapsedRealtime + mUnexemptedSyncScheduledTimeoutMillis); + maybeInformListeners(packageName, userId, elapsedRealtime, + appUsage.currentBucket, appUsage.bucketingReason, false); + } + } + } + + private void reportExemptedSyncStart(String packageName, int userId) { + if (!mAppIdleEnabled) return; + + final long elapsedRealtime = mInjector.elapsedRealtime(); + + synchronized (mAppIdleLock) { + AppUsageHistory appUsage = mAppIdleHistory.reportUsage(packageName, userId, + STANDBY_BUCKET_ACTIVE, REASON_SUB_USAGE_EXEMPTED_SYNC_START, + 0, + elapsedRealtime + mExemptedSyncStartTimeoutMillis); + maybeInformListeners(packageName, userId, elapsedRealtime, + appUsage.currentBucket, appUsage.bucketingReason, false); + } + } + + @Override + public void postCheckIdleStates(int userId) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_CHECK_IDLE_STATES, userId, 0)); + } + + @Override + public void postOneTimeCheckIdleStates() { + if (mInjector.getBootPhase() < PHASE_SYSTEM_SERVICES_READY) { + // Not booted yet; wait for it! + mPendingOneTimeCheckIdleStates = true; + } else { + mHandler.sendEmptyMessage(MSG_ONE_TIME_CHECK_IDLE_STATES); + mPendingOneTimeCheckIdleStates = false; + } + } + + @VisibleForTesting + boolean checkIdleStates(int checkUserId) { + if (!mAppIdleEnabled) { + return false; + } + + final int[] runningUserIds; + try { + runningUserIds = mInjector.getRunningUserIds(); + if (checkUserId != UserHandle.USER_ALL + && !ArrayUtils.contains(runningUserIds, checkUserId)) { + return false; + } + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + + final long elapsedRealtime = mInjector.elapsedRealtime(); + for (int i = 0; i < runningUserIds.length; i++) { + final int userId = runningUserIds[i]; + if (checkUserId != UserHandle.USER_ALL && checkUserId != userId) { + continue; + } + if (DEBUG) { + Slog.d(TAG, "Checking idle state for user " + userId); + } + List<PackageInfo> packages = mPackageManager.getInstalledPackagesAsUser( + PackageManager.MATCH_DISABLED_COMPONENTS, + userId); + final int packageCount = packages.size(); + for (int p = 0; p < packageCount; p++) { + final PackageInfo pi = packages.get(p); + final String packageName = pi.packageName; + checkAndUpdateStandbyState(packageName, userId, pi.applicationInfo.uid, + elapsedRealtime); + } + } + if (DEBUG) { + Slog.d(TAG, "checkIdleStates took " + + (mInjector.elapsedRealtime() - elapsedRealtime)); + } + return true; + } + + /** Check if we need to update the standby state of a specific app. */ + private void checkAndUpdateStandbyState(String packageName, @UserIdInt int userId, + int uid, long elapsedRealtime) { + if (uid <= 0) { + try { + uid = mPackageManager.getPackageUidAsUser(packageName, userId); + } catch (PackageManager.NameNotFoundException e) { + // Not a valid package for this user, nothing to do + // TODO: Remove any history of removed packages + return; + } + } + final boolean isSpecial = isAppSpecial(packageName, + UserHandle.getAppId(uid), + userId); + if (DEBUG) { + Slog.d(TAG, " Checking idle state for " + packageName + " special=" + + isSpecial); + } + if (isSpecial) { + synchronized (mAppIdleLock) { + mAppIdleHistory.setAppStandbyBucket(packageName, userId, elapsedRealtime, + STANDBY_BUCKET_EXEMPTED, REASON_MAIN_DEFAULT); + } + maybeInformListeners(packageName, userId, elapsedRealtime, + STANDBY_BUCKET_EXEMPTED, REASON_MAIN_DEFAULT, false); + } else { + synchronized (mAppIdleLock) { + final AppIdleHistory.AppUsageHistory app = + mAppIdleHistory.getAppUsageHistory(packageName, + userId, elapsedRealtime); + int reason = app.bucketingReason; + final int oldMainReason = reason & REASON_MAIN_MASK; + + // If the bucket was forced by the user/developer, leave it alone. + // A usage event will be the only way to bring it out of this forced state + if (oldMainReason == REASON_MAIN_FORCED) { + return; + } + final int oldBucket = app.currentBucket; + int newBucket = Math.max(oldBucket, STANDBY_BUCKET_ACTIVE); // Undo EXEMPTED + boolean predictionLate = predictionTimedOut(app, elapsedRealtime); + // Compute age-based bucket + if (oldMainReason == REASON_MAIN_DEFAULT + || oldMainReason == REASON_MAIN_USAGE + || oldMainReason == REASON_MAIN_TIMEOUT + || predictionLate) { + + if (!predictionLate && app.lastPredictedBucket >= STANDBY_BUCKET_ACTIVE + && app.lastPredictedBucket <= STANDBY_BUCKET_RARE) { + newBucket = app.lastPredictedBucket; + reason = REASON_MAIN_PREDICTED | REASON_SUB_PREDICTED_RESTORED; + if (DEBUG) { + Slog.d(TAG, "Restored predicted newBucket = " + newBucket); + } + } else { + newBucket = getBucketForLocked(packageName, userId, + elapsedRealtime); + if (DEBUG) { + Slog.d(TAG, "Evaluated AOSP newBucket = " + newBucket); + } + reason = REASON_MAIN_TIMEOUT; + } + } + + // Check if the app is within one of the timeouts for forced bucket elevation + final long elapsedTimeAdjusted = mAppIdleHistory.getElapsedTime(elapsedRealtime); + if (newBucket >= STANDBY_BUCKET_ACTIVE + && app.bucketActiveTimeoutTime > elapsedTimeAdjusted) { + newBucket = STANDBY_BUCKET_ACTIVE; + reason = app.bucketingReason; + if (DEBUG) { + Slog.d(TAG, " Keeping at ACTIVE due to min timeout"); + } + } else if (newBucket >= STANDBY_BUCKET_WORKING_SET + && app.bucketWorkingSetTimeoutTime > elapsedTimeAdjusted) { + newBucket = STANDBY_BUCKET_WORKING_SET; + // If it was already there, keep the reason, else assume timeout to WS + reason = (newBucket == oldBucket) + ? app.bucketingReason + : REASON_MAIN_USAGE | REASON_SUB_USAGE_ACTIVE_TIMEOUT; + if (DEBUG) { + Slog.d(TAG, " Keeping at WORKING_SET due to min timeout"); + } + } + if (DEBUG) { + Slog.d(TAG, " Old bucket=" + oldBucket + + ", newBucket=" + newBucket); + } + if (oldBucket < newBucket || predictionLate) { + mAppIdleHistory.setAppStandbyBucket(packageName, userId, + elapsedRealtime, newBucket, reason); + maybeInformListeners(packageName, userId, elapsedRealtime, + newBucket, reason, false); + } + } + } + } + + /** Returns true if there hasn't been a prediction for the app in a while. */ + private boolean predictionTimedOut(AppIdleHistory.AppUsageHistory app, long elapsedRealtime) { + return app.lastPredictedTime > 0 + && mAppIdleHistory.getElapsedTime(elapsedRealtime) + - app.lastPredictedTime > mPredictionTimeoutMillis; + } + + /** Inform listeners if the bucket has changed since it was last reported to listeners */ + private void maybeInformListeners(String packageName, int userId, + long elapsedRealtime, int bucket, int reason, boolean userStartedInteracting) { + synchronized (mAppIdleLock) { + if (mAppIdleHistory.shouldInformListeners(packageName, userId, + elapsedRealtime, bucket)) { + final StandbyUpdateRecord r = StandbyUpdateRecord.obtain(packageName, userId, + bucket, reason, userStartedInteracting); + if (DEBUG) Slog.d(TAG, "Standby bucket for " + packageName + "=" + bucket); + mHandler.sendMessage(mHandler.obtainMessage(MSG_INFORM_LISTENERS, r)); + } + } + } + + /** + * Evaluates next bucket based on time since last used and the bucketing thresholds. + * @param packageName the app + * @param userId the user + * @param elapsedRealtime as the name suggests, current elapsed time + * @return the bucket for the app, based on time since last used + */ + @GuardedBy("mAppIdleLock") + @StandbyBuckets + private int getBucketForLocked(String packageName, int userId, + long elapsedRealtime) { + int bucketIndex = mAppIdleHistory.getThresholdIndex(packageName, userId, + elapsedRealtime, mAppStandbyScreenThresholds, mAppStandbyElapsedThresholds); + return THRESHOLD_BUCKETS[bucketIndex]; + } + + private void notifyBatteryStats(String packageName, int userId, boolean idle) { + try { + final int uid = mPackageManager.getPackageUidAsUser(packageName, + PackageManager.MATCH_UNINSTALLED_PACKAGES, userId); + if (idle) { + mInjector.noteEvent(BatteryStats.HistoryItem.EVENT_PACKAGE_INACTIVE, + packageName, uid); + } else { + mInjector.noteEvent(BatteryStats.HistoryItem.EVENT_PACKAGE_ACTIVE, + packageName, uid); + } + } catch (PackageManager.NameNotFoundException | RemoteException e) { + } + } + + @Override + public void reportEvent(UsageEvents.Event event, long elapsedRealtime, int userId) { + if (!mAppIdleEnabled) return; + synchronized (mAppIdleLock) { + final String pkg = event.getPackageName(); + final int eventType = event.getEventType(); + // TODO: Ideally this should call isAppIdleFiltered() to avoid calling back + // about apps that are on some kind of whitelist anyway. + final boolean previouslyIdle = mAppIdleHistory.isIdle( + pkg, userId, elapsedRealtime); + // Inform listeners if necessary + if ((eventType == UsageEvents.Event.ACTIVITY_RESUMED + || eventType == UsageEvents.Event.ACTIVITY_PAUSED + || eventType == UsageEvents.Event.SYSTEM_INTERACTION + || eventType == UsageEvents.Event.USER_INTERACTION + || eventType == UsageEvents.Event.NOTIFICATION_SEEN + || eventType == UsageEvents.Event.SLICE_PINNED + || eventType == UsageEvents.Event.SLICE_PINNED_PRIV + || eventType == UsageEvents.Event.FOREGROUND_SERVICE_START)) { + + final AppUsageHistory appHistory = mAppIdleHistory.getAppUsageHistory( + pkg, userId, elapsedRealtime); + final int prevBucket = appHistory.currentBucket; + final int prevBucketReason = appHistory.bucketingReason; + final long nextCheckTime; + final int subReason = usageEventToSubReason(eventType); + final int reason = REASON_MAIN_USAGE | subReason; + if (eventType == UsageEvents.Event.NOTIFICATION_SEEN + || eventType == UsageEvents.Event.SLICE_PINNED) { + // Mild usage elevates to WORKING_SET but doesn't change usage time. + mAppIdleHistory.reportUsage(appHistory, pkg, + STANDBY_BUCKET_WORKING_SET, subReason, + 0, elapsedRealtime + mNotificationSeenTimeoutMillis); + nextCheckTime = mNotificationSeenTimeoutMillis; + } else if (eventType == UsageEvents.Event.SYSTEM_INTERACTION) { + mAppIdleHistory.reportUsage(appHistory, pkg, + STANDBY_BUCKET_ACTIVE, subReason, + 0, elapsedRealtime + mSystemInteractionTimeoutMillis); + nextCheckTime = mSystemInteractionTimeoutMillis; + } else if (eventType == UsageEvents.Event.FOREGROUND_SERVICE_START) { + // Only elevate bucket if this is the first usage of the app + if (prevBucket != STANDBY_BUCKET_NEVER) return; + mAppIdleHistory.reportUsage(appHistory, pkg, + STANDBY_BUCKET_ACTIVE, subReason, + 0, elapsedRealtime + mInitialForegroundServiceStartTimeoutMillis); + nextCheckTime = mInitialForegroundServiceStartTimeoutMillis; + } else { + mAppIdleHistory.reportUsage(appHistory, pkg, + STANDBY_BUCKET_ACTIVE, subReason, + elapsedRealtime, elapsedRealtime + mStrongUsageTimeoutMillis); + nextCheckTime = mStrongUsageTimeoutMillis; + } + mHandler.sendMessageDelayed(mHandler.obtainMessage + (MSG_CHECK_PACKAGE_IDLE_STATE, userId, -1, pkg), + nextCheckTime); + final boolean userStartedInteracting = + appHistory.currentBucket == STANDBY_BUCKET_ACTIVE && + prevBucket != appHistory.currentBucket && + (prevBucketReason & REASON_MAIN_MASK) != REASON_MAIN_USAGE; + maybeInformListeners(pkg, userId, elapsedRealtime, + appHistory.currentBucket, reason, userStartedInteracting); + + if (previouslyIdle) { + notifyBatteryStats(pkg, userId, false); + } + } + } + } + + private int usageEventToSubReason(int eventType) { + switch (eventType) { + case UsageEvents.Event.ACTIVITY_RESUMED: return REASON_SUB_USAGE_MOVE_TO_FOREGROUND; + case UsageEvents.Event.ACTIVITY_PAUSED: return REASON_SUB_USAGE_MOVE_TO_BACKGROUND; + case UsageEvents.Event.SYSTEM_INTERACTION: return REASON_SUB_USAGE_SYSTEM_INTERACTION; + case UsageEvents.Event.USER_INTERACTION: return REASON_SUB_USAGE_USER_INTERACTION; + case UsageEvents.Event.NOTIFICATION_SEEN: return REASON_SUB_USAGE_NOTIFICATION_SEEN; + case UsageEvents.Event.SLICE_PINNED: return REASON_SUB_USAGE_SLICE_PINNED; + case UsageEvents.Event.SLICE_PINNED_PRIV: return REASON_SUB_USAGE_SLICE_PINNED_PRIV; + case UsageEvents.Event.FOREGROUND_SERVICE_START: + return REASON_SUB_USAGE_FOREGROUND_SERVICE_START; + default: return 0; + } + } + + @VisibleForTesting + void forceIdleState(String packageName, int userId, boolean idle) { + if (!mAppIdleEnabled) return; + + final int appId = getAppId(packageName); + if (appId < 0) return; + final long elapsedRealtime = mInjector.elapsedRealtime(); + + final boolean previouslyIdle = isAppIdleFiltered(packageName, appId, + userId, elapsedRealtime); + final int standbyBucket; + synchronized (mAppIdleLock) { + standbyBucket = mAppIdleHistory.setIdle(packageName, userId, idle, elapsedRealtime); + } + final boolean stillIdle = isAppIdleFiltered(packageName, appId, + userId, elapsedRealtime); + // Inform listeners if necessary + if (previouslyIdle != stillIdle) { + maybeInformListeners(packageName, userId, elapsedRealtime, standbyBucket, + REASON_MAIN_FORCED, false); + if (!stillIdle) { + notifyBatteryStats(packageName, userId, idle); + } + } + } + + @Override + public void setLastJobRunTime(String packageName, int userId, long elapsedRealtime) { + synchronized (mAppIdleLock) { + mAppIdleHistory.setLastJobRunTime(packageName, userId, elapsedRealtime); + } + } + + @Override + public long getTimeSinceLastJobRun(String packageName, int userId) { + final long elapsedRealtime = mInjector.elapsedRealtime(); + synchronized (mAppIdleLock) { + return mAppIdleHistory.getTimeSinceLastJobRun(packageName, userId, elapsedRealtime); + } + } + + @Override + public void onUserRemoved(int userId) { + synchronized (mAppIdleLock) { + mAppIdleHistory.onUserRemoved(userId); + synchronized (mActiveAdminApps) { + mActiveAdminApps.remove(userId); + } + } + } + + private boolean isAppIdleUnfiltered(String packageName, int userId, long elapsedRealtime) { + synchronized (mAppIdleLock) { + return mAppIdleHistory.isIdle(packageName, userId, elapsedRealtime); + } + } + + @Override + public void addListener(AppIdleStateChangeListener listener) { + synchronized (mPackageAccessListeners) { + if (!mPackageAccessListeners.contains(listener)) { + mPackageAccessListeners.add(listener); + } + } + } + + @Override + public void removeListener(AppIdleStateChangeListener listener) { + synchronized (mPackageAccessListeners) { + mPackageAccessListeners.remove(listener); + } + } + + @Override + public int getAppId(String packageName) { + try { + ApplicationInfo ai = mPackageManager.getApplicationInfo(packageName, + PackageManager.MATCH_ANY_USER + | PackageManager.MATCH_DISABLED_COMPONENTS); + return ai.uid; + } catch (PackageManager.NameNotFoundException re) { + return -1; + } + } + + @Override + public boolean isAppIdleFiltered(String packageName, int userId, long elapsedRealtime, + boolean shouldObfuscateInstantApps) { + if (shouldObfuscateInstantApps && + mInjector.isPackageEphemeral(userId, packageName)) { + return false; + } + return isAppIdleFiltered(packageName, getAppId(packageName), userId, elapsedRealtime); + } + + private boolean isAppSpecial(String packageName, int appId, int userId) { + if (packageName == null) return false; + // If not enabled at all, of course nobody is ever idle. + if (!mAppIdleEnabled) { + return true; + } + if (appId < Process.FIRST_APPLICATION_UID) { + // System uids never go idle. + return true; + } + if (packageName.equals("android")) { + // Nor does the framework (which should be redundant with the above, but for MR1 we will + // retain this for safety). + return true; + } + if (mSystemServicesReady) { + try { + // We allow all whitelisted apps, including those that don't want to be whitelisted + // for idle mode, because app idle (aka app standby) is really not as big an issue + // for controlling who participates vs. doze mode. + if (mInjector.isPowerSaveWhitelistExceptIdleApp(packageName)) { + return true; + } + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + + if (isActiveDeviceAdmin(packageName, userId)) { + return true; + } + + if (isActiveNetworkScorer(packageName)) { + return true; + } + + if (mAppWidgetManager != null + && mInjector.isBoundWidgetPackage(mAppWidgetManager, packageName, userId)) { + return true; + } + + if (isDeviceProvisioningPackage(packageName)) { + return true; + } + } + + // Check this last, as it can be the most expensive check + if (isCarrierApp(packageName)) { + return true; + } + + return false; + } + + @Override + public boolean isAppIdleFiltered(String packageName, int appId, int userId, + long elapsedRealtime) { + if (isAppSpecial(packageName, appId, userId)) { + return false; + } else { + return isAppIdleUnfiltered(packageName, userId, elapsedRealtime); + } + } + + @Override + public int[] getIdleUidsForUser(int userId) { + if (!mAppIdleEnabled) { + return new int[0]; + } + + final long elapsedRealtime = mInjector.elapsedRealtime(); + + List<ApplicationInfo> apps; + try { + ParceledListSlice<ApplicationInfo> slice = AppGlobals.getPackageManager() + .getInstalledApplications(/* flags= */ 0, userId); + if (slice == null) { + return new int[0]; + } + apps = slice.getList(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + + // State of each uid. Key is the uid. Value lower 16 bits is the number of apps + // associated with that uid, upper 16 bits is the number of those apps that is idle. + SparseIntArray uidStates = new SparseIntArray(); + + // Now resolve all app state. Iterating over all apps, keeping track of how many + // we find for each uid and how many of those are idle. + for (int i = apps.size() - 1; i >= 0; i--) { + ApplicationInfo ai = apps.get(i); + + // Check whether this app is idle. + boolean idle = isAppIdleFiltered(ai.packageName, UserHandle.getAppId(ai.uid), + userId, elapsedRealtime); + + int index = uidStates.indexOfKey(ai.uid); + if (index < 0) { + uidStates.put(ai.uid, 1 + (idle ? 1<<16 : 0)); + } else { + int value = uidStates.valueAt(index); + uidStates.setValueAt(index, value + 1 + (idle ? 1<<16 : 0)); + } + } + if (DEBUG) { + Slog.d(TAG, "getIdleUids took " + (mInjector.elapsedRealtime() - elapsedRealtime)); + } + int numIdle = 0; + for (int i = uidStates.size() - 1; i >= 0; i--) { + int value = uidStates.valueAt(i); + if ((value&0x7fff) == (value>>16)) { + numIdle++; + } + } + + int[] res = new int[numIdle]; + numIdle = 0; + for (int i = uidStates.size() - 1; i >= 0; i--) { + int value = uidStates.valueAt(i); + if ((value&0x7fff) == (value>>16)) { + res[numIdle] = uidStates.keyAt(i); + numIdle++; + } + } + + return res; + } + + @Override + public void setAppIdleAsync(String packageName, boolean idle, int userId) { + if (packageName == null || !mAppIdleEnabled) return; + + mHandler.obtainMessage(MSG_FORCE_IDLE_STATE, userId, idle ? 1 : 0, packageName) + .sendToTarget(); + } + + @Override + @StandbyBuckets public int getAppStandbyBucket(String packageName, int userId, + long elapsedRealtime, boolean shouldObfuscateInstantApps) { + if (!mAppIdleEnabled || (shouldObfuscateInstantApps + && mInjector.isPackageEphemeral(userId, packageName))) { + return STANDBY_BUCKET_ACTIVE; + } + + synchronized (mAppIdleLock) { + return mAppIdleHistory.getAppStandbyBucket(packageName, userId, elapsedRealtime); + } + } + + @Override + public List<AppStandbyInfo> getAppStandbyBuckets(int userId) { + synchronized (mAppIdleLock) { + return mAppIdleHistory.getAppStandbyBuckets(userId, mAppIdleEnabled); + } + } + + @VisibleForTesting + void setAppStandbyBucket(String packageName, int userId, @StandbyBuckets int newBucket, + int reason, long elapsedRealtime) { + setAppStandbyBucket(packageName, userId, newBucket, reason, elapsedRealtime, false); + } + + @Override + public void setAppStandbyBucket(String packageName, int userId, @StandbyBuckets int newBucket, + int reason, long elapsedRealtime, boolean resetTimeout) { + synchronized (mAppIdleLock) { + // If the package is not installed, don't allow the bucket to be set. + if (!mInjector.isPackageInstalled(packageName, 0, userId)) { + return; + } + AppIdleHistory.AppUsageHistory app = mAppIdleHistory.getAppUsageHistory(packageName, + userId, elapsedRealtime); + boolean predicted = (reason & REASON_MAIN_MASK) == REASON_MAIN_PREDICTED; + + // Don't allow changing bucket if higher than ACTIVE + if (app.currentBucket < STANDBY_BUCKET_ACTIVE) return; + + // Don't allow prediction to change from/to NEVER + if ((app.currentBucket == STANDBY_BUCKET_NEVER + || newBucket == STANDBY_BUCKET_NEVER) + && predicted) { + return; + } + + // If the bucket was forced, don't allow prediction to override + if ((app.bucketingReason & REASON_MAIN_MASK) == REASON_MAIN_FORCED && predicted) return; + + // If the bucket is required to stay in a higher state for a specified duration, don't + // override unless the duration has passed + if (predicted) { + // Check if the app is within one of the timeouts for forced bucket elevation + final long elapsedTimeAdjusted = mAppIdleHistory.getElapsedTime(elapsedRealtime); + // In case of not using the prediction, just keep track of it for applying after + // ACTIVE or WORKING_SET timeout. + mAppIdleHistory.updateLastPrediction(app, elapsedTimeAdjusted, newBucket); + + if (newBucket > STANDBY_BUCKET_ACTIVE + && app.bucketActiveTimeoutTime > elapsedTimeAdjusted) { + newBucket = STANDBY_BUCKET_ACTIVE; + reason = app.bucketingReason; + if (DEBUG) { + Slog.d(TAG, " Keeping at ACTIVE due to min timeout"); + } + } else if (newBucket > STANDBY_BUCKET_WORKING_SET + && app.bucketWorkingSetTimeoutTime > elapsedTimeAdjusted) { + newBucket = STANDBY_BUCKET_WORKING_SET; + if (app.currentBucket != newBucket) { + reason = REASON_MAIN_USAGE | REASON_SUB_USAGE_ACTIVE_TIMEOUT; + } else { + reason = app.bucketingReason; + } + if (DEBUG) { + Slog.d(TAG, " Keeping at WORKING_SET due to min timeout"); + } + } + } + + mAppIdleHistory.setAppStandbyBucket(packageName, userId, elapsedRealtime, newBucket, + reason, resetTimeout); + } + maybeInformListeners(packageName, userId, elapsedRealtime, newBucket, reason, false); + } + + @VisibleForTesting + boolean isActiveDeviceAdmin(String packageName, int userId) { + synchronized (mActiveAdminApps) { + final Set<String> adminPkgs = mActiveAdminApps.get(userId); + return adminPkgs != null && adminPkgs.contains(packageName); + } + } + + @Override + public void addActiveDeviceAdmin(String adminPkg, int userId) { + synchronized (mActiveAdminApps) { + Set<String> adminPkgs = mActiveAdminApps.get(userId); + if (adminPkgs == null) { + adminPkgs = new ArraySet<>(); + mActiveAdminApps.put(userId, adminPkgs); + } + adminPkgs.add(adminPkg); + } + } + + @Override + public void setActiveAdminApps(Set<String> adminPkgs, int userId) { + synchronized (mActiveAdminApps) { + if (adminPkgs == null) { + mActiveAdminApps.remove(userId); + } else { + mActiveAdminApps.put(userId, adminPkgs); + } + } + } + + @Override + public void onAdminDataAvailable() { + mAdminDataAvailableLatch.countDown(); + } + + /** + * This will only ever be called once - during device boot. + */ + private void waitForAdminData() { + if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN)) { + ConcurrentUtils.waitForCountDownNoInterrupt(mAdminDataAvailableLatch, + WAIT_FOR_ADMIN_DATA_TIMEOUT_MS, "Wait for admin data"); + } + } + + @VisibleForTesting + Set<String> getActiveAdminAppsForTest(int userId) { + synchronized (mActiveAdminApps) { + return mActiveAdminApps.get(userId); + } + } + + /** + * Returns {@code true} if the supplied package is the device provisioning app. Otherwise, + * returns {@code false}. + */ + private boolean isDeviceProvisioningPackage(String packageName) { + String deviceProvisioningPackage = mContext.getResources().getString( + com.android.internal.R.string.config_deviceProvisioningPackage); + return deviceProvisioningPackage != null && deviceProvisioningPackage.equals(packageName); + } + + private boolean isCarrierApp(String packageName) { + synchronized (mAppIdleLock) { + if (!mHaveCarrierPrivilegedApps) { + fetchCarrierPrivilegedAppsLocked(); + } + if (mCarrierPrivilegedApps != null) { + return mCarrierPrivilegedApps.contains(packageName); + } + return false; + } + } + + @Override + public void clearCarrierPrivilegedApps() { + if (DEBUG) { + Slog.i(TAG, "Clearing carrier privileged apps list"); + } + synchronized (mAppIdleLock) { + mHaveCarrierPrivilegedApps = false; + mCarrierPrivilegedApps = null; // Need to be refetched. + } + } + + @GuardedBy("mAppIdleLock") + private void fetchCarrierPrivilegedAppsLocked() { + TelephonyManager telephonyManager = + mContext.getSystemService(TelephonyManager.class); + mCarrierPrivilegedApps = telephonyManager.getPackagesWithCarrierPrivilegesForAllPhones(); + mHaveCarrierPrivilegedApps = true; + if (DEBUG) { + Slog.d(TAG, "apps with carrier privilege " + mCarrierPrivilegedApps); + } + } + + private boolean isActiveNetworkScorer(String packageName) { + String activeScorer = mInjector.getActiveNetworkScorer(); + return packageName != null && packageName.equals(activeScorer); + } + + private void informListeners(String packageName, int userId, int bucket, int reason, + boolean userInteraction) { + final boolean idle = bucket >= STANDBY_BUCKET_RARE; + synchronized (mPackageAccessListeners) { + for (AppIdleStateChangeListener listener : mPackageAccessListeners) { + listener.onAppIdleStateChanged(packageName, userId, idle, bucket, reason); + if (userInteraction) { + listener.onUserInteractionStarted(packageName, userId); + } + } + } + } + + @Override + public void flushToDisk(int userId) { + synchronized (mAppIdleLock) { + mAppIdleHistory.writeAppIdleTimes(userId); + } + } + + @Override + public void flushDurationsToDisk() { + // Persist elapsed and screen on time. If this fails for whatever reason, the apps will be + // considered not-idle, which is the safest outcome in such an event. + synchronized (mAppIdleLock) { + mAppIdleHistory.writeAppIdleDurations(); + } + } + + private boolean isDisplayOn() { + return mInjector.isDefaultDisplayOn(); + } + + @VisibleForTesting + void clearAppIdleForPackage(String packageName, int userId) { + synchronized (mAppIdleLock) { + mAppIdleHistory.clearUsage(packageName, userId); + } + } + + private class PackageReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (Intent.ACTION_PACKAGE_ADDED.equals(action) + || Intent.ACTION_PACKAGE_CHANGED.equals(action)) { + clearCarrierPrivilegedApps(); + } + if ((Intent.ACTION_PACKAGE_REMOVED.equals(action) || + Intent.ACTION_PACKAGE_ADDED.equals(action)) + && !intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { + clearAppIdleForPackage(intent.getData().getSchemeSpecificPart(), + getSendingUserId()); + } + } + } + + @Override + public void initializeDefaultsForSystemApps(int userId) { + if (!mSystemServicesReady) { + // Do it later, since SettingsProvider wasn't queried yet for app_standby_enabled + mPendingInitializeDefaults = true; + return; + } + Slog.d(TAG, "Initializing defaults for system apps on user " + userId + ", " + + "appIdleEnabled=" + mAppIdleEnabled); + final long elapsedRealtime = mInjector.elapsedRealtime(); + List<PackageInfo> packages = mPackageManager.getInstalledPackagesAsUser( + PackageManager.MATCH_DISABLED_COMPONENTS, + userId); + final int packageCount = packages.size(); + synchronized (mAppIdleLock) { + for (int i = 0; i < packageCount; i++) { + final PackageInfo pi = packages.get(i); + String packageName = pi.packageName; + if (pi.applicationInfo != null && pi.applicationInfo.isSystemApp()) { + // Mark app as used for 2 hours. After that it can timeout to whatever the + // past usage pattern was. + mAppIdleHistory.reportUsage(packageName, userId, STANDBY_BUCKET_ACTIVE, + REASON_SUB_USAGE_SYSTEM_UPDATE, 0, + elapsedRealtime + mSystemUpdateUsageTimeoutMillis); + } + } + // Immediately persist defaults to disk + mAppIdleHistory.writeAppIdleTimes(userId); + } + } + + @Override + public void postReportContentProviderUsage(String name, String packageName, int userId) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = name; + args.arg2 = packageName; + args.arg3 = userId; + mHandler.obtainMessage(MSG_REPORT_CONTENT_PROVIDER_USAGE, args) + .sendToTarget(); + } + + @Override + public void postReportSyncScheduled(String packageName, int userId, boolean exempted) { + mHandler.obtainMessage(MSG_REPORT_SYNC_SCHEDULED, userId, exempted ? 1 : 0, packageName) + .sendToTarget(); + } + + @Override + public void postReportExemptedSyncStart(String packageName, int userId) { + mHandler.obtainMessage(MSG_REPORT_EXEMPTED_SYNC_START, userId, 0, packageName) + .sendToTarget(); + } + + @Override + public void dumpUser(IndentingPrintWriter idpw, int userId, String pkg) { + synchronized (mAppIdleLock) { + mAppIdleHistory.dump(idpw, userId, pkg); + } + } + + @Override + public void dumpState(String[] args, PrintWriter pw) { + synchronized (mAppIdleLock) { + pw.println("Carrier privileged apps (have=" + mHaveCarrierPrivilegedApps + + "): " + mCarrierPrivilegedApps); + } + + final long now = System.currentTimeMillis(); + + pw.println(); + pw.println("Settings:"); + + pw.print(" mCheckIdleIntervalMillis="); + TimeUtils.formatDuration(mCheckIdleIntervalMillis, pw); + pw.println(); + + pw.print(" mStrongUsageTimeoutMillis="); + TimeUtils.formatDuration(mStrongUsageTimeoutMillis, pw); + pw.println(); + pw.print(" mNotificationSeenTimeoutMillis="); + TimeUtils.formatDuration(mNotificationSeenTimeoutMillis, pw); + pw.println(); + pw.print(" mSyncAdapterTimeoutMillis="); + TimeUtils.formatDuration(mSyncAdapterTimeoutMillis, pw); + pw.println(); + pw.print(" mSystemInteractionTimeoutMillis="); + TimeUtils.formatDuration(mSystemInteractionTimeoutMillis, pw); + pw.println(); + pw.print(" mInitialForegroundServiceStartTimeoutMillis="); + TimeUtils.formatDuration(mInitialForegroundServiceStartTimeoutMillis, pw); + pw.println(); + + pw.print(" mPredictionTimeoutMillis="); + TimeUtils.formatDuration(mPredictionTimeoutMillis, pw); + pw.println(); + + pw.print(" mExemptedSyncScheduledNonDozeTimeoutMillis="); + TimeUtils.formatDuration(mExemptedSyncScheduledNonDozeTimeoutMillis, pw); + pw.println(); + pw.print(" mExemptedSyncScheduledDozeTimeoutMillis="); + TimeUtils.formatDuration(mExemptedSyncScheduledDozeTimeoutMillis, pw); + pw.println(); + pw.print(" mExemptedSyncStartTimeoutMillis="); + TimeUtils.formatDuration(mExemptedSyncStartTimeoutMillis, pw); + pw.println(); + pw.print(" mUnexemptedSyncScheduledTimeoutMillis="); + TimeUtils.formatDuration(mUnexemptedSyncScheduledTimeoutMillis, pw); + pw.println(); + + pw.print(" mSystemUpdateUsageTimeoutMillis="); + TimeUtils.formatDuration(mSystemUpdateUsageTimeoutMillis, pw); + pw.println(); + + pw.println(); + pw.print("mAppIdleEnabled="); pw.print(mAppIdleEnabled); + pw.println(); + pw.print("mScreenThresholds="); pw.println(Arrays.toString(mAppStandbyScreenThresholds)); + pw.print("mElapsedThresholds="); pw.println(Arrays.toString(mAppStandbyElapsedThresholds)); + pw.println(); + } + + /** + * Injector for interaction with external code. Override methods to provide a mock + * implementation for tests. + * onBootPhase() must be called with at least the PHASE_SYSTEM_SERVICES_READY + */ + static class Injector { + + private final Context mContext; + private final Looper mLooper; + private IDeviceIdleController mDeviceIdleController; + private IBatteryStats mBatteryStats; + private PackageManagerInternal mPackageManagerInternal; + private DisplayManager mDisplayManager; + private PowerManager mPowerManager; + int mBootPhase; + + Injector(Context context, Looper looper) { + mContext = context; + mLooper = looper; + } + + Context getContext() { + return mContext; + } + + Looper getLooper() { + return mLooper; + } + + void onBootPhase(int phase) { + if (phase == PHASE_SYSTEM_SERVICES_READY) { + mDeviceIdleController = IDeviceIdleController.Stub.asInterface( + ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER)); + mBatteryStats = IBatteryStats.Stub.asInterface( + ServiceManager.getService(BatteryStats.SERVICE_NAME)); + mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class); + mDisplayManager = (DisplayManager) mContext.getSystemService( + Context.DISPLAY_SERVICE); + mPowerManager = mContext.getSystemService(PowerManager.class); + } + mBootPhase = phase; + } + + int getBootPhase() { + return mBootPhase; + } + + /** + * Returns the elapsed realtime since the device started. Override this + * to control the clock. + * @return elapsed realtime + */ + long elapsedRealtime() { + return SystemClock.elapsedRealtime(); + } + + long currentTimeMillis() { + return System.currentTimeMillis(); + } + + boolean isAppIdleEnabled() { + final boolean buildFlag = mContext.getResources().getBoolean( + com.android.internal.R.bool.config_enableAutoPowerModes); + final boolean runtimeFlag = Global.getInt(mContext.getContentResolver(), + Global.APP_STANDBY_ENABLED, 1) == 1 + && Global.getInt(mContext.getContentResolver(), + Global.ADAPTIVE_BATTERY_MANAGEMENT_ENABLED, 1) == 1; + return buildFlag && runtimeFlag; + } + + boolean isPowerSaveWhitelistExceptIdleApp(String packageName) throws RemoteException { + return mDeviceIdleController.isPowerSaveWhitelistExceptIdleApp(packageName); + } + + File getDataSystemDirectory() { + return Environment.getDataSystemDirectory(); + } + + void noteEvent(int event, String packageName, int uid) throws RemoteException { + mBatteryStats.noteEvent(event, packageName, uid); + } + + boolean isPackageEphemeral(int userId, String packageName) { + return mPackageManagerInternal.isPackageEphemeral(userId, packageName); + } + + boolean isPackageInstalled(String packageName, int flags, int userId) { + return mPackageManagerInternal.getPackageUid(packageName, flags, userId) >= 0; + } + + int[] getRunningUserIds() throws RemoteException { + return ActivityManager.getService().getRunningUserIds(); + } + + boolean isDefaultDisplayOn() { + return mDisplayManager + .getDisplay(Display.DEFAULT_DISPLAY).getState() == Display.STATE_ON; + } + + void registerDisplayListener(DisplayManager.DisplayListener listener, Handler handler) { + mDisplayManager.registerDisplayListener(listener, handler); + } + + String getActiveNetworkScorer() { + NetworkScoreManager nsm = (NetworkScoreManager) mContext.getSystemService( + Context.NETWORK_SCORE_SERVICE); + return nsm.getActiveScorerPackage(); + } + + public boolean isBoundWidgetPackage(AppWidgetManager appWidgetManager, String packageName, + int userId) { + return appWidgetManager.isBoundWidgetPackage(packageName, userId); + } + + String getAppIdleSettings() { + return Global.getString(mContext.getContentResolver(), + Global.APP_IDLE_CONSTANTS); + } + + /** Whether the device is in doze or not. */ + public boolean isDeviceIdleMode() { + return mPowerManager.isDeviceIdleMode(); + } + } + + class AppStandbyHandler extends Handler { + + AppStandbyHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_INFORM_LISTENERS: + StandbyUpdateRecord r = (StandbyUpdateRecord) msg.obj; + informListeners(r.packageName, r.userId, r.bucket, r.reason, + r.isUserInteraction); + r.recycle(); + break; + + case MSG_FORCE_IDLE_STATE: + forceIdleState((String) msg.obj, msg.arg1, msg.arg2 == 1); + break; + + case MSG_CHECK_IDLE_STATES: + if (checkIdleStates(msg.arg1) && mAppIdleEnabled) { + mHandler.sendMessageDelayed(mHandler.obtainMessage( + MSG_CHECK_IDLE_STATES, msg.arg1, 0), + mCheckIdleIntervalMillis); + } + break; + + case MSG_ONE_TIME_CHECK_IDLE_STATES: + mHandler.removeMessages(MSG_ONE_TIME_CHECK_IDLE_STATES); + waitForAdminData(); + checkIdleStates(UserHandle.USER_ALL); + break; + + case MSG_REPORT_CONTENT_PROVIDER_USAGE: + SomeArgs args = (SomeArgs) msg.obj; + reportContentProviderUsage((String) args.arg1, // authority name + (String) args.arg2, // package name + (int) args.arg3); // userId + args.recycle(); + break; + + case MSG_CHECK_PACKAGE_IDLE_STATE: + checkAndUpdateStandbyState((String) msg.obj, msg.arg1, msg.arg2, + mInjector.elapsedRealtime()); + break; + + case MSG_REPORT_SYNC_SCHEDULED: + final boolean exempted = msg.arg1 > 0 ? true : false; + if (exempted) { + reportExemptedSyncScheduled((String) msg.obj, msg.arg1); + } else { + reportUnexemptedSyncScheduled((String) msg.obj, msg.arg1); + } + break; + + case MSG_REPORT_EXEMPTED_SYNC_START: + reportExemptedSyncStart((String) msg.obj, msg.arg1); + break; + + default: + super.handleMessage(msg); + break; + + } + } + }; + + private final NetworkRequest mNetworkRequest = new NetworkRequest.Builder().build(); + + private final ConnectivityManager.NetworkCallback mNetworkCallback + = new ConnectivityManager.NetworkCallback() { + @Override + public void onAvailable(Network network) { + mConnectivityManager.unregisterNetworkCallback(this); + } + }; + + private final DisplayManager.DisplayListener mDisplayListener + = new DisplayManager.DisplayListener() { + + @Override public void onDisplayAdded(int displayId) { + } + + @Override public void onDisplayRemoved(int displayId) { + } + + @Override public void onDisplayChanged(int displayId) { + if (displayId == Display.DEFAULT_DISPLAY) { + final boolean displayOn = isDisplayOn(); + synchronized (mAppIdleLock) { + mAppIdleHistory.updateDisplay(displayOn, mInjector.elapsedRealtime()); + } + } + } + }; + + /** + * Observe settings changes for {@link Global#APP_IDLE_CONSTANTS}. + */ + private class SettingsObserver extends ContentObserver { + private static final String KEY_SCREEN_TIME_THRESHOLDS = "screen_thresholds"; + private static final String KEY_ELAPSED_TIME_THRESHOLDS = "elapsed_thresholds"; + private static final String KEY_STRONG_USAGE_HOLD_DURATION = "strong_usage_duration"; + private static final String KEY_NOTIFICATION_SEEN_HOLD_DURATION = + "notification_seen_duration"; + private static final String KEY_SYSTEM_UPDATE_HOLD_DURATION = + "system_update_usage_duration"; + private static final String KEY_PREDICTION_TIMEOUT = "prediction_timeout"; + private static final String KEY_SYNC_ADAPTER_HOLD_DURATION = "sync_adapter_duration"; + private static final String KEY_EXEMPTED_SYNC_SCHEDULED_NON_DOZE_HOLD_DURATION = + "exempted_sync_scheduled_nd_duration"; + private static final String KEY_EXEMPTED_SYNC_SCHEDULED_DOZE_HOLD_DURATION = + "exempted_sync_scheduled_d_duration"; + private static final String KEY_EXEMPTED_SYNC_START_HOLD_DURATION = + "exempted_sync_start_duration"; + private static final String KEY_UNEXEMPTED_SYNC_SCHEDULED_HOLD_DURATION = + "unexempted_sync_scheduled_duration"; + private static final String KEY_SYSTEM_INTERACTION_HOLD_DURATION = + "system_interaction_duration"; + private static final String KEY_INITIAL_FOREGROUND_SERVICE_START_HOLD_DURATION = + "initial_foreground_service_start_duration"; + public static final long DEFAULT_STRONG_USAGE_TIMEOUT = 1 * ONE_HOUR; + public static final long DEFAULT_NOTIFICATION_TIMEOUT = 12 * ONE_HOUR; + public static final long DEFAULT_SYSTEM_UPDATE_TIMEOUT = 2 * ONE_HOUR; + public static final long DEFAULT_SYSTEM_INTERACTION_TIMEOUT = 10 * ONE_MINUTE; + public static final long DEFAULT_SYNC_ADAPTER_TIMEOUT = 10 * ONE_MINUTE; + public static final long DEFAULT_EXEMPTED_SYNC_SCHEDULED_NON_DOZE_TIMEOUT = 10 * ONE_MINUTE; + public static final long DEFAULT_EXEMPTED_SYNC_SCHEDULED_DOZE_TIMEOUT = 4 * ONE_HOUR; + public static final long DEFAULT_EXEMPTED_SYNC_START_TIMEOUT = 10 * ONE_MINUTE; + public static final long DEFAULT_UNEXEMPTED_SYNC_SCHEDULED_TIMEOUT = 10 * ONE_MINUTE; + public static final long DEFAULT_INITIAL_FOREGROUND_SERVICE_START_TIMEOUT = 30 * ONE_MINUTE; + + private final KeyValueListParser mParser = new KeyValueListParser(','); + + SettingsObserver(Handler handler) { + super(handler); + } + + void registerObserver() { + final ContentResolver cr = mContext.getContentResolver(); + cr.registerContentObserver(Global.getUriFor(Global.APP_IDLE_CONSTANTS), false, this); + cr.registerContentObserver(Global.getUriFor(Global.APP_STANDBY_ENABLED), false, this); + cr.registerContentObserver(Global.getUriFor(Global.ADAPTIVE_BATTERY_MANAGEMENT_ENABLED), + false, this); + } + + @Override + public void onChange(boolean selfChange) { + updateSettings(); + postOneTimeCheckIdleStates(); + } + + void updateSettings() { + if (DEBUG) { + Slog.d(TAG, + "appidle=" + Global.getString(mContext.getContentResolver(), + Global.APP_STANDBY_ENABLED)); + Slog.d(TAG, + "adaptivebat=" + Global.getString(mContext.getContentResolver(), + Global.ADAPTIVE_BATTERY_MANAGEMENT_ENABLED)); + Slog.d(TAG, "appidleconstants=" + Global.getString( + mContext.getContentResolver(), + Global.APP_IDLE_CONSTANTS)); + } + + // Look at global settings for this. + // TODO: Maybe apply different thresholds for different users. + try { + mParser.setString(mInjector.getAppIdleSettings()); + } catch (IllegalArgumentException e) { + Slog.e(TAG, "Bad value for app idle settings: " + e.getMessage()); + // fallthrough, mParser is empty and all defaults will be returned. + } + + synchronized (mAppIdleLock) { + + String screenThresholdsValue = mParser.getString(KEY_SCREEN_TIME_THRESHOLDS, null); + mAppStandbyScreenThresholds = parseLongArray(screenThresholdsValue, + SCREEN_TIME_THRESHOLDS); + + String elapsedThresholdsValue = mParser.getString(KEY_ELAPSED_TIME_THRESHOLDS, + null); + mAppStandbyElapsedThresholds = parseLongArray(elapsedThresholdsValue, + ELAPSED_TIME_THRESHOLDS); + mCheckIdleIntervalMillis = Math.min(mAppStandbyElapsedThresholds[1] / 4, + COMPRESS_TIME ? ONE_MINUTE : 4 * 60 * ONE_MINUTE); // 4 hours + mStrongUsageTimeoutMillis = mParser.getDurationMillis( + KEY_STRONG_USAGE_HOLD_DURATION, + COMPRESS_TIME ? ONE_MINUTE : DEFAULT_STRONG_USAGE_TIMEOUT); + mNotificationSeenTimeoutMillis = mParser.getDurationMillis( + KEY_NOTIFICATION_SEEN_HOLD_DURATION, + COMPRESS_TIME ? 12 * ONE_MINUTE : DEFAULT_NOTIFICATION_TIMEOUT); + mSystemUpdateUsageTimeoutMillis = mParser.getDurationMillis( + KEY_SYSTEM_UPDATE_HOLD_DURATION, + COMPRESS_TIME ? 2 * ONE_MINUTE : DEFAULT_SYSTEM_UPDATE_TIMEOUT); + mPredictionTimeoutMillis = mParser.getDurationMillis( + KEY_PREDICTION_TIMEOUT, + COMPRESS_TIME ? 10 * ONE_MINUTE : DEFAULT_PREDICTION_TIMEOUT); + mSyncAdapterTimeoutMillis = mParser.getDurationMillis( + KEY_SYNC_ADAPTER_HOLD_DURATION, + COMPRESS_TIME ? ONE_MINUTE : DEFAULT_SYNC_ADAPTER_TIMEOUT); + + mExemptedSyncScheduledNonDozeTimeoutMillis = mParser.getDurationMillis( + KEY_EXEMPTED_SYNC_SCHEDULED_NON_DOZE_HOLD_DURATION, + COMPRESS_TIME ? (ONE_MINUTE / 2) + : DEFAULT_EXEMPTED_SYNC_SCHEDULED_NON_DOZE_TIMEOUT); + + mExemptedSyncScheduledDozeTimeoutMillis = mParser.getDurationMillis( + KEY_EXEMPTED_SYNC_SCHEDULED_DOZE_HOLD_DURATION, + COMPRESS_TIME ? ONE_MINUTE + : DEFAULT_EXEMPTED_SYNC_SCHEDULED_DOZE_TIMEOUT); + + mExemptedSyncStartTimeoutMillis = mParser.getDurationMillis( + KEY_EXEMPTED_SYNC_START_HOLD_DURATION, + COMPRESS_TIME ? ONE_MINUTE + : DEFAULT_EXEMPTED_SYNC_START_TIMEOUT); + + mUnexemptedSyncScheduledTimeoutMillis = mParser.getDurationMillis( + KEY_UNEXEMPTED_SYNC_SCHEDULED_HOLD_DURATION, + COMPRESS_TIME ? ONE_MINUTE + : DEFAULT_UNEXEMPTED_SYNC_SCHEDULED_TIMEOUT); // TODO + + mSystemInteractionTimeoutMillis = mParser.getDurationMillis( + KEY_SYSTEM_INTERACTION_HOLD_DURATION, + COMPRESS_TIME ? ONE_MINUTE : DEFAULT_SYSTEM_INTERACTION_TIMEOUT); + + mInitialForegroundServiceStartTimeoutMillis = mParser.getDurationMillis( + KEY_INITIAL_FOREGROUND_SERVICE_START_HOLD_DURATION, + COMPRESS_TIME ? ONE_MINUTE : + DEFAULT_INITIAL_FOREGROUND_SERVICE_START_TIMEOUT); + } + + // Check if app_idle_enabled has changed. Do this after getting the rest of the settings + // in case we need to change something based on the new values. + setAppIdleEnabled(mInjector.isAppIdleEnabled()); + } + + long[] parseLongArray(String values, long[] defaults) { + if (values == null) return defaults; + if (values.isEmpty()) { + // Reset to defaults + return defaults; + } else { + String[] thresholds = values.split("/"); + if (thresholds.length == THRESHOLD_BUCKETS.length) { + long[] array = new long[THRESHOLD_BUCKETS.length]; + for (int i = 0; i < THRESHOLD_BUCKETS.length; i++) { + try { + if (thresholds[i].startsWith("P") || thresholds[i].startsWith("p")) { + array[i] = Duration.parse(thresholds[i]).toMillis(); + } else { + array[i] = Long.parseLong(thresholds[i]); + } + } catch (NumberFormatException|DateTimeParseException e) { + return defaults; + } + } + return array; + } else { + return defaults; + } + } + } + } +} diff --git a/apex/permission/Android.bp b/apex/permission/Android.bp new file mode 100644 index 000000000000..027481401557 --- /dev/null +++ b/apex/permission/Android.bp @@ -0,0 +1,33 @@ +// 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.permission", + + manifest: "apex_manifest.json", + + key: "com.android.permission.key", + certificate: ":com.android.permission.certificate", +} + +apex_key { + name: "com.android.permission.key", + public_key: "com.android.permission.avbpubkey", + private_key: "com.android.permission.pem", +} + +android_app_certificate { + name: "com.android.permission.certificate", + certificate: "com.android.permission", +} diff --git a/apex/permission/OWNERS b/apex/permission/OWNERS new file mode 100644 index 000000000000..957e10a582a0 --- /dev/null +++ b/apex/permission/OWNERS @@ -0,0 +1,6 @@ +svetoslavganov@google.com +moltmann@google.com +eugenesusla@google.com +zhanghai@google.com +evanseverson@google.com +ntmyren@google.com diff --git a/apex/permission/apex_manifest.json b/apex/permission/apex_manifest.json new file mode 100644 index 000000000000..2a8c4f737dff --- /dev/null +++ b/apex/permission/apex_manifest.json @@ -0,0 +1,4 @@ +{ + "name": "com.android.permission", + "version": 1 +} diff --git a/apex/permission/com.android.permission.avbpubkey b/apex/permission/com.android.permission.avbpubkey Binary files differnew file mode 100644 index 000000000000..9eaf85259637 --- /dev/null +++ b/apex/permission/com.android.permission.avbpubkey diff --git a/apex/permission/com.android.permission.pem b/apex/permission/com.android.permission.pem new file mode 100644 index 000000000000..3d584be5440d --- /dev/null +++ b/apex/permission/com.android.permission.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKgIBAAKCAgEA6snt4eqoz85xiL9Sf6w1S1b9FgSHK05zYTh2JYPvQKQ3yeZp +E6avJ6FN6XcbmkDzSd658BvUGDBSPhOlzuUO4BsoKBuLMxP6TxIQXFKidzDqY0vQ +4qkS++bdIhUjwBP3OSZ3Czu0BiihK8GC75Abr//EyCyObGIGGfHEGANiOgrpP4X5 ++OmLzQLCjk4iE1kg+U6cRSRI/XLaoWC0TvIIuzxznrQ6r5GmzgTOwyBWyIB+bj73 +bmsweHTU+w9Y7kGOx4hO3XCLIhoBWEw0EbuW9nZmQ4sZls5Jo/CbyJlCclF11yVo +SCf2LG/T+9pah5NOmDQ1kPbU+0iKZIV4YFHGTIhyGDE/aPOuUT05ziCGDifgHr0u +SG1x/RLqsVh/POvNxnvP9cQFMQ08BvbEJaTTgB785iwKsvdqCfmng/SAyxSetmzP +StXVB3fh1OoZ8vunRbQYxnmUxycVqaA96zmBx2wLvbvzKo7pZFDE6nbhnT5+MRAM +z/VIK89W26uB4gj8sBFslqZjT0jPqsAZuvDm7swOtMwIcEolyGJuFLqlhN7UwMz2 +9y8+IpYixR+HvD1TZI9NtmuCmv3kPrWgoMZg6yvaBayTIr8RdYzi6FO/C1lLiraz +48dH3sXWRa8cgw6VcSUwYrEBIc3sotdsupO1iOjcFybIwaee0YTZJfjvbqkCAwEA +AQKCAgEArRnfdpaJi1xLPGTCMDsIt9kUku0XswgN7PmxsYsKFAB+2S40/jYAIRm9 +1YjpItsMA8RgFfSOdJ77o6TctCMQyo17F8bm4+uwuic5RLfv7Cx2QmsdQF8jDfFx +y7UGPJD7znjbf76uxXOjEB2FqZX3s9TAgkzHXIUQtoQW7RVhkCWHPjxKxgd5+NY2 +FrDoUpd9xhD9CcTsw1+wbRZdGW88nL6/B50dP2AFORM2VYo8MWr6y9FEn3YLsGOC +uu7fxBk1aUrHyl81VRkTMMROB1zkuiUk1FtzrEm+5U15rXXBFYOVe9+qeLhtuOlh +wueDoz0pzvF/JLe24uTik6YL0Ae6SD0pFXQ2KDrdH3cUHLok3r76/yGzaDNTFjS2 +2WbQ8dEJV8veNHk8gjGpFTJIsBUlcZpmUCDHlfvVMb3+2ahQ+28piQUt5t3zqJdZ +NDqsOHzY6BRPc+Wm85Xii/lWiQceZSee/b1Enu+nchsyXhSenBfC6bIGZReyMI0K +KKKuVhyR6OSOiR5ZdZ/NyXGqsWy05fn/h0X9hnpETsNaNYNKWvpHLfKll+STJpf7 +AZquJPIclQyiq5NONx6kfPztoCLkKV/zOgIj3Sx5oSZq+5gpO91nXWVwkTbqK1d1 +004q2Mah6UQyAk1XGQc2pHx7ouVcWawjU30vZ4C015Hv2lm/gVkCggEBAPltATYS +OqOSL1YAtIHPiHxMjNAgUdglq8JiJFXVfkocGU9eNub3Ed3sSWu6GB9Myu/sSKje +bJ5DZqxJnvB2Fqmu9I9OunLGFSD0aXs4prwsQ1Rm5FcbImtrxcciASdkoo8Pj0z4 +vk2r2NZD3VtER5Uh+YjSDkxcS9gBStXUpCL6gj69UpOxMmWqZVjyHatVB4lEvYJl +N82uT7N7QVNL1DzcZ9z4C4r7ks1Pm7ka12s5m/oaAlAMdVeofiPJe1xA9zRToSr4 +tIbMkOeXFLVRLuji/7XsOgal5Rl59p+OwLshX5cswPVOMrH6zt+hbsJ5q8M5dqnX +VAOBK7KNQ/EKZwcCggEBAPD6KVvyCim46n5EbcEqCkO7gevwZkw/9vLwmM5YsxTh +z9FQkPO0iB7mwbX8w04I91Pre4NdfcgMG0pP1b13Sb4KHBchqW1a+TCs3kSGC6gn +1SxmXHnA9jRxAkrWlGkoAQEz+aP61cXiiy2tXpQwJ8xQCKprfoqWZwhkCtEVU6CE +S7v9cscOHIqgNxx4WoceMmq4EoihHAZzHxTcNVbByckMjb2XQJ0iNw3lDP4ddvc+ +a4HzHfHkhzeQ5ZNc8SvWU8z80aSCOKRsSD3aUTZzxhZ4O2tZSW7v7p+FpvVee7bC +g8YCfszTdpVUMlLRLjScimAcovcFLSvtyupinxWg4M8CggEAN9YGEmOsSte7zwXj +YrfhtumwEBtcFwX/2Ej+F1Tuq4p0xAa0RaoDjumJWhtTsRYQy/raHSuFpzwxbNoi +QXQ+CIhI6RfXtz/OlQ0B2/rHoJJMFEXgUfuaDfAXW0eqeHYXyezSyIlamKqipPyW +Pgsf9yue39keKEv1EorfhNTQVaA8rezV4oglXwrxGyNALw2e3UTNI7ai8mFWKDis +XAg6n9E7UwUYGGnO6DUtCBgRJ0jDOQ6/e8n+LrxiWIKPIgzNCiK6jpMUXqTGv4Fb +umdNGAdQ9RnHt5tFmRlrczaSwJFtA7uaCpAR2zPpQbiywchZAiAIB2dTwGEXNiZX +kksg2wKCAQEA6pNad3qhkgPDoK6T+Jkn7M82paoaqtcJWWwEE7oceZNnbWZz9Agl +CY+vuawXonrv5+0vCq2Tp4zBdBFLC2h3jFrjBVFrUFxifpOIukOSTVqZFON/2bWQ +9XOcu6UuSz7522Xw+UNPnZXtzcUacD6AP08ZYGvLfrTyDyTzspyED5k48ALEHCkM +d5WGkFxII4etpF0TDZVnZo/iDbhe49k4yFFEGO6Ho26PESOLBkNAb2V/2bwDxlij +l9+g21Z6HiZA5SamHPH2mXgeyrcen1cL2QupK9J6vVcqfnboE6qp2zp2c+Yx8MlY +gfy4EA44YFaSDQVTTgrn8f9Eq+zc130H2QKCAQEAqOKgv68nIPdDSngNyCVyWego +boFiDaEJoBBg8FrBjTJ6wFLrNAnXmbvfTtgNmNAzF1cUPJZlIIsHgGrMCfpehbXq +WQQIw+E+yFbTGLxseGRfsLrV0CsgnAoOVeod+yIHmqc3livaUbrWhL1V2f6Ue+sE +7YLp/iP43NaMfA4kYk2ep7+ZJoEVkCjHJJaHWgAG3RynPJHkTJlSgu7wLYvGc9uE +ZsEFUM46lX02t7rrtMfasVGrUy1c2xOxFb4v1vG6iEZ7+YWeq5o3AkxUwEGn+mG4 +/3p+k4AaTXJDXgyZ0Sv6CkGuPHenAYG4cswcUUEf/G4Ag77x6LBNMgycJBxUJA== +-----END RSA PRIVATE KEY----- diff --git a/apex/permission/com.android.permission.pk8 b/apex/permission/com.android.permission.pk8 Binary files differnew file mode 100644 index 000000000000..d51673dbc2fc --- /dev/null +++ b/apex/permission/com.android.permission.pk8 diff --git a/apex/permission/com.android.permission.x509.pem b/apex/permission/com.android.permission.x509.pem new file mode 100644 index 000000000000..4b146c9edd4f --- /dev/null +++ b/apex/permission/com.android.permission.x509.pem @@ -0,0 +1,35 @@ +-----BEGIN CERTIFICATE----- +MIIGKzCCBBOgAwIBAgIUezo3fQeVZsmLpm/dkpGWJ/G/MN8wDQYJKoZIhvcNAQEL +BQAwgaMxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQH +DA1Nb3VudGFpbiBWaWV3MRAwDgYDVQQKDAdBbmRyb2lkMRAwDgYDVQQLDAdBbmRy +b2lkMR8wHQYDVQQDDBZjb20uYW5kcm9pZC5wZXJtaXNzaW9uMSIwIAYJKoZIhvcN +AQkBFhNhbmRyb2lkQGFuZHJvaWQuY29tMCAXDTE5MTAwOTIxMzExOVoYDzQ3NTcw +OTA0MjEzMTE5WjCBozELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWEx +FjAUBgNVBAcMDU1vdW50YWluIFZpZXcxEDAOBgNVBAoMB0FuZHJvaWQxEDAOBgNV +BAsMB0FuZHJvaWQxHzAdBgNVBAMMFmNvbS5hbmRyb2lkLnBlcm1pc3Npb24xIjAg +BgkqhkiG9w0BCQEWE2FuZHJvaWRAYW5kcm9pZC5jb20wggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCxefguRJ7E6tBCTEOeU2HJEGs6AQQapLz9hMed0aaJ +Qr7aTQiYJEk+sG4+jPYbjpxa8JDDzJHp+4g7DjfSb+dvT9n84A8lWaI/yRXTZTQN +Hu5m/bgHhi0LbySpiaFyodXBKUAnOhZyGPtYjtBFywFylueub8ryc1Z6UxxU7udH +1mkIr7sE48Qkq5SyjFROE96iFmYA+vS/JXOfS0NBHiMB4GBxx4V7kXpvrTI7hhZG +HiyhKvNh7wyHIhO9nDEw1rwtAH6CsL3YkQEVBeAU98m+0Au+qStLYkKHh2l8zT4W +7sVK1VSqfB+VqOUmeIGdzlBfqMsoXD+FJz6KnIdUHIwjFDjL7Xr+hd+7xve+Q3S+ +U3Blk/U6atY8PM09wNfilG+SvwcKk5IgriDcu3rWKgIFxbUUaxLrDW7pLlu6wt/d +GGtKK+Bc0jF+9Z901Tl33i5xhc5yOktT0btkKs7lSeE6VzP/Nk5g0SuzixmuRoh9 +f5Ge41N2ZCEHNXx3wZeVZwHIIPfYrL7Yql1Xoxbfs4ETFk6ChzVQcvjfDQQuK58J +uNc+TOCoI/qflXwGCwpuHl0ier8V5Z4tpMUl5rWyVR/QGRtLPvs2lLuxczDw1OXq +wEVtCMn9aNnd4y7R9PZ52hi53HAvDjpWefrLYi+Q04J6iGFQ1qAFBClK9DquBvmR +swIDAQABo1MwUTAdBgNVHQ4EFgQULpfus5s5SrqLkoUKyPXA0D1iHPMwHwYDVR0j +BBgwFoAULpfus5s5SrqLkoUKyPXA0D1iHPMwDwYDVR0TAQH/BAUwAwEB/zANBgkq +hkiG9w0BAQsFAAOCAgEAjxQG5EFv8V/9yV2glI53VOmlWMjfEgvUjd39s/XLyPlr +OzPOKSB0NFo8To3l4l+MsManxPK8y0OyfEVKbWVz9onv0ovo5MVokBmV/2G0jmsV +B4e9yjOq+DmqIvY/Qh63Ywb97sTgcFI8620MhQDbh2IpEGv4ZNV0H6rgXmgdSCBw +1EjBoYfFpN5aMgZjeyzZcq+d1IapdWqdhuEJQkMvoYS4WIumNIJlEXPQRoq/F5Ih +nszdbKI/jVyiGFa2oeZ3rja1Y6GCRU8TYEoKx1pjS8uQDOEDTwsG/QnUe9peEj0V +SsCkIidJWTomAmq9Tub9vpBe1zuTpuRAwxwR0qwgSxozV1Mvow1dJ19oFtHX0yD6 +ZjCpRn5PW9kMvSWSlrcrFs1NJf0j1Cvf7bHpkEDqLqpMnnh9jaFQq3nzDY+MWcIR +jDcgQpI+AiE2/qtauZnFEVhbce49nCnk9+5bpTTIZJdzqeaExe5KXHwEtZLaEDh4 +atLY9LuEvPsjmDIMOR6hycD9FvwGXhJOQBjESIWFwigtSb1Yud9n6201jw3MLJ4k ++WhkbmZgWy+xc+Mdm5H3XyB1lvHaHGkxu+QB9KyQuVQKwbUVcbwZIfTFPN6Zr/dS +ZXJqAbBhG/dBgF0LazuLaPVpibi+a3Y+tb9b8eXGkz4F97PWZIEDkELQ+9KOvhc= +-----END CERTIFICATE----- diff --git a/apex/statsd/.clang-format b/apex/statsd/.clang-format new file mode 100644 index 000000000000..cead3a079435 --- /dev/null +++ b/apex/statsd/.clang-format @@ -0,0 +1,17 @@ +BasedOnStyle: Google +AllowShortIfStatementsOnASingleLine: true +AllowShortFunctionsOnASingleLine: false +AllowShortLoopsOnASingleLine: true +BinPackArguments: true +BinPackParameters: true +ColumnLimit: 100 +CommentPragmas: NOLINT:.* +ContinuationIndentWidth: 8 +DerivePointerAlignment: false +IndentWidth: 4 +PointerAlignment: Left +TabWidth: 4 +AccessModifierOffset: -4 +IncludeCategories: + - Regex: '^"Log\.h"' + Priority: -1 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/OWNERS b/apex/statsd/OWNERS new file mode 100644 index 000000000000..bed9600bc955 --- /dev/null +++ b/apex/statsd/OWNERS @@ -0,0 +1,9 @@ +jeffreyhuang@google.com +joeo@google.com +jtnguyen@google.com +muhammadq@google.com +ruchirr@google.com +singhtejinder@google.com +tsaichristine@google.com +yaochen@google.com +yro@google.com
\ No newline at end of file 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----- diff --git a/apex/statsd/service/Android.bp b/apex/statsd/service/Android.bp new file mode 100644 index 000000000000..786e8b0260f8 --- /dev/null +++ b/apex/statsd/service/Android.bp @@ -0,0 +1,16 @@ +// Statsd Service jar, which will eventually be put in the statsd mainline apex. +// statsd-service needs to be added to PRODUCT_SYSTEM_SERVER_JARS. +// This jar will contain StatsCompanionService +java_library { + name: "statsd-service", + installable: true, + + srcs: [ + "java/**/*.java", + ], + + libs: [ + "framework", + "services.core", + ], +}
\ No newline at end of file diff --git a/apex/statsd/service/java/com/android/server/stats/StatsCompanionService.java b/apex/statsd/service/java/com/android/server/stats/StatsCompanionService.java new file mode 100644 index 000000000000..8a00c8318d58 --- /dev/null +++ b/apex/statsd/service/java/com/android/server/stats/StatsCompanionService.java @@ -0,0 +1,2876 @@ +/* + * 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.stats; + +import static android.app.AppOpsManager.OP_FLAGS_ALL_TRUSTED; +import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED; +import static android.content.pm.PermissionInfo.PROTECTION_DANGEROUS; +import static android.os.Process.getUidForPid; +import static android.os.storage.VolumeInfo.TYPE_PRIVATE; +import static android.os.storage.VolumeInfo.TYPE_PUBLIC; + +import static com.android.internal.util.Preconditions.checkNotNull; +import static com.android.server.am.MemoryStatUtil.readMemoryStatFromFilesystem; +import static com.android.server.stats.IonMemoryUtil.readProcessSystemIonHeapSizesFromDebugfs; +import static com.android.server.stats.IonMemoryUtil.readSystemIonHeapSizeFromDebugfs; +import static com.android.server.stats.ProcfsMemoryUtil.forEachPid; +import static com.android.server.stats.ProcfsMemoryUtil.readCmdlineFromProcfs; +import static com.android.server.stats.ProcfsMemoryUtil.readMemorySnapshotFromProcfs; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.ActivityManagerInternal; +import android.app.AlarmManager; +import android.app.AlarmManager.OnAlarmListener; +import android.app.AppOpsManager; +import android.app.AppOpsManager.HistoricalOps; +import android.app.AppOpsManager.HistoricalOpsRequest; +import android.app.AppOpsManager.HistoricalPackageOps; +import android.app.AppOpsManager.HistoricalUidOps; +import android.app.ProcessMemoryState; +import android.app.StatsManager; +import android.bluetooth.BluetoothActivityEnergyInfo; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.UidTraffic; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.IntentSender; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PermissionInfo; +import android.content.pm.UserInfo; +import android.hardware.biometrics.BiometricsProtoEnums; +import android.hardware.face.FaceManager; +import android.hardware.fingerprint.FingerprintManager; +import android.net.ConnectivityManager; +import android.net.INetworkStatsService; +import android.net.Network; +import android.net.NetworkRequest; +import android.net.NetworkStats; +import android.net.wifi.IWifiManager; +import android.net.wifi.WifiActivityEnergyInfo; +import android.os.BatteryStats; +import android.os.BatteryStatsInternal; +import android.os.Binder; +import android.os.Build; +import android.os.Bundle; +import android.os.CoolingDevice; +import android.os.Environment; +import android.os.FileUtils; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.IPullAtomCallback; +import android.os.IStatsCompanionService; +import android.os.IStatsManager; +import android.os.IStoraged; +import android.os.IThermalEventListener; +import android.os.IThermalService; +import android.os.Looper; +import android.os.ParcelFileDescriptor; +import android.os.Parcelable; +import android.os.Process; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.StatFs; +import android.os.StatsDimensionsValue; +import android.os.StatsLogEventWrapper; +import android.os.SynchronousResultReceiver; +import android.os.SystemClock; +import android.os.SystemProperties; +import android.os.Temperature; +import android.os.UserHandle; +import android.os.UserManager; +import android.os.storage.DiskInfo; +import android.os.storage.StorageManager; +import android.os.storage.VolumeInfo; +import android.provider.Settings; +import android.stats.storage.StorageEnums; +import android.telephony.ModemActivityInfo; +import android.telephony.TelephonyManager; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Log; +import android.util.Slog; +import android.util.StatsLog; +import android.util.proto.ProtoOutputStream; +import android.util.proto.ProtoStream; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.app.procstats.IProcessStats; +import com.android.internal.app.procstats.ProcessStats; +import com.android.internal.os.BackgroundThread; +import com.android.internal.os.BatterySipper; +import com.android.internal.os.BatteryStatsHelper; +import com.android.internal.os.BinderCallsStats.ExportedCallStat; +import com.android.internal.os.KernelCpuSpeedReader; +import com.android.internal.os.KernelCpuThreadReader; +import com.android.internal.os.KernelCpuThreadReaderDiff; +import com.android.internal.os.KernelCpuThreadReaderSettingsObserver; +import com.android.internal.os.KernelCpuUidTimeReader.KernelCpuUidActiveTimeReader; +import com.android.internal.os.KernelCpuUidTimeReader.KernelCpuUidClusterTimeReader; +import com.android.internal.os.KernelCpuUidTimeReader.KernelCpuUidFreqTimeReader; +import com.android.internal.os.KernelCpuUidTimeReader.KernelCpuUidUserSysTimeReader; +import com.android.internal.os.KernelWakelockReader; +import com.android.internal.os.KernelWakelockStats; +import com.android.internal.os.LooperStats; +import com.android.internal.os.PowerProfile; +import com.android.internal.os.ProcessCpuTracker; +import com.android.internal.os.StoragedUidIoStatsReader; +import com.android.internal.util.DumpUtils; +import com.android.server.BinderCallsStatsService; +import com.android.server.LocalServices; +import com.android.server.SystemService; +import com.android.server.SystemServiceManager; +import com.android.server.am.MemoryStatUtil.MemoryStat; +import com.android.server.role.RoleManagerInternal; +import com.android.server.stats.IonMemoryUtil.IonAllocations; +import com.android.server.stats.ProcfsMemoryUtil.MemorySnapshot; +import com.android.server.storage.DiskStatsFileLogger; +import com.android.server.storage.DiskStatsLoggingService; + +import com.google.android.collect.Sets; + +import libcore.io.IoUtils; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * Helper service for statsd (the native stats management service in cmds/statsd/). + * Used for registering and receiving alarms on behalf of statsd. + * + * @hide + */ +public class StatsCompanionService extends IStatsCompanionService.Stub { + /** + * How long to wait on an individual subsystem to return its stats. + */ + private static final long EXTERNAL_STATS_SYNC_TIMEOUT_MILLIS = 2000; + private static final long MILLIS_IN_A_DAY = TimeUnit.DAYS.toMillis(1); + + public static final String RESULT_RECEIVER_CONTROLLER_KEY = "controller_activity"; + public static final String CONFIG_DIR = "/data/misc/stats-service"; + + static final String TAG = "StatsCompanionService"; + static final boolean DEBUG = false; + /** + * Hard coded field ids of frameworks/base/cmds/statsd/src/uid_data.proto + * to be used in ProtoOutputStream. + */ + private static final int APPLICATION_INFO_FIELD_ID = 1; + private static final int UID_FIELD_ID = 1; + private static final int VERSION_FIELD_ID = 2; + private static final int VERSION_STRING_FIELD_ID = 3; + private static final int PACKAGE_NAME_FIELD_ID = 4; + private static final int INSTALLER_FIELD_ID = 5; + + public static final int CODE_DATA_BROADCAST = 1; + public static final int CODE_SUBSCRIBER_BROADCAST = 1; + public static final int CODE_ACTIVE_CONFIGS_BROADCAST = 1; + /** + * The last report time is provided with each intent registered to + * StatsManager#setFetchReportsOperation. This allows easy de-duping in the receiver if + * statsd is requesting the client to retrieve the same statsd data. The last report time + * corresponds to the last_report_elapsed_nanos that will provided in the current + * ConfigMetricsReport, and this timestamp also corresponds to the + * current_report_elapsed_nanos of the most recently obtained ConfigMetricsReport. + */ + public static final String EXTRA_LAST_REPORT_TIME = "android.app.extra.LAST_REPORT_TIME"; + public static final int DEATH_THRESHOLD = 10; + /** + * Which native processes to snapshot memory for. + * + * <p>Processes are matched by their cmdline in procfs. Example: cat /proc/pid/cmdline returns + * /system/bin/statsd for the stats daemon. + */ + private static final Set<String> MEMORY_INTERESTING_NATIVE_PROCESSES = Sets.newHashSet( + "/system/bin/statsd", // Stats daemon. + "/system/bin/surfaceflinger", + "/system/bin/apexd", // APEX daemon. + "/system/bin/audioserver", + "/system/bin/cameraserver", + "/system/bin/drmserver", + "/system/bin/healthd", + "/system/bin/incidentd", + "/system/bin/installd", + "/system/bin/lmkd", // Low memory killer daemon. + "/system/bin/logd", + "media.codec", + "media.extractor", + "media.metrics", + "/system/bin/mediadrmserver", + "/system/bin/mediaserver", + "/system/bin/performanced", + "/system/bin/tombstoned", + "/system/bin/traced", // Perfetto. + "/system/bin/traced_probes", // Perfetto. + "webview_zygote", + "zygote", + "zygote64"); + /** + * Lowest available uid for apps. + * + * <p>Used to quickly discard memory snapshots of the zygote forks from native process + * measurements. + */ + private static final int MIN_APP_UID = 10_000; + + private static final int CPU_TIME_PER_THREAD_FREQ_MAX_NUM_FREQUENCIES = 8; + + static final class CompanionHandler extends Handler { + CompanionHandler(Looper looper) { + super(looper); + } + } + + private final Context mContext; + private final AlarmManager mAlarmManager; + private final INetworkStatsService mNetworkStatsService; + @GuardedBy("sStatsdLock") + private static IStatsManager sStatsd; + private static final Object sStatsdLock = new Object(); + + private final OnAlarmListener mAnomalyAlarmListener = new AnomalyAlarmListener(); + private final OnAlarmListener mPullingAlarmListener = new PullingAlarmListener(); + private final OnAlarmListener mPeriodicAlarmListener = new PeriodicAlarmListener(); + private final BroadcastReceiver mAppUpdateReceiver; + private final BroadcastReceiver mUserUpdateReceiver; + private final ShutdownEventReceiver mShutdownEventReceiver; + + private static final class PullerKey { + private final int mUid; + private final int mAtomTag; + + PullerKey(int uid, int atom) { + mUid = uid; + mAtomTag = atom; + } + + public int getUid() { + return mUid; + } + + public int getAtom() { + return mAtomTag; + } + + @Override + public int hashCode() { + return Objects.hash(mUid, mAtomTag); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof PullerKey) { + PullerKey other = (PullerKey) obj; + return this.mUid == other.getUid() && this.mAtomTag == other.getAtom(); + } + return false; + } + } + + private static final class PullerValue { + private final long mCoolDownNs; + private final long mTimeoutNs; + private int[] mAdditiveFields; + private IPullAtomCallback mCallback; + + PullerValue(long coolDownNs, long timeoutNs, int[] additiveFields, + IPullAtomCallback callback) { + mCoolDownNs = coolDownNs; + mTimeoutNs = timeoutNs; + mAdditiveFields = additiveFields; + mCallback = callback; + } + + public long getCoolDownNs() { + return mCoolDownNs; + } + + public long getTimeoutNs() { + return mTimeoutNs; + } + + public int[] getAdditiveFields() { + return mAdditiveFields; + } + + public IPullAtomCallback getCallback() { + return mCallback; + } + } + + private final HashMap<PullerKey, PullerValue> mPullers = new HashMap<>(); + + private final KernelWakelockReader mKernelWakelockReader = new KernelWakelockReader(); + private final KernelWakelockStats mTmpWakelockStats = new KernelWakelockStats(); + private IWifiManager mWifiManager = null; + private TelephonyManager mTelephony = null; + @GuardedBy("sStatsdLock") + private final HashSet<Long> mDeathTimeMillis = new HashSet<>(); + @GuardedBy("sStatsdLock") + private final HashMap<Long, String> mDeletedFiles = new HashMap<>(); + private final CompanionHandler mHandler; + + // Disables throttler on CPU time readers. + private KernelCpuUidUserSysTimeReader mCpuUidUserSysTimeReader = + new KernelCpuUidUserSysTimeReader(false); + private KernelCpuSpeedReader[] mKernelCpuSpeedReaders; + private KernelCpuUidFreqTimeReader mCpuUidFreqTimeReader = + new KernelCpuUidFreqTimeReader(false); + private KernelCpuUidActiveTimeReader mCpuUidActiveTimeReader = + new KernelCpuUidActiveTimeReader(false); + private KernelCpuUidClusterTimeReader mCpuUidClusterTimeReader = + new KernelCpuUidClusterTimeReader(false); + private StoragedUidIoStatsReader mStoragedUidIoStatsReader = + new StoragedUidIoStatsReader(); + @Nullable + private final KernelCpuThreadReaderDiff mKernelCpuThreadReader; + + private long mDebugElapsedClockPreviousValue = 0; + private long mDebugElapsedClockPullCount = 0; + private long mDebugFailingElapsedClockPreviousValue = 0; + private long mDebugFailingElapsedClockPullCount = 0; + private BatteryStatsHelper mBatteryStatsHelper = null; + private static final int MAX_BATTERY_STATS_HELPER_FREQUENCY_MS = 1000; + private long mBatteryStatsHelperTimestampMs = -MAX_BATTERY_STATS_HELPER_FREQUENCY_MS; + + private static IThermalService sThermalService; + private File mBaseDir = + new File(SystemServiceManager.ensureSystemDir(), "stats_companion"); + @GuardedBy("this") + ProcessCpuTracker mProcessCpuTracker = null; + + public StatsCompanionService(Context context) { + super(); + mContext = context; + mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); + mNetworkStatsService = INetworkStatsService.Stub.asInterface( + ServiceManager.getService(Context.NETWORK_STATS_SERVICE)); + mBaseDir.mkdirs(); + mAppUpdateReceiver = new AppUpdateReceiver(); + mUserUpdateReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + synchronized (sStatsdLock) { + if (sStatsd == null) { + Slog.w(TAG, "Could not access statsd for UserUpdateReceiver"); + return; + } + try { + // Pull the latest state of UID->app name, version mapping. + // Needed since the new user basically has a version of every app. + informAllUidsLocked(context); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to inform statsd latest update of all apps", e); + forgetEverythingLocked(); + } + } + } + }; + mShutdownEventReceiver = new ShutdownEventReceiver(); + if (DEBUG) Slog.d(TAG, "Registered receiver for ACTION_PACKAGE_REPLACED and ADDED."); + PowerProfile powerProfile = new PowerProfile(context); + final int numClusters = powerProfile.getNumCpuClusters(); + mKernelCpuSpeedReaders = new KernelCpuSpeedReader[numClusters]; + int firstCpuOfCluster = 0; + for (int i = 0; i < numClusters; i++) { + final int numSpeedSteps = powerProfile.getNumSpeedStepsInCpuCluster(i); + mKernelCpuSpeedReaders[i] = new KernelCpuSpeedReader(firstCpuOfCluster, + numSpeedSteps); + firstCpuOfCluster += powerProfile.getNumCoresInCpuCluster(i); + } + + // Enable push notifications of throttling from vendor thermal + // management subsystem via thermalservice. + IBinder b = ServiceManager.getService("thermalservice"); + + if (b != null) { + sThermalService = IThermalService.Stub.asInterface(b); + try { + sThermalService.registerThermalEventListener( + new ThermalEventListener()); + Slog.i(TAG, "register thermal listener successfully"); + } catch (RemoteException e) { + // Should never happen. + Slog.e(TAG, "register thermal listener error"); + } + } else { + Slog.e(TAG, "cannot find thermalservice, no throttling push notifications"); + } + + // Default NetworkRequest should cover all transport types. + final NetworkRequest request = new NetworkRequest.Builder().build(); + final ConnectivityManager connectivityManager = + (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); + connectivityManager.registerNetworkCallback(request, new ConnectivityStatsCallback()); + + HandlerThread handlerThread = new HandlerThread(TAG); + handlerThread.start(); + mHandler = new CompanionHandler(handlerThread.getLooper()); + + mKernelCpuThreadReader = + KernelCpuThreadReaderSettingsObserver.getSettingsModifiedReader(mContext); + } + + @Override + public void sendDataBroadcast(IBinder intentSenderBinder, long lastReportTimeNs) { + enforceCallingPermission(); + IntentSender intentSender = new IntentSender(intentSenderBinder); + Intent intent = new Intent(); + intent.putExtra(EXTRA_LAST_REPORT_TIME, lastReportTimeNs); + try { + intentSender.sendIntent(mContext, CODE_DATA_BROADCAST, intent, null, null); + } catch (IntentSender.SendIntentException e) { + Slog.w(TAG, "Unable to send using IntentSender"); + } + } + + @Override + public void sendActiveConfigsChangedBroadcast(IBinder intentSenderBinder, long[] configIds) { + enforceCallingPermission(); + IntentSender intentSender = new IntentSender(intentSenderBinder); + Intent intent = new Intent(); + intent.putExtra(StatsManager.EXTRA_STATS_ACTIVE_CONFIG_KEYS, configIds); + try { + intentSender.sendIntent(mContext, CODE_ACTIVE_CONFIGS_BROADCAST, intent, null, null); + if (DEBUG) { + Slog.d(TAG, "Sent broadcast with config ids " + Arrays.toString(configIds)); + } + } catch (IntentSender.SendIntentException e) { + Slog.w(TAG, "Unable to send active configs changed broadcast using IntentSender"); + } + } + + @Override + public void sendSubscriberBroadcast(IBinder intentSenderBinder, long configUid, long configKey, + long subscriptionId, long subscriptionRuleId, String[] cookies, + StatsDimensionsValue dimensionsValue) { + enforceCallingPermission(); + IntentSender intentSender = new IntentSender(intentSenderBinder); + Intent intent = + new Intent() + .putExtra(StatsManager.EXTRA_STATS_CONFIG_UID, configUid) + .putExtra(StatsManager.EXTRA_STATS_CONFIG_KEY, configKey) + .putExtra(StatsManager.EXTRA_STATS_SUBSCRIPTION_ID, subscriptionId) + .putExtra(StatsManager.EXTRA_STATS_SUBSCRIPTION_RULE_ID, subscriptionRuleId) + .putExtra(StatsManager.EXTRA_STATS_DIMENSIONS_VALUE, dimensionsValue); + + ArrayList<String> cookieList = new ArrayList<>(cookies.length); + for (String cookie : cookies) { + cookieList.add(cookie); + } + intent.putStringArrayListExtra( + StatsManager.EXTRA_STATS_BROADCAST_SUBSCRIBER_COOKIES, cookieList); + + if (DEBUG) { + Slog.d(TAG, + String.format("Statsd sendSubscriberBroadcast with params {%d %d %d %d %s %s}", + configUid, configKey, subscriptionId, subscriptionRuleId, + Arrays.toString(cookies), + dimensionsValue)); + } + try { + intentSender.sendIntent(mContext, CODE_SUBSCRIBER_BROADCAST, intent, null, null); + } catch (IntentSender.SendIntentException e) { + Slog.w(TAG, + "Unable to send using IntentSender from uid " + configUid + + "; presumably it had been cancelled."); + } + } + + private final static int[] toIntArray(List<Integer> list) { + int[] ret = new int[list.size()]; + for (int i = 0; i < ret.length; i++) { + ret[i] = list.get(i); + } + return ret; + } + + private final static long[] toLongArray(List<Long> list) { + long[] ret = new long[list.size()]; + for (int i = 0; i < ret.length; i++) { + ret[i] = list.get(i); + } + return ret; + } + + // Assumes that sStatsdLock is held. + @GuardedBy("sStatsdLock") + private final void informAllUidsLocked(Context context) throws RemoteException { + UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE); + PackageManager pm = context.getPackageManager(); + final List<UserInfo> users = um.getUsers(true); + if (DEBUG) { + Slog.d(TAG, "Iterating over " + users.size() + " profiles."); + } + + ParcelFileDescriptor[] fds; + try { + fds = ParcelFileDescriptor.createPipe(); + } catch (IOException e) { + Slog.e(TAG, "Failed to create a pipe to send uid map data.", e); + return; + } + sStatsd.informAllUidData(fds[0]); + try { + fds[0].close(); + } catch (IOException e) { + Slog.e(TAG, "Failed to close the read side of the pipe.", e); + } + final ParcelFileDescriptor writeFd = fds[1]; + BackgroundThread.getHandler().post(() -> { + FileOutputStream fout = new ParcelFileDescriptor.AutoCloseOutputStream(writeFd); + try { + ProtoOutputStream output = new ProtoOutputStream(fout); + int numRecords = 0; + // Add in all the apps for every user/profile. + for (UserInfo profile : users) { + List<PackageInfo> pi = + pm.getInstalledPackagesAsUser(PackageManager.MATCH_KNOWN_PACKAGES, + profile.id); + for (int j = 0; j < pi.size(); j++) { + if (pi.get(j).applicationInfo != null) { + String installer; + try { + installer = pm.getInstallerPackageName(pi.get(j).packageName); + } catch (IllegalArgumentException e) { + installer = ""; + } + long applicationInfoToken = + output.start(ProtoStream.FIELD_TYPE_MESSAGE + | ProtoStream.FIELD_COUNT_REPEATED + | APPLICATION_INFO_FIELD_ID); + output.write(ProtoStream.FIELD_TYPE_INT32 + | ProtoStream.FIELD_COUNT_SINGLE | UID_FIELD_ID, + pi.get(j).applicationInfo.uid); + output.write(ProtoStream.FIELD_TYPE_INT64 + | ProtoStream.FIELD_COUNT_SINGLE + | VERSION_FIELD_ID, pi.get(j).getLongVersionCode()); + output.write(ProtoStream.FIELD_TYPE_STRING + | ProtoStream.FIELD_COUNT_SINGLE | VERSION_STRING_FIELD_ID, + pi.get(j).versionName); + output.write(ProtoStream.FIELD_TYPE_STRING + | ProtoStream.FIELD_COUNT_SINGLE + | PACKAGE_NAME_FIELD_ID, pi.get(j).packageName); + output.write(ProtoStream.FIELD_TYPE_STRING + | ProtoStream.FIELD_COUNT_SINGLE + | INSTALLER_FIELD_ID, + installer == null ? "" : installer); + numRecords++; + output.end(applicationInfoToken); + } + } + } + output.flush(); + if (DEBUG) { + Slog.d(TAG, "Sent data for " + numRecords + " apps"); + } + } finally { + IoUtils.closeQuietly(fout); + } + }); + } + + private final static class AppUpdateReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + /** + * App updates actually consist of REMOVE, ADD, and then REPLACE broadcasts. To avoid + * waste, we ignore the REMOVE and ADD broadcasts that contain the replacing flag. + * If we can't find the value for EXTRA_REPLACING, we default to false. + */ + if (!intent.getAction().equals(Intent.ACTION_PACKAGE_REPLACED) + && intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { + return; // Keep only replacing or normal add and remove. + } + if (DEBUG) Slog.d(TAG, "StatsCompanionService noticed an app was updated."); + synchronized (sStatsdLock) { + if (sStatsd == null) { + Slog.w(TAG, "Could not access statsd to inform it of an app update"); + return; + } + try { + if (intent.getAction().equals(Intent.ACTION_PACKAGE_REMOVED)) { + Bundle b = intent.getExtras(); + int uid = b.getInt(Intent.EXTRA_UID); + boolean replacing = intent.getBooleanExtra(Intent.EXTRA_REPLACING, false); + if (!replacing) { + // Don't bother sending an update if we're right about to get another + // intent for the new version that's added. + PackageManager pm = context.getPackageManager(); + String app = intent.getData().getSchemeSpecificPart(); + sStatsd.informOnePackageRemoved(app, uid); + } + } else { + PackageManager pm = context.getPackageManager(); + Bundle b = intent.getExtras(); + int uid = b.getInt(Intent.EXTRA_UID); + String app = intent.getData().getSchemeSpecificPart(); + PackageInfo pi = pm.getPackageInfo(app, PackageManager.MATCH_ANY_USER); + String installer; + try { + installer = pm.getInstallerPackageName(app); + } catch (IllegalArgumentException e) { + installer = ""; + } + sStatsd.informOnePackage( + app, + uid, + pi.getLongVersionCode(), + pi.versionName == null ? "" : pi.versionName, + installer == null ? "" : installer); + } + } catch (Exception e) { + Slog.w(TAG, "Failed to inform statsd of an app update", e); + } + } + } + } + + public final static class AnomalyAlarmListener implements OnAlarmListener { + @Override + public void onAlarm() { + Slog.i(TAG, "StatsCompanionService believes an anomaly has occurred at time " + + System.currentTimeMillis() + "ms."); + synchronized (sStatsdLock) { + if (sStatsd == null) { + Slog.w(TAG, "Could not access statsd to inform it of anomaly alarm firing"); + return; + } + try { + // Two-way call to statsd to retain AlarmManager wakelock + sStatsd.informAnomalyAlarmFired(); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to inform statsd of anomaly alarm firing", e); + } + } + // AlarmManager releases its own wakelock here. + } + } + + public final static class PullingAlarmListener implements OnAlarmListener { + @Override + public void onAlarm() { + if (DEBUG) { + Slog.d(TAG, "Time to poll something."); + } + synchronized (sStatsdLock) { + if (sStatsd == null) { + Slog.w(TAG, "Could not access statsd to inform it of pulling alarm firing."); + return; + } + try { + // Two-way call to statsd to retain AlarmManager wakelock + sStatsd.informPollAlarmFired(); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to inform statsd of pulling alarm firing.", e); + } + } + } + } + + public final static class PeriodicAlarmListener implements OnAlarmListener { + @Override + public void onAlarm() { + if (DEBUG) { + Slog.d(TAG, "Time to trigger periodic alarm."); + } + synchronized (sStatsdLock) { + if (sStatsd == null) { + Slog.w(TAG, "Could not access statsd to inform it of periodic alarm firing."); + return; + } + try { + // Two-way call to statsd to retain AlarmManager wakelock + sStatsd.informAlarmForSubscriberTriggeringFired(); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to inform statsd of periodic alarm firing.", e); + } + } + // AlarmManager releases its own wakelock here. + } + } + + public final static class ShutdownEventReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + /** + * Skip immediately if intent is not relevant to device shutdown. + */ + if (!intent.getAction().equals(Intent.ACTION_REBOOT) + && !(intent.getAction().equals(Intent.ACTION_SHUTDOWN) + && (intent.getFlags() & Intent.FLAG_RECEIVER_FOREGROUND) != 0)) { + return; + } + + Slog.i(TAG, "StatsCompanionService noticed a shutdown."); + synchronized (sStatsdLock) { + if (sStatsd == null) { + Slog.w(TAG, "Could not access statsd to inform it of a shutdown event."); + return; + } + try { + sStatsd.informDeviceShutdown(); + } catch (Exception e) { + Slog.w(TAG, "Failed to inform statsd of a shutdown event.", e); + } + } + } + } + + @Override // Binder call + public void setAnomalyAlarm(long timestampMs) { + enforceCallingPermission(); + if (DEBUG) Slog.d(TAG, "Setting anomaly alarm for " + timestampMs); + final long callingToken = Binder.clearCallingIdentity(); + try { + // using ELAPSED_REALTIME, not ELAPSED_REALTIME_WAKEUP, so if device is asleep, will + // only fire when it awakens. + // AlarmManager will automatically cancel any previous mAnomalyAlarmListener alarm. + mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME, timestampMs, TAG + ".anomaly", + mAnomalyAlarmListener, mHandler); + } finally { + Binder.restoreCallingIdentity(callingToken); + } + } + + @Override // Binder call + public void cancelAnomalyAlarm() { + enforceCallingPermission(); + if (DEBUG) Slog.d(TAG, "Cancelling anomaly alarm"); + final long callingToken = Binder.clearCallingIdentity(); + try { + mAlarmManager.cancel(mAnomalyAlarmListener); + } finally { + Binder.restoreCallingIdentity(callingToken); + } + } + + @Override // Binder call + public void setAlarmForSubscriberTriggering(long timestampMs) { + enforceCallingPermission(); + if (DEBUG) { + Slog.d(TAG, + "Setting periodic alarm in about " + (timestampMs + - SystemClock.elapsedRealtime())); + } + final long callingToken = Binder.clearCallingIdentity(); + try { + // using ELAPSED_REALTIME, not ELAPSED_REALTIME_WAKEUP, so if device is asleep, will + // only fire when it awakens. + mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME, timestampMs, TAG + ".periodic", + mPeriodicAlarmListener, mHandler); + } finally { + Binder.restoreCallingIdentity(callingToken); + } + } + + @Override // Binder call + public void cancelAlarmForSubscriberTriggering() { + enforceCallingPermission(); + if (DEBUG) { + Slog.d(TAG, "Cancelling periodic alarm"); + } + final long callingToken = Binder.clearCallingIdentity(); + try { + mAlarmManager.cancel(mPeriodicAlarmListener); + } finally { + Binder.restoreCallingIdentity(callingToken); + } + } + + @Override // Binder call + public void setPullingAlarm(long nextPullTimeMs) { + enforceCallingPermission(); + if (DEBUG) { + Slog.d(TAG, "Setting pulling alarm in about " + + (nextPullTimeMs - SystemClock.elapsedRealtime())); + } + final long callingToken = Binder.clearCallingIdentity(); + try { + // using ELAPSED_REALTIME, not ELAPSED_REALTIME_WAKEUP, so if device is asleep, will + // only fire when it awakens. + mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME, nextPullTimeMs, TAG + ".pull", + mPullingAlarmListener, mHandler); + } finally { + Binder.restoreCallingIdentity(callingToken); + } + } + + @Override // Binder call + public void cancelPullingAlarm() { + enforceCallingPermission(); + if (DEBUG) { + Slog.d(TAG, "Cancelling pulling alarm"); + } + final long callingToken = Binder.clearCallingIdentity(); + try { + mAlarmManager.cancel(mPullingAlarmListener); + } finally { + Binder.restoreCallingIdentity(callingToken); + } + } + + private void addNetworkStats( + int tag, List<StatsLogEventWrapper> ret, NetworkStats stats, boolean withFGBG) { + int size = stats.size(); + long elapsedNanos = SystemClock.elapsedRealtimeNanos(); + long wallClockNanos = SystemClock.currentTimeMicro() * 1000L; + NetworkStats.Entry entry = new NetworkStats.Entry(); // For recycling + for (int j = 0; j < size; j++) { + stats.getValues(j, entry); + StatsLogEventWrapper e = new StatsLogEventWrapper(tag, elapsedNanos, wallClockNanos); + e.writeInt(entry.uid); + if (withFGBG) { + e.writeInt(entry.set); + } + e.writeLong(entry.rxBytes); + e.writeLong(entry.rxPackets); + e.writeLong(entry.txBytes); + e.writeLong(entry.txPackets); + ret.add(e); + } + } + + /** + * Allows rollups per UID but keeping the set (foreground/background) slicing. + * Adapted from groupedByUid in frameworks/base/core/java/android/net/NetworkStats.java + */ + private NetworkStats rollupNetworkStatsByFGBG(NetworkStats stats) { + final NetworkStats ret = new NetworkStats(stats.getElapsedRealtime(), 1); + + final NetworkStats.Entry entry = new NetworkStats.Entry(); + entry.iface = NetworkStats.IFACE_ALL; + entry.tag = NetworkStats.TAG_NONE; + entry.metered = NetworkStats.METERED_ALL; + entry.roaming = NetworkStats.ROAMING_ALL; + + int size = stats.size(); + NetworkStats.Entry recycle = new NetworkStats.Entry(); // Used for retrieving values + for (int i = 0; i < size; i++) { + stats.getValues(i, recycle); + + // Skip specific tags, since already counted in TAG_NONE + if (recycle.tag != NetworkStats.TAG_NONE) continue; + + entry.set = recycle.set; // Allows slicing by background/foreground + entry.uid = recycle.uid; + entry.rxBytes = recycle.rxBytes; + entry.rxPackets = recycle.rxPackets; + entry.txBytes = recycle.txBytes; + entry.txPackets = recycle.txPackets; + // Operations purposefully omitted since we don't use them for statsd. + ret.combineValues(entry); + } + return ret; + } + + /** + * Helper method to extract the Parcelable controller info from a + * SynchronousResultReceiver. + */ + private static <T extends Parcelable> T awaitControllerInfo( + @Nullable SynchronousResultReceiver receiver) { + if (receiver == null) { + return null; + } + + try { + final SynchronousResultReceiver.Result result = + receiver.awaitResult(EXTERNAL_STATS_SYNC_TIMEOUT_MILLIS); + if (result.bundle != null) { + // This is the final destination for the Bundle. + result.bundle.setDefusable(true); + + final T data = result.bundle.getParcelable( + RESULT_RECEIVER_CONTROLLER_KEY); + if (data != null) { + return data; + } + } + Slog.e(TAG, "no controller energy info supplied for " + receiver.getName()); + } catch (TimeoutException e) { + Slog.w(TAG, "timeout reading " + receiver.getName() + " stats"); + } + return null; + } + + private void pullKernelWakelock( + int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + final KernelWakelockStats wakelockStats = + mKernelWakelockReader.readKernelWakelockStats(mTmpWakelockStats); + for (Map.Entry<String, KernelWakelockStats.Entry> ent : wakelockStats.entrySet()) { + String name = ent.getKey(); + KernelWakelockStats.Entry kws = ent.getValue(); + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeString(name); + e.writeInt(kws.mCount); + e.writeInt(kws.mVersion); + e.writeLong(kws.mTotalTime); + pulledData.add(e); + } + } + + private void pullWifiBytesTransfer( + int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + long token = Binder.clearCallingIdentity(); + try { + // TODO: Consider caching the following call to get BatteryStatsInternal. + BatteryStatsInternal bs = LocalServices.getService(BatteryStatsInternal.class); + String[] ifaces = bs.getWifiIfaces(); + if (ifaces.length == 0) { + return; + } + if (mNetworkStatsService == null) { + Slog.e(TAG, "NetworkStats Service is not available!"); + return; + } + // Combine all the metrics per Uid into one record. + NetworkStats stats = mNetworkStatsService.getDetailedUidStats(ifaces).groupedByUid(); + addNetworkStats(tagId, pulledData, stats, false); + } catch (RemoteException e) { + Slog.e(TAG, "Pulling netstats for wifi bytes has error", e); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + private void pullWifiBytesTransferByFgBg( + int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + long token = Binder.clearCallingIdentity(); + try { + BatteryStatsInternal bs = LocalServices.getService(BatteryStatsInternal.class); + String[] ifaces = bs.getWifiIfaces(); + if (ifaces.length == 0) { + return; + } + if (mNetworkStatsService == null) { + Slog.e(TAG, "NetworkStats Service is not available!"); + return; + } + NetworkStats stats = rollupNetworkStatsByFGBG( + mNetworkStatsService.getDetailedUidStats(ifaces)); + addNetworkStats(tagId, pulledData, stats, true); + } catch (RemoteException e) { + Slog.e(TAG, "Pulling netstats for wifi bytes w/ fg/bg has error", e); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + private void pullMobileBytesTransfer( + int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + long token = Binder.clearCallingIdentity(); + try { + BatteryStatsInternal bs = LocalServices.getService(BatteryStatsInternal.class); + String[] ifaces = bs.getMobileIfaces(); + if (ifaces.length == 0) { + return; + } + if (mNetworkStatsService == null) { + Slog.e(TAG, "NetworkStats Service is not available!"); + return; + } + // Combine all the metrics per Uid into one record. + NetworkStats stats = mNetworkStatsService.getDetailedUidStats(ifaces).groupedByUid(); + addNetworkStats(tagId, pulledData, stats, false); + } catch (RemoteException e) { + Slog.e(TAG, "Pulling netstats for mobile bytes has error", e); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + private void pullBluetoothBytesTransfer( + int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + BluetoothActivityEnergyInfo info = fetchBluetoothData(); + if (info.getUidTraffic() != null) { + for (UidTraffic traffic : info.getUidTraffic()) { + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, + wallClockNanos); + e.writeInt(traffic.getUid()); + e.writeLong(traffic.getRxBytes()); + e.writeLong(traffic.getTxBytes()); + pulledData.add(e); + } + } + } + + private void pullMobileBytesTransferByFgBg( + int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + long token = Binder.clearCallingIdentity(); + try { + BatteryStatsInternal bs = LocalServices.getService(BatteryStatsInternal.class); + String[] ifaces = bs.getMobileIfaces(); + if (ifaces.length == 0) { + return; + } + if (mNetworkStatsService == null) { + Slog.e(TAG, "NetworkStats Service is not available!"); + return; + } + NetworkStats stats = rollupNetworkStatsByFGBG( + mNetworkStatsService.getDetailedUidStats(ifaces)); + addNetworkStats(tagId, pulledData, stats, true); + } catch (RemoteException e) { + Slog.e(TAG, "Pulling netstats for mobile bytes w/ fg/bg has error", e); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + private void pullCpuTimePerFreq( + int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + for (int cluster = 0; cluster < mKernelCpuSpeedReaders.length; cluster++) { + long[] clusterTimeMs = mKernelCpuSpeedReaders[cluster].readAbsolute(); + if (clusterTimeMs != null) { + for (int speed = clusterTimeMs.length - 1; speed >= 0; --speed) { + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, + wallClockNanos); + e.writeInt(cluster); + e.writeInt(speed); + e.writeLong(clusterTimeMs[speed]); + pulledData.add(e); + } + } + } + } + + private void pullKernelUidCpuTime( + int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + mCpuUidUserSysTimeReader.readAbsolute((uid, timesUs) -> { + long userTimeUs = timesUs[0], systemTimeUs = timesUs[1]; + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeInt(uid); + e.writeLong(userTimeUs); + e.writeLong(systemTimeUs); + pulledData.add(e); + }); + } + + private void pullKernelUidCpuFreqTime( + int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + mCpuUidFreqTimeReader.readAbsolute((uid, cpuFreqTimeMs) -> { + for (int freqIndex = 0; freqIndex < cpuFreqTimeMs.length; ++freqIndex) { + if (cpuFreqTimeMs[freqIndex] != 0) { + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, + wallClockNanos); + e.writeInt(uid); + e.writeInt(freqIndex); + e.writeLong(cpuFreqTimeMs[freqIndex]); + pulledData.add(e); + } + } + }); + } + + private void pullKernelUidCpuClusterTime( + int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + mCpuUidClusterTimeReader.readAbsolute((uid, cpuClusterTimesMs) -> { + for (int i = 0; i < cpuClusterTimesMs.length; i++) { + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, + wallClockNanos); + e.writeInt(uid); + e.writeInt(i); + e.writeLong(cpuClusterTimesMs[i]); + pulledData.add(e); + } + }); + } + + private void pullKernelUidCpuActiveTime( + int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + mCpuUidActiveTimeReader.readAbsolute((uid, cpuActiveTimesMs) -> { + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeInt(uid); + e.writeLong((long) cpuActiveTimesMs); + pulledData.add(e); + }); + } + + private void pullWifiActivityInfo( + int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + long token = Binder.clearCallingIdentity(); + synchronized (this) { + if (mWifiManager == null) { + mWifiManager = + IWifiManager.Stub.asInterface( + ServiceManager.getService(Context.WIFI_SERVICE)); + } + } + if (mWifiManager != null) { + try { + SynchronousResultReceiver wifiReceiver = new SynchronousResultReceiver("wifi"); + mWifiManager.requestActivityInfo(wifiReceiver); + final WifiActivityEnergyInfo wifiInfo = awaitControllerInfo(wifiReceiver); + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, + wallClockNanos); + e.writeLong(wifiInfo.getTimeStamp()); + e.writeInt(wifiInfo.getStackState()); + e.writeLong(wifiInfo.getControllerTxTimeMillis()); + e.writeLong(wifiInfo.getControllerRxTimeMillis()); + e.writeLong(wifiInfo.getControllerIdleTimeMillis()); + e.writeLong(wifiInfo.getControllerEnergyUsed()); + pulledData.add(e); + } catch (RemoteException e) { + Slog.e(TAG, + "Pulling wifiManager for wifi controller activity energy info has error", + e); + } finally { + Binder.restoreCallingIdentity(token); + } + } + } + + private void pullModemActivityInfo( + int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + long token = Binder.clearCallingIdentity(); + synchronized (this) { + if (mTelephony == null) { + mTelephony = mContext.getSystemService(TelephonyManager.class); + } + } + if (mTelephony != null) { + SynchronousResultReceiver modemReceiver = new SynchronousResultReceiver("telephony"); + mTelephony.requestModemActivityInfo(modemReceiver); + final ModemActivityInfo modemInfo = awaitControllerInfo(modemReceiver); + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeLong(modemInfo.getTimestamp()); + e.writeLong(modemInfo.getSleepTimeMillis()); + e.writeLong(modemInfo.getIdleTimeMillis()); + e.writeLong(modemInfo.getTransmitPowerInfo().get(0).getTimeInMillis()); + e.writeLong(modemInfo.getTransmitPowerInfo().get(1).getTimeInMillis()); + e.writeLong(modemInfo.getTransmitPowerInfo().get(2).getTimeInMillis()); + e.writeLong(modemInfo.getTransmitPowerInfo().get(3).getTimeInMillis()); + e.writeLong(modemInfo.getTransmitPowerInfo().get(4).getTimeInMillis()); + e.writeLong(modemInfo.getReceiveTimeMillis()); + pulledData.add(e); + } + } + + private void pullBluetoothActivityInfo( + int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + BluetoothActivityEnergyInfo info = fetchBluetoothData(); + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeLong(info.getTimeStamp()); + e.writeInt(info.getBluetoothStackState()); + e.writeLong(info.getControllerTxTimeMillis()); + e.writeLong(info.getControllerRxTimeMillis()); + e.writeLong(info.getControllerIdleTimeMillis()); + e.writeLong(info.getControllerEnergyUsed()); + pulledData.add(e); + } + + private synchronized BluetoothActivityEnergyInfo fetchBluetoothData() { + final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); + if (adapter != null) { + SynchronousResultReceiver bluetoothReceiver = new SynchronousResultReceiver( + "bluetooth"); + adapter.requestControllerActivityEnergyInfo(bluetoothReceiver); + return awaitControllerInfo(bluetoothReceiver); + } else { + Slog.e(TAG, "Failed to get bluetooth adapter!"); + return null; + } + } + + private void pullSystemElapsedRealtime( + int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeLong(SystemClock.elapsedRealtime()); + pulledData.add(e); + } + + private void pullSystemUpTime(int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeLong(SystemClock.uptimeMillis()); + pulledData.add(e); + } + + private void pullProcessMemoryState( + int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + List<ProcessMemoryState> processMemoryStates = + LocalServices.getService( + ActivityManagerInternal.class).getMemoryStateForProcesses(); + for (ProcessMemoryState processMemoryState : processMemoryStates) { + final MemoryStat memoryStat = readMemoryStatFromFilesystem(processMemoryState.uid, + processMemoryState.pid); + if (memoryStat == null) { + continue; + } + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeInt(processMemoryState.uid); + e.writeString(processMemoryState.processName); + e.writeInt(processMemoryState.oomScore); + e.writeLong(memoryStat.pgfault); + e.writeLong(memoryStat.pgmajfault); + e.writeLong(memoryStat.rssInBytes); + e.writeLong(memoryStat.cacheInBytes); + e.writeLong(memoryStat.swapInBytes); + e.writeLong(-1); // unused + e.writeLong(-1); // unused + e.writeInt(-1); // unsed + pulledData.add(e); + } + } + + private void pullProcessMemoryHighWaterMark( + int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + List<ProcessMemoryState> managedProcessList = + LocalServices.getService( + ActivityManagerInternal.class).getMemoryStateForProcesses(); + for (ProcessMemoryState managedProcess : managedProcessList) { + final MemorySnapshot snapshot = readMemorySnapshotFromProcfs(managedProcess.pid); + if (snapshot == null) { + continue; + } + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeInt(managedProcess.uid); + e.writeString(managedProcess.processName); + // RSS high-water mark in bytes. + e.writeLong((long) snapshot.rssHighWaterMarkInKilobytes * 1024L); + e.writeInt(snapshot.rssHighWaterMarkInKilobytes); + pulledData.add(e); + } + forEachPid((pid, cmdLine) -> { + if (!MEMORY_INTERESTING_NATIVE_PROCESSES.contains(cmdLine)) { + return; + } + final MemorySnapshot snapshot = readMemorySnapshotFromProcfs(pid); + if (snapshot == null) { + return; + } + // Sometimes we get here a process that is not included in the whitelist. It comes + // from forking the zygote for an app. We can ignore that sample because this process + // is collected by ProcessMemoryState. + if (isAppUid(snapshot.uid)) { + return; + } + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeInt(snapshot.uid); + e.writeString(cmdLine); + // RSS high-water mark in bytes. + e.writeLong((long) snapshot.rssHighWaterMarkInKilobytes * 1024L); + e.writeInt(snapshot.rssHighWaterMarkInKilobytes); + pulledData.add(e); + }); + // Invoke rss_hwm_reset binary to reset RSS HWM counters for all processes. + SystemProperties.set("sys.rss_hwm_reset.on", "1"); + } + + private void pullProcessMemorySnapshot( + int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + List<ProcessMemoryState> managedProcessList = + LocalServices.getService( + ActivityManagerInternal.class).getMemoryStateForProcesses(); + for (ProcessMemoryState managedProcess : managedProcessList) { + final MemorySnapshot snapshot = readMemorySnapshotFromProcfs(managedProcess.pid); + if (snapshot == null) { + continue; + } + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeInt(managedProcess.uid); + e.writeString(managedProcess.processName); + e.writeInt(managedProcess.pid); + e.writeInt(managedProcess.oomScore); + e.writeInt(snapshot.rssInKilobytes); + e.writeInt(snapshot.anonRssInKilobytes); + e.writeInt(snapshot.swapInKilobytes); + e.writeInt(snapshot.anonRssInKilobytes + snapshot.swapInKilobytes); + pulledData.add(e); + } + forEachPid((pid, cmdLine) -> { + if (!MEMORY_INTERESTING_NATIVE_PROCESSES.contains(cmdLine)) { + return; + } + final MemorySnapshot snapshot = readMemorySnapshotFromProcfs(pid); + if (snapshot == null) { + return; + } + // Sometimes we get here a process that is not included in the whitelist. It comes + // from forking the zygote for an app. We can ignore that sample because this process + // is collected by ProcessMemoryState. + if (isAppUid(snapshot.uid)) { + return; + } + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeInt(snapshot.uid); + e.writeString(cmdLine); + e.writeInt(pid); + e.writeInt(-1001); // Placeholder for native processes, OOM_SCORE_ADJ_MIN - 1. + e.writeInt(snapshot.rssInKilobytes); + e.writeInt(snapshot.anonRssInKilobytes); + e.writeInt(snapshot.swapInKilobytes); + e.writeInt(snapshot.anonRssInKilobytes + snapshot.swapInKilobytes); + pulledData.add(e); + }); + } + + private static boolean isAppUid(int uid) { + return uid >= MIN_APP_UID; + } + + private void pullSystemIonHeapSize( + int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + final long systemIonHeapSizeInBytes = readSystemIonHeapSizeFromDebugfs(); + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeLong(systemIonHeapSizeInBytes); + pulledData.add(e); + } + + private void pullProcessSystemIonHeapSize( + int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + List<IonAllocations> result = readProcessSystemIonHeapSizesFromDebugfs(); + for (IonAllocations allocations : result) { + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeInt(getUidForPid(allocations.pid)); + e.writeString(readCmdlineFromProcfs(allocations.pid)); + e.writeInt((int) (allocations.totalSizeInBytes / 1024)); + e.writeInt(allocations.count); + e.writeInt((int) (allocations.maxSizeInBytes / 1024)); + pulledData.add(e); + } + } + + private void pullBinderCallsStats( + int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + BinderCallsStatsService.Internal binderStats = + LocalServices.getService(BinderCallsStatsService.Internal.class); + if (binderStats == null) { + throw new IllegalStateException("binderStats is null"); + } + + List<ExportedCallStat> callStats = binderStats.getExportedCallStats(); + binderStats.reset(); + for (ExportedCallStat callStat : callStats) { + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeInt(callStat.workSourceUid); + e.writeString(callStat.className); + e.writeString(callStat.methodName); + e.writeLong(callStat.callCount); + e.writeLong(callStat.exceptionCount); + e.writeLong(callStat.latencyMicros); + e.writeLong(callStat.maxLatencyMicros); + e.writeLong(callStat.cpuTimeMicros); + e.writeLong(callStat.maxCpuTimeMicros); + e.writeLong(callStat.maxReplySizeBytes); + e.writeLong(callStat.maxRequestSizeBytes); + e.writeLong(callStat.recordedCallCount); + e.writeInt(callStat.screenInteractive ? 1 : 0); + e.writeInt(callStat.callingUid); + pulledData.add(e); + } + } + + private void pullBinderCallsStatsExceptions( + int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + BinderCallsStatsService.Internal binderStats = + LocalServices.getService(BinderCallsStatsService.Internal.class); + if (binderStats == null) { + throw new IllegalStateException("binderStats is null"); + } + + ArrayMap<String, Integer> exceptionStats = binderStats.getExportedExceptionStats(); + // TODO: decouple binder calls exceptions with the rest of the binder calls data so that we + // can reset the exception stats. + for (Entry<String, Integer> entry : exceptionStats.entrySet()) { + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeString(entry.getKey()); + e.writeInt(entry.getValue()); + pulledData.add(e); + } + } + + private void pullLooperStats(int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + LooperStats looperStats = LocalServices.getService(LooperStats.class); + if (looperStats == null) { + throw new IllegalStateException("looperStats null"); + } + + List<LooperStats.ExportedEntry> entries = looperStats.getEntries(); + looperStats.reset(); + for (LooperStats.ExportedEntry entry : entries) { + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeInt(entry.workSourceUid); + e.writeString(entry.handlerClassName); + e.writeString(entry.threadName); + e.writeString(entry.messageName); + e.writeLong(entry.messageCount); + e.writeLong(entry.exceptionCount); + e.writeLong(entry.recordedMessageCount); + e.writeLong(entry.totalLatencyMicros); + e.writeLong(entry.cpuUsageMicros); + e.writeBoolean(entry.isInteractive); + e.writeLong(entry.maxCpuUsageMicros); + e.writeLong(entry.maxLatencyMicros); + e.writeLong(entry.recordedDelayMessageCount); + e.writeLong(entry.delayMillis); + e.writeLong(entry.maxDelayMillis); + pulledData.add(e); + } + } + + private void pullDiskStats(int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + // Run a quick-and-dirty performance test: write 512 bytes + byte[] junk = new byte[512]; + for (int i = 0; i < junk.length; i++) junk[i] = (byte) i; // Write nonzero bytes + + File tmp = new File(Environment.getDataDirectory(), "system/statsdperftest.tmp"); + FileOutputStream fos = null; + IOException error = null; + + long before = SystemClock.elapsedRealtime(); + try { + fos = new FileOutputStream(tmp); + fos.write(junk); + } catch (IOException e) { + error = e; + } finally { + try { + if (fos != null) fos.close(); + } catch (IOException e) { + // Do nothing. + } + } + + long latency = SystemClock.elapsedRealtime() - before; + if (tmp.exists()) tmp.delete(); + + if (error != null) { + Slog.e(TAG, "Error performing diskstats latency test"); + latency = -1; + } + // File based encryption. + boolean fileBased = StorageManager.isFileEncryptedNativeOnly(); + + //Recent disk write speed. Binder call to storaged. + int writeSpeed = -1; + try { + IBinder binder = ServiceManager.getService("storaged"); + if (binder == null) { + Slog.e(TAG, "storaged not found"); + } + IStoraged storaged = IStoraged.Stub.asInterface(binder); + writeSpeed = storaged.getRecentPerf(); + } catch (RemoteException e) { + Slog.e(TAG, "storaged not found"); + } + + // Add info pulledData. + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeLong(latency); + e.writeBoolean(fileBased); + e.writeInt(writeSpeed); + pulledData.add(e); + } + + private void pullDirectoryUsage(int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + StatFs statFsData = new StatFs(Environment.getDataDirectory().getAbsolutePath()); + StatFs statFsSystem = new StatFs(Environment.getRootDirectory().getAbsolutePath()); + StatFs statFsCache = new StatFs(Environment.getDownloadCacheDirectory().getAbsolutePath()); + + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeInt(StatsLog.DIRECTORY_USAGE__DIRECTORY__DATA); + e.writeLong(statFsData.getAvailableBytes()); + e.writeLong(statFsData.getTotalBytes()); + pulledData.add(e); + + e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeInt(StatsLog.DIRECTORY_USAGE__DIRECTORY__CACHE); + e.writeLong(statFsCache.getAvailableBytes()); + e.writeLong(statFsCache.getTotalBytes()); + pulledData.add(e); + + e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeInt(StatsLog.DIRECTORY_USAGE__DIRECTORY__SYSTEM); + e.writeLong(statFsSystem.getAvailableBytes()); + e.writeLong(statFsSystem.getTotalBytes()); + pulledData.add(e); + } + + private void pullAppSize(int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + try { + String jsonStr = IoUtils.readFileAsString(DiskStatsLoggingService.DUMPSYS_CACHE_PATH); + JSONObject json = new JSONObject(jsonStr); + long cache_time = json.optLong(DiskStatsFileLogger.LAST_QUERY_TIMESTAMP_KEY, -1L); + JSONArray pkg_names = json.getJSONArray(DiskStatsFileLogger.PACKAGE_NAMES_KEY); + JSONArray app_sizes = json.getJSONArray(DiskStatsFileLogger.APP_SIZES_KEY); + JSONArray app_data_sizes = json.getJSONArray(DiskStatsFileLogger.APP_DATA_KEY); + JSONArray app_cache_sizes = json.getJSONArray(DiskStatsFileLogger.APP_CACHES_KEY); + // Sanity check: Ensure all 4 lists have the same length. + int length = pkg_names.length(); + if (app_sizes.length() != length || app_data_sizes.length() != length + || app_cache_sizes.length() != length) { + Slog.e(TAG, "formatting error in diskstats cache file!"); + return; + } + for (int i = 0; i < length; i++) { + StatsLogEventWrapper e = + new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeString(pkg_names.getString(i)); + e.writeLong(app_sizes.optLong(i, -1L)); + e.writeLong(app_data_sizes.optLong(i, -1L)); + e.writeLong(app_cache_sizes.optLong(i, -1L)); + e.writeLong(cache_time); + pulledData.add(e); + } + } catch (IOException | JSONException e) { + Slog.e(TAG, "exception reading diskstats cache file", e); + } + } + + private void pullCategorySize(int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + try { + String jsonStr = IoUtils.readFileAsString(DiskStatsLoggingService.DUMPSYS_CACHE_PATH); + JSONObject json = new JSONObject(jsonStr); + long cacheTime = json.optLong(DiskStatsFileLogger.LAST_QUERY_TIMESTAMP_KEY, -1L); + + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeInt(StatsLog.CATEGORY_SIZE__CATEGORY__APP_SIZE); + e.writeLong(json.optLong(DiskStatsFileLogger.APP_SIZE_AGG_KEY, -1L)); + e.writeLong(cacheTime); + pulledData.add(e); + + e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeInt(StatsLog.CATEGORY_SIZE__CATEGORY__APP_DATA_SIZE); + e.writeLong(json.optLong(DiskStatsFileLogger.APP_DATA_SIZE_AGG_KEY, -1L)); + e.writeLong(cacheTime); + pulledData.add(e); + + e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeInt(StatsLog.CATEGORY_SIZE__CATEGORY__APP_CACHE_SIZE); + e.writeLong(json.optLong(DiskStatsFileLogger.APP_CACHE_AGG_KEY, -1L)); + e.writeLong(cacheTime); + pulledData.add(e); + + e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeInt(StatsLog.CATEGORY_SIZE__CATEGORY__PHOTOS); + e.writeLong(json.optLong(DiskStatsFileLogger.PHOTOS_KEY, -1L)); + e.writeLong(cacheTime); + pulledData.add(e); + + e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeInt(StatsLog.CATEGORY_SIZE__CATEGORY__VIDEOS); + e.writeLong(json.optLong(DiskStatsFileLogger.VIDEOS_KEY, -1L)); + e.writeLong(cacheTime); + pulledData.add(e); + + e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeInt(StatsLog.CATEGORY_SIZE__CATEGORY__AUDIO); + e.writeLong(json.optLong(DiskStatsFileLogger.AUDIO_KEY, -1L)); + e.writeLong(cacheTime); + pulledData.add(e); + + e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeInt(StatsLog.CATEGORY_SIZE__CATEGORY__DOWNLOADS); + e.writeLong(json.optLong(DiskStatsFileLogger.DOWNLOADS_KEY, -1L)); + e.writeLong(cacheTime); + pulledData.add(e); + + e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeInt(StatsLog.CATEGORY_SIZE__CATEGORY__SYSTEM); + e.writeLong(json.optLong(DiskStatsFileLogger.SYSTEM_KEY, -1L)); + e.writeLong(cacheTime); + pulledData.add(e); + + e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeInt(StatsLog.CATEGORY_SIZE__CATEGORY__OTHER); + e.writeLong(json.optLong(DiskStatsFileLogger.MISC_KEY, -1L)); + e.writeLong(cacheTime); + pulledData.add(e); + } catch (IOException | JSONException e) { + Slog.e(TAG, "exception reading diskstats cache file", e); + } + } + + private void pullNumBiometricsEnrolled(int modality, int tagId, long elapsedNanos, + long wallClockNanos, List<StatsLogEventWrapper> pulledData) { + final PackageManager pm = mContext.getPackageManager(); + FingerprintManager fingerprintManager = null; + FaceManager faceManager = null; + + if (pm.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) { + fingerprintManager = mContext.getSystemService( + FingerprintManager.class); + } + if (pm.hasSystemFeature(PackageManager.FEATURE_FACE)) { + faceManager = mContext.getSystemService(FaceManager.class); + } + + if (modality == BiometricsProtoEnums.MODALITY_FINGERPRINT && fingerprintManager == null) { + return; + } + if (modality == BiometricsProtoEnums.MODALITY_FACE && faceManager == null) { + return; + } + UserManager userManager = mContext.getSystemService(UserManager.class); + if (userManager == null) { + return; + } + + final long token = Binder.clearCallingIdentity(); + for (UserInfo user : userManager.getUsers()) { + final int userId = user.getUserHandle().getIdentifier(); + int numEnrolled = 0; + if (modality == BiometricsProtoEnums.MODALITY_FINGERPRINT) { + numEnrolled = fingerprintManager.getEnrolledFingerprints(userId).size(); + } else if (modality == BiometricsProtoEnums.MODALITY_FACE) { + numEnrolled = faceManager.getEnrolledFaces(userId).size(); + } else { + return; + } + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeInt(userId); + e.writeInt(numEnrolled); + pulledData.add(e); + } + Binder.restoreCallingIdentity(token); + } + + // read high watermark for section + private long readProcStatsHighWaterMark(int section) { + try { + File[] files = mBaseDir.listFiles((d, name) -> { + return name.toLowerCase().startsWith(String.valueOf(section) + '_'); + }); + if (files == null || files.length == 0) { + return 0; + } + if (files.length > 1) { + Log.e(TAG, "Only 1 file expected for high water mark. Found " + files.length); + } + return Long.valueOf(files[0].getName().split("_")[1]); + } catch (SecurityException e) { + Log.e(TAG, "Failed to get procstats high watermark file.", e); + } catch (NumberFormatException e) { + Log.e(TAG, "Failed to parse file name.", e); + } + return 0; + } + + private IProcessStats mProcessStats = + IProcessStats.Stub.asInterface(ServiceManager.getService(ProcessStats.SERVICE_NAME)); + + private void pullProcessStats(int section, int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + synchronized (this) { + try { + long lastHighWaterMark = readProcStatsHighWaterMark(section); + List<ParcelFileDescriptor> statsFiles = new ArrayList<>(); + long highWaterMark = mProcessStats.getCommittedStats( + lastHighWaterMark, section, true, statsFiles); + if (statsFiles.size() != 1) { + return; + } + InputStream stream = new ParcelFileDescriptor.AutoCloseInputStream( + statsFiles.get(0)); + int[] len = new int[1]; + byte[] stats = readFully(stream, len); + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, + wallClockNanos); + e.writeStorage(Arrays.copyOf(stats, len[0])); + pulledData.add(e); + new File(mBaseDir.getAbsolutePath() + "/" + section + "_" + + lastHighWaterMark).delete(); + new File( + mBaseDir.getAbsolutePath() + "/" + section + "_" + + highWaterMark).createNewFile(); + } catch (IOException e) { + Log.e(TAG, "Getting procstats failed: ", e); + } catch (RemoteException e) { + Log.e(TAG, "Getting procstats failed: ", e); + } catch (SecurityException e) { + Log.e(TAG, "Getting procstats failed: ", e); + } + } + } + + static byte[] readFully(InputStream stream, int[] outLen) throws IOException { + int pos = 0; + final int initialAvail = stream.available(); + byte[] data = new byte[initialAvail > 0 ? (initialAvail + 1) : 16384]; + while (true) { + int amt = stream.read(data, pos, data.length - pos); + if (DEBUG) { + Slog.i(TAG, "Read " + amt + " bytes at " + pos + " of avail " + data.length); + } + if (amt < 0) { + if (DEBUG) { + Slog.i(TAG, "**** FINISHED READING: pos=" + pos + " len=" + data.length); + } + outLen[0] = pos; + return data; + } + pos += amt; + if (pos >= data.length) { + byte[] newData = new byte[pos + 16384]; + if (DEBUG) { + Slog.i(TAG, "Copying " + pos + " bytes to new array len " + newData.length); + } + System.arraycopy(data, 0, newData, 0, pos); + data = newData; + } + } + } + + private void pullPowerProfile( + int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + PowerProfile powerProfile = new PowerProfile(mContext); + checkNotNull(powerProfile); + + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, + wallClockNanos); + ProtoOutputStream proto = new ProtoOutputStream(); + powerProfile.writeToProto(proto); + proto.flush(); + e.writeStorage(proto.getBytes()); + pulledData.add(e); + } + + private void pullBuildInformation(int tagId, + long elapsedNanos, long wallClockNanos, List<StatsLogEventWrapper> pulledData) { + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeString(Build.FINGERPRINT); + e.writeString(Build.BRAND); + e.writeString(Build.PRODUCT); + e.writeString(Build.DEVICE); + e.writeString(Build.VERSION.RELEASE); + e.writeString(Build.ID); + e.writeString(Build.VERSION.INCREMENTAL); + e.writeString(Build.TYPE); + e.writeString(Build.TAGS); + pulledData.add(e); + } + + private BatteryStatsHelper getBatteryStatsHelper() { + if (mBatteryStatsHelper == null) { + final long callingToken = Binder.clearCallingIdentity(); + try { + // clearCallingIdentity required for BatteryStatsHelper.checkWifiOnly(). + mBatteryStatsHelper = new BatteryStatsHelper(mContext, false); + } finally { + Binder.restoreCallingIdentity(callingToken); + } + mBatteryStatsHelper.create((Bundle) null); + } + long currentTime = SystemClock.elapsedRealtime(); + if (currentTime - mBatteryStatsHelperTimestampMs >= MAX_BATTERY_STATS_HELPER_FREQUENCY_MS) { + // Load BatteryStats and do all the calculations. + mBatteryStatsHelper.refreshStats(BatteryStats.STATS_SINCE_CHARGED, UserHandle.USER_ALL); + // Calculations are done so we don't need to save the raw BatteryStats data in RAM. + mBatteryStatsHelper.clearStats(); + mBatteryStatsHelperTimestampMs = currentTime; + } + return mBatteryStatsHelper; + } + + private long milliAmpHrsToNanoAmpSecs(double mAh) { + final long MILLI_AMP_HR_TO_NANO_AMP_SECS = 1_000_000L * 3600L; + return (long) (mAh * MILLI_AMP_HR_TO_NANO_AMP_SECS + 0.5); + } + + private void pullDeviceCalculatedPowerUse(int tagId, + long elapsedNanos, final long wallClockNanos, List<StatsLogEventWrapper> pulledData) { + BatteryStatsHelper bsHelper = getBatteryStatsHelper(); + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeLong(milliAmpHrsToNanoAmpSecs(bsHelper.getComputedPower())); + pulledData.add(e); + } + + private void pullDeviceCalculatedPowerBlameUid(int tagId, + long elapsedNanos, final long wallClockNanos, List<StatsLogEventWrapper> pulledData) { + final List<BatterySipper> sippers = getBatteryStatsHelper().getUsageList(); + if (sippers == null) { + return; + } + for (BatterySipper bs : sippers) { + if (bs.drainType != bs.drainType.APP) { + continue; + } + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeInt(bs.uidObj.getUid()); + e.writeLong(milliAmpHrsToNanoAmpSecs(bs.totalPowerMah)); + pulledData.add(e); + } + } + + private void pullDeviceCalculatedPowerBlameOther(int tagId, + long elapsedNanos, final long wallClockNanos, List<StatsLogEventWrapper> pulledData) { + final List<BatterySipper> sippers = getBatteryStatsHelper().getUsageList(); + if (sippers == null) { + return; + } + for (BatterySipper bs : sippers) { + if (bs.drainType == bs.drainType.APP) { + continue; // This is a separate atom; see pullDeviceCalculatedPowerBlameUid(). + } + if (bs.drainType == bs.drainType.USER) { + continue; // This is not supported. We purposefully calculate over USER_ALL. + } + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeInt(bs.drainType.ordinal()); + e.writeLong(milliAmpHrsToNanoAmpSecs(bs.totalPowerMah)); + pulledData.add(e); + } + } + + private void pullDiskIo(int tagId, long elapsedNanos, final long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + mStoragedUidIoStatsReader.readAbsolute((uid, fgCharsRead, fgCharsWrite, fgBytesRead, + fgBytesWrite, bgCharsRead, bgCharsWrite, bgBytesRead, bgBytesWrite, + fgFsync, bgFsync) -> { + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, + wallClockNanos); + e.writeInt(uid); + e.writeLong(fgCharsRead); + e.writeLong(fgCharsWrite); + e.writeLong(fgBytesRead); + e.writeLong(fgBytesWrite); + e.writeLong(bgCharsRead); + e.writeLong(bgCharsWrite); + e.writeLong(bgBytesRead); + e.writeLong(bgBytesWrite); + e.writeLong(fgFsync); + e.writeLong(bgFsync); + pulledData.add(e); + }); + } + + private void pullProcessCpuTime(int tagId, long elapsedNanos, final long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + synchronized (this) { + if (mProcessCpuTracker == null) { + mProcessCpuTracker = new ProcessCpuTracker(false); + mProcessCpuTracker.init(); + } + mProcessCpuTracker.update(); + for (int i = 0; i < mProcessCpuTracker.countStats(); i++) { + ProcessCpuTracker.Stats st = mProcessCpuTracker.getStats(i); + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, + wallClockNanos); + e.writeInt(st.uid); + e.writeString(st.name); + e.writeLong(st.base_utime); + e.writeLong(st.base_stime); + pulledData.add(e); + } + } + } + + private void pullCpuTimePerThreadFreq(int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + if (this.mKernelCpuThreadReader == null) { + throw new IllegalStateException("mKernelCpuThreadReader is null"); + } + ArrayList<KernelCpuThreadReader.ProcessCpuUsage> processCpuUsages = + this.mKernelCpuThreadReader.getProcessCpuUsageDiffed(); + if (processCpuUsages == null) { + throw new IllegalStateException("processCpuUsages is null"); + } + int[] cpuFrequencies = mKernelCpuThreadReader.getCpuFrequenciesKhz(); + if (cpuFrequencies.length > CPU_TIME_PER_THREAD_FREQ_MAX_NUM_FREQUENCIES) { + String message = "Expected maximum " + CPU_TIME_PER_THREAD_FREQ_MAX_NUM_FREQUENCIES + + " frequencies, but got " + cpuFrequencies.length; + Slog.w(TAG, message); + throw new IllegalStateException(message); + } + for (int i = 0; i < processCpuUsages.size(); i++) { + KernelCpuThreadReader.ProcessCpuUsage processCpuUsage = processCpuUsages.get(i); + ArrayList<KernelCpuThreadReader.ThreadCpuUsage> threadCpuUsages = + processCpuUsage.threadCpuUsages; + for (int j = 0; j < threadCpuUsages.size(); j++) { + KernelCpuThreadReader.ThreadCpuUsage threadCpuUsage = threadCpuUsages.get(j); + if (threadCpuUsage.usageTimesMillis.length != cpuFrequencies.length) { + String message = "Unexpected number of usage times," + + " expected " + cpuFrequencies.length + + " but got " + threadCpuUsage.usageTimesMillis.length; + Slog.w(TAG, message); + throw new IllegalStateException(message); + } + + StatsLogEventWrapper e = + new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeInt(processCpuUsage.uid); + e.writeInt(processCpuUsage.processId); + e.writeInt(threadCpuUsage.threadId); + e.writeString(processCpuUsage.processName); + e.writeString(threadCpuUsage.threadName); + for (int k = 0; k < CPU_TIME_PER_THREAD_FREQ_MAX_NUM_FREQUENCIES; k++) { + if (k < cpuFrequencies.length) { + e.writeInt(cpuFrequencies[k]); + e.writeInt(threadCpuUsage.usageTimesMillis[k]); + } else { + // If we have no more frequencies to write, we still must write empty data. + // We know that this data is empty (and not just zero) because all + // frequencies are expected to be greater than zero + e.writeInt(0); + e.writeInt(0); + } + } + pulledData.add(e); + } + } + } + + private void pullTemperature(int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + long callingToken = Binder.clearCallingIdentity(); + try { + List<Temperature> temperatures = sThermalService.getCurrentTemperatures(); + for (Temperature temp : temperatures) { + StatsLogEventWrapper e = + new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeInt(temp.getType()); + e.writeString(temp.getName()); + e.writeInt((int) (temp.getValue() * 10)); + e.writeInt(temp.getStatus()); + pulledData.add(e); + } + } catch (RemoteException e) { + // Should not happen. + Slog.e(TAG, "Disconnected from thermal service. Cannot pull temperatures."); + } finally { + Binder.restoreCallingIdentity(callingToken); + } + } + + private void pullCoolingDevices(int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + long callingToken = Binder.clearCallingIdentity(); + try { + List<CoolingDevice> devices = sThermalService.getCurrentCoolingDevices(); + for (CoolingDevice device : devices) { + StatsLogEventWrapper e = + new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeInt(device.getType()); + e.writeString(device.getName()); + e.writeInt((int) (device.getValue())); + pulledData.add(e); + } + } catch (RemoteException e) { + // Should not happen. + Slog.e(TAG, "Disconnected from thermal service. Cannot pull temperatures."); + } finally { + Binder.restoreCallingIdentity(callingToken); + } + } + + private void pullDebugElapsedClock(int tagId, + long elapsedNanos, final long wallClockNanos, List<StatsLogEventWrapper> pulledData) { + final long elapsedMillis = SystemClock.elapsedRealtime(); + final long clockDiffMillis = mDebugElapsedClockPreviousValue == 0 + ? 0 : elapsedMillis - mDebugElapsedClockPreviousValue; + + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeLong(mDebugElapsedClockPullCount); + e.writeLong(elapsedMillis); + // Log it twice to be able to test multi-value aggregation from ValueMetric. + e.writeLong(elapsedMillis); + e.writeLong(clockDiffMillis); + e.writeInt(1 /* always set */); + pulledData.add(e); + + if (mDebugElapsedClockPullCount % 2 == 1) { + StatsLogEventWrapper e2 = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e2.writeLong(mDebugElapsedClockPullCount); + e2.writeLong(elapsedMillis); + // Log it twice to be able to test multi-value aggregation from ValueMetric. + e2.writeLong(elapsedMillis); + e2.writeLong(clockDiffMillis); + e2.writeInt(2 /* set on odd pulls */); + pulledData.add(e2); + } + + mDebugElapsedClockPullCount++; + mDebugElapsedClockPreviousValue = elapsedMillis; + } + + private void pullDebugFailingElapsedClock(int tagId, + long elapsedNanos, final long wallClockNanos, List<StatsLogEventWrapper> pulledData) { + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + final long elapsedMillis = SystemClock.elapsedRealtime(); + // Fails every 5 buckets. + if (mDebugFailingElapsedClockPullCount++ % 5 == 0) { + mDebugFailingElapsedClockPreviousValue = elapsedMillis; + throw new RuntimeException("Failing debug elapsed clock"); + } + + e.writeLong(mDebugFailingElapsedClockPullCount); + e.writeLong(elapsedMillis); + // Log it twice to be able to test multi-value aggregation from ValueMetric. + e.writeLong(elapsedMillis); + e.writeLong(mDebugFailingElapsedClockPreviousValue == 0 + ? 0 : elapsedMillis - mDebugFailingElapsedClockPreviousValue); + mDebugFailingElapsedClockPreviousValue = elapsedMillis; + pulledData.add(e); + } + + private void pullDangerousPermissionState(long elapsedNanos, final long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + long token = Binder.clearCallingIdentity(); + try { + PackageManager pm = mContext.getPackageManager(); + + List<UserInfo> users = mContext.getSystemService(UserManager.class).getUsers(); + + int numUsers = users.size(); + for (int userNum = 0; userNum < numUsers; userNum++) { + UserHandle user = users.get(userNum).getUserHandle(); + + List<PackageInfo> pkgs = pm.getInstalledPackagesAsUser( + PackageManager.GET_PERMISSIONS, user.getIdentifier()); + + int numPkgs = pkgs.size(); + for (int pkgNum = 0; pkgNum < numPkgs; pkgNum++) { + PackageInfo pkg = pkgs.get(pkgNum); + + if (pkg.requestedPermissions == null) { + continue; + } + + int numPerms = pkg.requestedPermissions.length; + for (int permNum = 0; permNum < numPerms; permNum++) { + String permName = pkg.requestedPermissions[permNum]; + + PermissionInfo permissionInfo; + int permissionFlags = 0; + try { + permissionInfo = pm.getPermissionInfo(permName, 0); + permissionFlags = + pm.getPermissionFlags(permName, pkg.packageName, user); + + } catch (PackageManager.NameNotFoundException ignored) { + continue; + } + + if (permissionInfo.getProtection() != PROTECTION_DANGEROUS) { + continue; + } + + StatsLogEventWrapper e = new StatsLogEventWrapper( + StatsLog.DANGEROUS_PERMISSION_STATE, elapsedNanos, wallClockNanos); + + e.writeString(permName); + e.writeInt(pkg.applicationInfo.uid); + e.writeString(pkg.packageName); + e.writeBoolean((pkg.requestedPermissionsFlags[permNum] + & REQUESTED_PERMISSION_GRANTED) != 0); + e.writeInt(permissionFlags); + + pulledData.add(e); + } + } + } + } catch (Throwable t) { + Log.e(TAG, "Could not read permissions", t); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + private void pullAppOps(long elapsedNanos, final long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + long token = Binder.clearCallingIdentity(); + try { + AppOpsManager appOps = mContext.getSystemService(AppOpsManager.class); + + CompletableFuture<HistoricalOps> ops = new CompletableFuture<>(); + HistoricalOpsRequest histOpsRequest = + new HistoricalOpsRequest.Builder(0, Long.MAX_VALUE).build(); + appOps.getHistoricalOps(histOpsRequest, mContext.getMainExecutor(), ops::complete); + + HistoricalOps histOps = ops.get(EXTERNAL_STATS_SYNC_TIMEOUT_MILLIS, + TimeUnit.MILLISECONDS); + + for (int uidIdx = 0; uidIdx < histOps.getUidCount(); uidIdx++) { + final HistoricalUidOps uidOps = histOps.getUidOpsAt(uidIdx); + final int uid = uidOps.getUid(); + for (int pkgIdx = 0; pkgIdx < uidOps.getPackageCount(); pkgIdx++) { + final HistoricalPackageOps packageOps = uidOps.getPackageOpsAt(pkgIdx); + for (int opIdx = 0; opIdx < packageOps.getOpCount(); opIdx++) { + final AppOpsManager.HistoricalOp op = packageOps.getOpAt(opIdx); + StatsLogEventWrapper e = new StatsLogEventWrapper(StatsLog.APP_OPS, + elapsedNanos, wallClockNanos); + + e.writeInt(uid); + e.writeString(packageOps.getPackageName()); + e.writeInt(op.getOpCode()); + e.writeLong(op.getForegroundAccessCount(OP_FLAGS_ALL_TRUSTED)); + e.writeLong(op.getBackgroundAccessCount(OP_FLAGS_ALL_TRUSTED)); + e.writeLong(op.getForegroundRejectCount(OP_FLAGS_ALL_TRUSTED)); + e.writeLong(op.getBackgroundRejectCount(OP_FLAGS_ALL_TRUSTED)); + e.writeLong(op.getForegroundAccessDuration(OP_FLAGS_ALL_TRUSTED)); + e.writeLong(op.getBackgroundAccessDuration(OP_FLAGS_ALL_TRUSTED)); + pulledData.add(e); + } + } + } + } catch (Throwable t) { + Log.e(TAG, "Could not read appops", t); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + + /** + * Add a RoleHolder atom for each package that holds a role. + * + * @param elapsedNanos the time since boot + * @param wallClockNanos the time on the clock + * @param pulledData the data sink to write to + */ + private void pullRoleHolders(long elapsedNanos, final long wallClockNanos, + @NonNull List<StatsLogEventWrapper> pulledData) { + long callingToken = Binder.clearCallingIdentity(); + try { + PackageManager pm = mContext.getPackageManager(); + RoleManagerInternal rmi = LocalServices.getService(RoleManagerInternal.class); + + List<UserInfo> users = mContext.getSystemService(UserManager.class).getUsers(); + + int numUsers = users.size(); + for (int userNum = 0; userNum < numUsers; userNum++) { + int userId = users.get(userNum).getUserHandle().getIdentifier(); + + ArrayMap<String, ArraySet<String>> roles = rmi.getRolesAndHolders( + userId); + + int numRoles = roles.size(); + for (int roleNum = 0; roleNum < numRoles; roleNum++) { + String roleName = roles.keyAt(roleNum); + ArraySet<String> holders = roles.valueAt(roleNum); + + int numHolders = holders.size(); + for (int holderNum = 0; holderNum < numHolders; holderNum++) { + String holderName = holders.valueAt(holderNum); + + PackageInfo pkg; + try { + pkg = pm.getPackageInfoAsUser(holderName, 0, userId); + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Role holder " + holderName + " not found"); + return; + } + + StatsLogEventWrapper e = new StatsLogEventWrapper(StatsLog.ROLE_HOLDER, + elapsedNanos, wallClockNanos); + e.writeInt(pkg.applicationInfo.uid); + e.writeString(holderName); + e.writeString(roleName); + pulledData.add(e); + } + } + } + } finally { + Binder.restoreCallingIdentity(callingToken); + } + } + + private void pullTimeZoneDataInfo(int tagId, + long elapsedNanos, long wallClockNanos, List<StatsLogEventWrapper> pulledData) { + String tzDbVersion = "Unknown"; + try { + tzDbVersion = android.icu.util.TimeZone.getTZDataVersion(); + } catch (Exception e) { + Log.e(TAG, "Getting tzdb version failed: ", e); + } + + StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeString(tzDbVersion); + pulledData.add(e); + } + + private void pullExternalStorageInfo(int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + StorageManager storageManager = mContext.getSystemService(StorageManager.class); + if (storageManager != null) { + List<VolumeInfo> volumes = storageManager.getVolumes(); + for (VolumeInfo vol : volumes) { + final String envState = VolumeInfo.getEnvironmentForState(vol.getState()); + final DiskInfo diskInfo = vol.getDisk(); + if (diskInfo != null) { + if (envState.equals(Environment.MEDIA_MOUNTED)) { + // Get the type of the volume, if it is adoptable or portable. + int volumeType = StatsLog.EXTERNAL_STORAGE_INFO__VOLUME_TYPE__OTHER; + if (vol.getType() == TYPE_PUBLIC) { + volumeType = StatsLog.EXTERNAL_STORAGE_INFO__VOLUME_TYPE__PUBLIC; + } else if (vol.getType() == TYPE_PRIVATE) { + volumeType = StatsLog.EXTERNAL_STORAGE_INFO__VOLUME_TYPE__PRIVATE; + } + // Get the type of external storage inserted in the device (sd cards, + // usb, etc) + int externalStorageType; + if (diskInfo.isSd()) { + externalStorageType = StorageEnums.SD_CARD; + } else if (diskInfo.isUsb()) { + externalStorageType = StorageEnums.USB; + } else { + externalStorageType = StorageEnums.OTHER; + } + StatsLogEventWrapper e = + new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeInt(externalStorageType); + e.writeInt(volumeType); + e.writeLong(diskInfo.size); + pulledData.add(e); + } + } + } + } + } + + private void pullAppsOnExternalStorageInfo(int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + PackageManager pm = mContext.getPackageManager(); + StorageManager storage = mContext.getSystemService(StorageManager.class); + List<ApplicationInfo> apps = pm.getInstalledApplications(/* flags = */ 0); + for (ApplicationInfo appInfo : apps) { + UUID storageUuid = appInfo.storageUuid; + if (storageUuid != null) { + VolumeInfo volumeInfo = storage.findVolumeByUuid(appInfo.storageUuid.toString()); + if (volumeInfo != null) { + DiskInfo diskInfo = volumeInfo.getDisk(); + if (diskInfo != null) { + int externalStorageType = -1; + if (diskInfo.isSd()) { + externalStorageType = StorageEnums.SD_CARD; + } else if (diskInfo.isUsb()) { + externalStorageType = StorageEnums.USB; + } else if (appInfo.isExternal()) { + externalStorageType = StorageEnums.OTHER; + } + // App is installed on external storage. + if (externalStorageType != -1) { + StatsLogEventWrapper e = + new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeInt(externalStorageType); + e.writeString(appInfo.packageName); + pulledData.add(e); + } + } + } + } + } + } + + private void pullFaceSettings(int tagId, long elapsedNanos, long wallClockNanos, + List<StatsLogEventWrapper> pulledData) { + long callingToken = Binder.clearCallingIdentity(); + try { + List<UserInfo> users = mContext.getSystemService(UserManager.class).getUsers(); + int numUsers = users.size(); + for (int userNum = 0; userNum < numUsers; userNum++) { + int userId = users.get(userNum).getUserHandle().getIdentifier(); + + StatsLogEventWrapper e = + new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeBoolean(Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.FACE_UNLOCK_KEYGUARD_ENABLED, 1, + userId) != 0); + e.writeBoolean(Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.FACE_UNLOCK_DISMISSES_KEYGUARD, + 0, userId) != 0); + e.writeBoolean(Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.FACE_UNLOCK_ATTENTION_REQUIRED, 1, + userId) != 0); + e.writeBoolean(Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.FACE_UNLOCK_APP_ENABLED, 1, + userId) != 0); + e.writeBoolean(Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.FACE_UNLOCK_ALWAYS_REQUIRE_CONFIRMATION, 0, + userId) != 0); + e.writeBoolean(Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.FACE_UNLOCK_DIVERSITY_REQUIRED, 1, + userId) != 0); + + pulledData.add(e); + } + } finally { + Binder.restoreCallingIdentity(callingToken); + } + } + + /** + * Pulls various data. + */ + @Override // Binder call + public StatsLogEventWrapper[] pullData(int tagId) { + enforceCallingPermission(); + if (DEBUG) { + Slog.d(TAG, "Pulling " + tagId); + } + List<StatsLogEventWrapper> ret = new ArrayList<>(); + long elapsedNanos = SystemClock.elapsedRealtimeNanos(); + long wallClockNanos = SystemClock.currentTimeMicro() * 1000L; + switch (tagId) { + case StatsLog.WIFI_BYTES_TRANSFER: { + pullWifiBytesTransfer(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.MOBILE_BYTES_TRANSFER: { + pullMobileBytesTransfer(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.WIFI_BYTES_TRANSFER_BY_FG_BG: { + pullWifiBytesTransferByFgBg(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.MOBILE_BYTES_TRANSFER_BY_FG_BG: { + pullMobileBytesTransferByFgBg(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.BLUETOOTH_BYTES_TRANSFER: { + pullBluetoothBytesTransfer(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.KERNEL_WAKELOCK: { + pullKernelWakelock(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.CPU_TIME_PER_FREQ: { + pullCpuTimePerFreq(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.CPU_TIME_PER_UID: { + pullKernelUidCpuTime(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.CPU_TIME_PER_UID_FREQ: { + pullKernelUidCpuFreqTime(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.CPU_CLUSTER_TIME: { + pullKernelUidCpuClusterTime(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.CPU_ACTIVE_TIME: { + pullKernelUidCpuActiveTime(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.WIFI_ACTIVITY_INFO: { + pullWifiActivityInfo(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.MODEM_ACTIVITY_INFO: { + pullModemActivityInfo(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.BLUETOOTH_ACTIVITY_INFO: { + pullBluetoothActivityInfo(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.SYSTEM_UPTIME: { + pullSystemUpTime(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.SYSTEM_ELAPSED_REALTIME: { + pullSystemElapsedRealtime(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.PROCESS_MEMORY_STATE: { + pullProcessMemoryState(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.PROCESS_MEMORY_HIGH_WATER_MARK: { + pullProcessMemoryHighWaterMark(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.PROCESS_MEMORY_SNAPSHOT: { + pullProcessMemorySnapshot(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.SYSTEM_ION_HEAP_SIZE: { + pullSystemIonHeapSize(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.PROCESS_SYSTEM_ION_HEAP_SIZE: { + pullProcessSystemIonHeapSize(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.BINDER_CALLS: { + pullBinderCallsStats(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.BINDER_CALLS_EXCEPTIONS: { + pullBinderCallsStatsExceptions(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.LOOPER_STATS: { + pullLooperStats(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.DISK_STATS: { + pullDiskStats(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.DIRECTORY_USAGE: { + pullDirectoryUsage(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.APP_SIZE: { + pullAppSize(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.CATEGORY_SIZE: { + pullCategorySize(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.NUM_FINGERPRINTS_ENROLLED: { + pullNumBiometricsEnrolled(BiometricsProtoEnums.MODALITY_FINGERPRINT, tagId, + elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.NUM_FACES_ENROLLED: { + pullNumBiometricsEnrolled(BiometricsProtoEnums.MODALITY_FACE, tagId, elapsedNanos, + wallClockNanos, ret); + break; + } + case StatsLog.PROC_STATS: { + pullProcessStats(ProcessStats.REPORT_ALL, tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.PROC_STATS_PKG_PROC: { + pullProcessStats(ProcessStats.REPORT_PKG_PROC_STATS, tagId, elapsedNanos, + wallClockNanos, ret); + break; + } + case StatsLog.DISK_IO: { + pullDiskIo(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.POWER_PROFILE: { + pullPowerProfile(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.BUILD_INFORMATION: { + pullBuildInformation(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.PROCESS_CPU_TIME: { + pullProcessCpuTime(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.CPU_TIME_PER_THREAD_FREQ: { + pullCpuTimePerThreadFreq(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.DEVICE_CALCULATED_POWER_USE: { + pullDeviceCalculatedPowerUse(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.DEVICE_CALCULATED_POWER_BLAME_UID: { + pullDeviceCalculatedPowerBlameUid(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.DEVICE_CALCULATED_POWER_BLAME_OTHER: { + pullDeviceCalculatedPowerBlameOther(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.TEMPERATURE: { + pullTemperature(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.COOLING_DEVICE: { + pullCoolingDevices(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.DEBUG_ELAPSED_CLOCK: { + pullDebugElapsedClock(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.DEBUG_FAILING_ELAPSED_CLOCK: { + pullDebugFailingElapsedClock(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.ROLE_HOLDER: { + pullRoleHolders(elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.DANGEROUS_PERMISSION_STATE: { + pullDangerousPermissionState(elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.TIME_ZONE_DATA_INFO: { + pullTimeZoneDataInfo(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.EXTERNAL_STORAGE_INFO: { + pullExternalStorageInfo(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.APPS_ON_EXTERNAL_STORAGE_INFO: { + pullAppsOnExternalStorageInfo(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.FACE_SETTINGS: { + pullFaceSettings(tagId, elapsedNanos, wallClockNanos, ret); + break; + } + case StatsLog.APP_OPS: { + pullAppOps(elapsedNanos, wallClockNanos, ret); + break; + } + default: + Slog.w(TAG, "No such tagId data as " + tagId); + return null; + } + return ret.toArray(new StatsLogEventWrapper[ret.size()]); + } + + @Override // Binder call + public void statsdReady() { + enforceCallingPermission(); + if (DEBUG) { + Slog.d(TAG, "learned that statsdReady"); + } + sayHiToStatsd(); // tell statsd that we're ready too and link to it + mContext.sendBroadcastAsUser(new Intent(StatsManager.ACTION_STATSD_STARTED) + .addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND), + UserHandle.SYSTEM, android.Manifest.permission.DUMP); + } + + @Override + public void triggerUidSnapshot() { + enforceCallingPermission(); + synchronized (sStatsdLock) { + final long token = Binder.clearCallingIdentity(); + try { + informAllUidsLocked(mContext); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to trigger uid snapshot.", e); + } finally { + restoreCallingIdentity(token); + } + } + } + + private void enforceCallingPermission() { + if (Binder.getCallingPid() == Process.myPid()) { + return; + } + mContext.enforceCallingPermission(android.Manifest.permission.STATSCOMPANION, null); + } + + @Override + public void registerPullAtomCallback(int atomTag, long coolDownNs, long timeoutNs, + int[] additiveFields, IPullAtomCallback pullerCallback) { + synchronized (sStatsdLock) { + // Always cache the puller in SCS. + // If statsd is down, we will register it when it comes back up. + int callingUid = Binder.getCallingUid(); + final long token = Binder.clearCallingIdentity(); + PullerKey key = new PullerKey(callingUid, atomTag); + PullerValue val = new PullerValue( + coolDownNs, timeoutNs, additiveFields, pullerCallback); + mPullers.put(key, val); + + if (sStatsd == null) { + Slog.w(TAG, "Could not access statsd for registering puller for atom " + atomTag); + return; + } + try { + sStatsd.registerPullAtomCallback( + callingUid, atomTag, coolDownNs, timeoutNs, additiveFields, pullerCallback); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to access statsd to register puller for atom " + atomTag); + } finally { + Binder.restoreCallingIdentity(token); + } + } + } + + // Lifecycle and related code + + /** + * Fetches the statsd IBinder service. + * Note: This should only be called from sayHiToStatsd. All other clients should use the cached + * sStatsd with a null check. + */ + private static IStatsManager fetchStatsdService() { + return IStatsManager.Stub.asInterface(ServiceManager.getService("stats")); + } + + public static final class Lifecycle extends SystemService { + private StatsCompanionService mStatsCompanionService; + + public Lifecycle(Context context) { + super(context); + } + + @Override + public void onStart() { + mStatsCompanionService = new StatsCompanionService(getContext()); + try { + publishBinderService(Context.STATS_COMPANION_SERVICE, + mStatsCompanionService); + if (DEBUG) Slog.d(TAG, "Published " + Context.STATS_COMPANION_SERVICE); + } catch (Exception e) { + Slog.e(TAG, "Failed to publishBinderService", e); + } + } + + @Override + public void onBootPhase(int phase) { + super.onBootPhase(phase); + if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) { + mStatsCompanionService.systemReady(); + } + } + } + + /** + * Now that the android system is ready, StatsCompanion is ready too, so inform statsd. + */ + private void systemReady() { + if (DEBUG) Slog.d(TAG, "Learned that systemReady"); + sayHiToStatsd(); + } + + /** + * Tells statsd that statscompanion is ready. If the binder call returns, link to + * statsd. + */ + private void sayHiToStatsd() { + synchronized (sStatsdLock) { + if (sStatsd != null) { + Slog.e(TAG, "Trying to fetch statsd, but it was already fetched", + new IllegalStateException( + "sStatsd is not null when being fetched")); + return; + } + sStatsd = fetchStatsdService(); + if (sStatsd == null) { + Slog.i(TAG, + "Could not yet find statsd to tell it that StatsCompanion is " + + "alive."); + return; + } + if (DEBUG) Slog.d(TAG, "Saying hi to statsd"); + try { + sStatsd.statsCompanionReady(); + // If the statsCompanionReady two-way binder call returns, link to statsd. + try { + sStatsd.asBinder().linkToDeath(new StatsdDeathRecipient(), 0); + } catch (RemoteException e) { + Slog.e(TAG, "linkToDeath(StatsdDeathRecipient) failed", e); + forgetEverythingLocked(); + } + // Setup broadcast receiver for updates. + IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_REPLACED); + filter.addAction(Intent.ACTION_PACKAGE_ADDED); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addDataScheme("package"); + mContext.registerReceiverAsUser(mAppUpdateReceiver, UserHandle.ALL, filter, + null, + null); + + // Setup receiver for user initialize (which happens once for a new user) + // and + // if a user is removed. + filter = new IntentFilter(Intent.ACTION_USER_INITIALIZE); + filter.addAction(Intent.ACTION_USER_REMOVED); + mContext.registerReceiverAsUser(mUserUpdateReceiver, UserHandle.ALL, + filter, null, null); + + // Setup receiver for device reboots or shutdowns. + filter = new IntentFilter(Intent.ACTION_REBOOT); + filter.addAction(Intent.ACTION_SHUTDOWN); + mContext.registerReceiverAsUser( + mShutdownEventReceiver, UserHandle.ALL, filter, null, null); + final long token = Binder.clearCallingIdentity(); + try { + // Pull the latest state of UID->app name, version mapping when + // statsd starts. + informAllUidsLocked(mContext); + // Register all pullers. If SCS has just started, this should be empty. + registerAllPullersLocked(); + } finally { + restoreCallingIdentity(token); + } + Slog.i(TAG, "Told statsd that StatsCompanionService is alive."); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to inform statsd that statscompanion is ready", e); + forgetEverythingLocked(); + } + } + } + + @GuardedBy("sStatsdLock") + private void registerAllPullersLocked() throws RemoteException { + // TODO: pass in one call, using a file descriptor (similar to uidmap). + for (Map.Entry<PullerKey, PullerValue> entry : mPullers.entrySet()) { + PullerKey key = entry.getKey(); + PullerValue val = entry.getValue(); + sStatsd.registerPullAtomCallback(key.getUid(), key.getAtom(), val.getCoolDownNs(), + val.getTimeoutNs(), val.getAdditiveFields(), val.getCallback()); + } + } + + private class StatsdDeathRecipient implements IBinder.DeathRecipient { + @Override + public void binderDied() { + Slog.i(TAG, "Statsd is dead - erase all my knowledge, except pullers"); + synchronized (sStatsdLock) { + long now = SystemClock.elapsedRealtime(); + for (Long timeMillis : mDeathTimeMillis) { + long ageMillis = now - timeMillis; + if (ageMillis > MILLIS_IN_A_DAY) { + mDeathTimeMillis.remove(timeMillis); + } + } + for (Long timeMillis : mDeletedFiles.keySet()) { + long ageMillis = now - timeMillis; + if (ageMillis > MILLIS_IN_A_DAY * 7) { + mDeletedFiles.remove(timeMillis); + } + } + mDeathTimeMillis.add(now); + if (mDeathTimeMillis.size() >= DEATH_THRESHOLD) { + mDeathTimeMillis.clear(); + File[] configs = FileUtils.listFilesOrEmpty(new File(CONFIG_DIR)); + if (configs.length > 0) { + String fileName = configs[0].getName(); + if (configs[0].delete()) { + mDeletedFiles.put(now, fileName); + } + } + } + forgetEverythingLocked(); + } + } + } + + @GuardedBy("StatsCompanionService.sStatsdLock") + private void forgetEverythingLocked() { + sStatsd = null; + mContext.unregisterReceiver(mAppUpdateReceiver); + mContext.unregisterReceiver(mUserUpdateReceiver); + mContext.unregisterReceiver(mShutdownEventReceiver); + cancelAnomalyAlarm(); + cancelPullingAlarm(); + + BinderCallsStatsService.Internal binderStats = + LocalServices.getService(BinderCallsStatsService.Internal.class); + if (binderStats != null) { + binderStats.reset(); + } + + LooperStats looperStats = LocalServices.getService(LooperStats.class); + if (looperStats != null) { + looperStats.reset(); + } + } + + @Override + protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { + if (!DumpUtils.checkDumpPermission(mContext, TAG, writer)) return; + + synchronized (sStatsdLock) { + writer.println( + "Number of configuration files deleted: " + mDeletedFiles.size()); + if (mDeletedFiles.size() > 0) { + writer.println(" timestamp, deleted file name"); + } + long lastBootMillis = + SystemClock.currentThreadTimeMillis() - SystemClock.elapsedRealtime(); + for (Long elapsedMillis : mDeletedFiles.keySet()) { + long deletionMillis = lastBootMillis + elapsedMillis; + writer.println( + " " + deletionMillis + ", " + mDeletedFiles.get(elapsedMillis)); + } + } + } + + // Thermal event received from vendor thermal management subsystem + private static final class ThermalEventListener extends IThermalEventListener.Stub { + @Override + public void notifyThrottling(Temperature temp) { + StatsLog.write(StatsLog.THERMAL_THROTTLING_SEVERITY_STATE_CHANGED, temp.getType(), + temp.getName(), (int) (temp.getValue() * 10), temp.getStatus()); + } + } + + private static final class ConnectivityStatsCallback extends + ConnectivityManager.NetworkCallback { + @Override + public void onAvailable(Network network) { + StatsLog.write(StatsLog.CONNECTIVITY_STATE_CHANGED, network.netId, + StatsLog.CONNECTIVITY_STATE_CHANGED__STATE__CONNECTED); + } + + @Override + public void onLost(Network network) { + StatsLog.write(StatsLog.CONNECTIVITY_STATE_CHANGED, network.netId, + StatsLog.CONNECTIVITY_STATE_CHANGED__STATE__DISCONNECTED); + } + } +} |