summaryrefslogtreecommitdiff
path: root/apex
diff options
context:
space:
mode:
Diffstat (limited to 'apex')
-rw-r--r--apex/blobstore/framework/Android.bp40
-rw-r--r--apex/blobstore/framework/java/android/app/blob/BlobStoreManager.java41
-rw-r--r--apex/blobstore/framework/java/android/app/blob/BlobStoreManagerFrameworkInitializer.java34
-rw-r--r--apex/blobstore/framework/java/android/app/blob/IBlobStoreManager.aidl20
-rw-r--r--apex/blobstore/service/Android.bp27
-rw-r--r--apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java39
-rw-r--r--apex/jobscheduler/OWNERS6
-rw-r--r--apex/jobscheduler/README_js-mainline.md20
-rw-r--r--apex/jobscheduler/framework/Android.bp29
-rw-r--r--apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java122
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl68
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl38
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/IJobService.aidl34
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/JobInfo.aidl19
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/JobInfo.java1597
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/JobParameters.aidl19
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/JobParameters.java395
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/JobScheduler.java191
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/JobSchedulerFrameworkInitializer.java52
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/JobService.java157
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java220
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/JobSnapshot.aidl19
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/JobSnapshot.java119
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/JobWorkItem.aidl20
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/JobWorkItem.java212
-rw-r--r--apex/jobscheduler/framework/java/android/os/DeviceIdleManager.java100
-rw-r--r--apex/jobscheduler/framework/java/android/os/IDeviceIdleController.aidl51
-rw-r--r--apex/jobscheduler/framework/java/com/android/server/DeviceIdleInternal.java72
-rw-r--r--apex/jobscheduler/framework/java/com/android/server/deviceidle/ConstraintController.java32
-rw-r--r--apex/jobscheduler/framework/java/com/android/server/deviceidle/IDeviceIdleConstraint.java67
-rw-r--r--apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java119
-rw-r--r--apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java130
-rw-r--r--apex/jobscheduler/service/Android.bp15
-rw-r--r--apex/jobscheduler/service/java/com/android/server/AnyMotionDetector.java520
-rw-r--r--apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java4586
-rw-r--r--apex/jobscheduler/service/java/com/android/server/deviceidle/BluetoothConstraint.java137
-rw-r--r--apex/jobscheduler/service/java/com/android/server/deviceidle/DeviceIdleConstraintTracker.java57
-rw-r--r--apex/jobscheduler/service/java/com/android/server/deviceidle/TvConstraintController.java66
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/GrantedUriPermissions.java174
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/JobCompletedListener.java31
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java725
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/JobPackageTracker.java653
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java3353
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java428
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java852
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/JobStore.java1272
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/StateChangedListener.java42
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java246
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java282
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java672
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/ContentObserverController.java544
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java313
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java139
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java1867
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java2688
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java145
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/StorageController.java200
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/TimeController.java604
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/idle/CarIdlenessTracker.java193
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/idle/DeviceIdlenessTracker.java196
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/idle/IdlenessListener.java32
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/idle/IdlenessTracker.java52
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java72
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java74
-rw-r--r--apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java699
-rw-r--r--apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java1754
-rw-r--r--apex/permission/Android.bp33
-rw-r--r--apex/permission/OWNERS6
-rw-r--r--apex/permission/apex_manifest.json4
-rw-r--r--apex/permission/com.android.permission.avbpubkeybin0 -> 1032 bytes
-rw-r--r--apex/permission/com.android.permission.pem51
-rw-r--r--apex/permission/com.android.permission.pk8bin0 -> 2376 bytes
-rw-r--r--apex/permission/com.android.permission.x509.pem35
-rw-r--r--apex/statsd/.clang-format17
-rw-r--r--apex/statsd/Android.bp46
-rw-r--r--apex/statsd/OWNERS9
-rw-r--r--apex/statsd/apex_manifest.json5
-rw-r--r--apex/statsd/com.android.os.statsd.avbpubkeybin0 -> 1032 bytes
-rw-r--r--apex/statsd/com.android.os.statsd.pem51
-rw-r--r--apex/statsd/com.android.os.statsd.pk8bin0 -> 2375 bytes
-rw-r--r--apex/statsd/com.android.os.statsd.x509.pem30
-rw-r--r--apex/statsd/service/Android.bp16
-rw-r--r--apex/statsd/service/java/com/android/server/stats/StatsCompanionService.java2876
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">
+ * &#60;service android:name="MyJobService"
+ * android:permission="android.permission.BIND_JOB_SERVICE" &#62;
+ * ...
+ * &#60;/service&#62;
+ * </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>"&lt;null&gt;"</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
new file mode 100644
index 000000000000..9eaf85259637
--- /dev/null
+++ b/apex/permission/com.android.permission.avbpubkey
Binary files differ
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
new file mode 100644
index 000000000000..d51673dbc2fc
--- /dev/null
+++ b/apex/permission/com.android.permission.pk8
Binary files differ
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
new file mode 100644
index 000000000000..d78af8b8bef2
--- /dev/null
+++ b/apex/statsd/com.android.os.statsd.avbpubkey
Binary files differ
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
new file mode 100644
index 000000000000..49910f80a05c
--- /dev/null
+++ b/apex/statsd/com.android.os.statsd.pk8
Binary files differ
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);
+ }
+ }
+}